From 86c2c13e0f41807dda18959a6f571e4b54db894f Mon Sep 17 00:00:00 2001 From: Tadgh Date: Sun, 24 Nov 2024 12:31:59 -0800 Subject: [PATCH 1/6] 7.6.0 Mergeback (#6494) * 6323 resource creation deadlock (#6324) * make map reads concurrent * change log * CUstom version number for guaranteed build determinism * licenses * Expand translation cache (#6341) * Expand translation cache * Add changelog * Correction to #6341 (#6342) * Contained bug (#6402) * Contained bug * more tests * changelog, tests, implementation * code review * backwards logic * Fix Questionnaire doc (#6400) * fix reindex optimizeStorage=ALL_VERSIONS (#6421) * fixing broken rename of last step of reindex (#6429) * Fixes for the translation of parameter issues as part of the output for $validate-code operation. (#6438) * Move the validation providers to the test utilities package such that they can be reused. * Update IValidationSupport.CodeValidationIssue structure such that it meets the FHIR specification. Update RemoteTerminologyServiceValidationSupport and VersionSpecificWorkerContextWrapper translation for issues. * New tests for issue translation. Move test class to a different package so that we can add another test class. * Some simplification in the issue API to simplify building of issues. * Fix compilation errors. * Update providers to allow multiple responses and add support for the fetchCodeSystem call through method find. * Setup the first test for resource validation with remote terminology providers. * Fix NullPointerException * avoid calling validateCode for CodeSystem where system is null * Keep old public API methods (and class name) in IValidationSupport and mark them as deprecated to avoid breaking dependencies. * Revert local change to debug for duplicate errors. * Add more test cases for resource validation. Throw exception to signal missing test setup to make it obvious. * Simplify test setup. * Add some more javadoc * Add javadoc for the new test class * Add more tests * Address code review comments in IValidationSupport. * Add changelog * Change Repository search interface from Map to Multimap (#6445) Change Map to Multimap to support multiple and clauses. * licenses * fix interceptor hooks from requestDetails not getting called for STORAGE_PRECHECK_FOR_CACHED_SEARCH (#6436) * fix interceptor hooks from requestDetails not getting called for STORAGE_PRECHECK_FOR_CACHED_SEARCH * added unit tests and updated changelog * added one more test case * Add Adapter api (#6450) Lightweight implementation of the adapter pattern. * Bulk Import job status not changed after activation - failing test, fix, changelog (#6452) * Update CR to 3.13.1 (#6433) * Automated Migration Testing (HAPI-FHIR) - updated test migration scripts for 7_4_0 (#6439) * Rel 7 6 CVE (#6446) * Bump commons io * Bump HS and Lucene, and hibernate * Jetty bump for CVE * Resolve CVES * Bump with mismatch lucene * Bump velocity template engine * Revert bom bump * wip * Version bump * move changelog, fix cve * Bump commons-lang * Replace imports * wip * fix HQL break * remove dead code * Fix changelog entry * Bump org.hl7.fhir.core to 6.4.0 (#6454) * Bump HAPI version + org.hl7.fhir.core to 6.4.0 * Apply spotless * Correct a bug with duplicate parser IDs being assigned. (#6456) * Fix changelog entry * wip * compilation problem * Correct tests * changelog * Update hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java Co-authored-by: volodymyr-korzh <132366313+volodymyr-korzh@users.noreply.github.com> * Correct a bug with duplicate parser IDs being assigned - test fixes * Correct a bug with duplicate parser IDs being assigned - spotless * Correct a bug with duplicate parser IDs being assigned - added test-utilities dependency to fhir-structures poms * Correct a bug with duplicate parser IDs being assigned - test fixes * Contained resources without assigned IDs are now assigned GUIDs - address comments --------- Co-authored-by: volodymyr-korzh <132366313+volodymyr-korzh@users.noreply.github.com> Co-authored-by: volodymyr * licenses * ValueSet expansion fails if Hibernate Search configured to use Lucene - fixed incompatibility between Hibernate Search and Lucene versions (#6468) * Change the migrator to avoid table locks when adding an index. (#6489) * version bump * Updating version to: 7.6.1 post release. * Bump to 7.7.7. * deadsapce * Profile reference param (#6501) * updated QueryStack to not throw error with _profile as a ReferenceParam * update QueryStack * remove warnings * cleanup cast * cleanup test * add changelog * cleanup imports * updates per review feedback * updates per review feedback --------- Co-authored-by: taha.attari@smilecdr.com * move changelog --------- Co-authored-by: JasonRoberts-smile <85363818+JasonRoberts-smile@users.noreply.github.com> Co-authored-by: James Agnew Co-authored-by: Brenin Rhodes Co-authored-by: Emre Dincturk <74370953+mrdnctrk@users.noreply.github.com> Co-authored-by: TipzCM Co-authored-by: Martha Mitran Co-authored-by: Michael Buckley Co-authored-by: volodymyr-korzh <132366313+volodymyr-korzh@users.noreply.github.com> Co-authored-by: dotasek Co-authored-by: volodymyr Co-authored-by: markiantorno Co-authored-by: Gary Graham Co-authored-by: Taha Co-authored-by: taha.attari@smilecdr.com --- .../context/support/IValidationSupport.java | 321 +++++++++++++++--- .../java/ca/uhn/fhir/parser/BaseParser.java | 46 ++- .../java/ca/uhn/fhir/parser/JsonParser.java | 15 +- .../java/ca/uhn/fhir/parser/RDFParser.java | 4 +- .../java/ca/uhn/fhir/parser/XmlParser.java | 10 +- .../ca/uhn/fhir/repository/Repository.java | 44 ++- .../java/ca/uhn/fhir/util/FhirTerser.java | 88 ++--- .../java/ca/uhn/fhir/util/VersionEnum.java | 1 + .../fhir/util/adapters/AdapterManager.java | 62 ++++ .../uhn/fhir/util/adapters/AdapterUtils.java | 48 +++ .../ca/uhn/fhir/util/adapters/IAdaptable.java | 38 +++ .../fhir/util/adapters/IAdapterFactory.java | 44 +++ .../fhir/util/adapters/IAdapterManager.java | 29 ++ .../uhn/fhir/util/adapters/package-info.java | 39 +++ .../util/adapters/AdapterManagerTest.java | 78 +++++ .../fhir/util/adapters/AdapterUtilsTest.java | 123 +++++++ .../7_6_0/6403-json-parser-bugs-again.yaml | 4 + .../7_6_0/6403-json-parser-bugs.yaml | 6 + ...ptimize-storage-all-versions-posgtres.yaml | 6 + ...ge-all-versions-for-a-single-resource.yaml | 5 + .../6422-fixes-remote-terminology-issues.yaml | 7 + ...called-for-precheck-for-cached-search.yaml | 8 + .../7_6_0/6445-repository-api-multimap.yaml | 4 + ...-status-not-changing-after-activation.yaml | 6 + ...search-lucene-version-incompatibility.yaml | 5 + ...-change-index-add-concurrency-default.yaml | 4 + .../hapi/fhir/changelog/7_6_0/changes.yaml | 2 +- .../6502-profile-can-be-reference-param.yaml | 5 + .../docs/clinical_reasoning/questionnaires.md | 36 +- .../bulk/imprt/svc/BulkDataImportSvcImpl.java | 7 + .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 9 +- .../fhir/jpa/dao/BaseHapiFhirSystemDao.java | 2 +- .../dao/data/IBatch2WorkChunkRepository.java | 2 +- .../jpa/search/SearchCoordinatorSvcImpl.java | 4 +- .../fhir/jpa/search/builder/QueryStack.java | 12 +- .../ca/uhn/fhir/jpa/term/TermReadSvcImpl.java | 5 +- .../fhir/jpa/mdm/helper/BaseMdmHelper.java | 3 + .../jpa/batch2/JpaJobPersistenceImplTest.java | 5 + .../bulk/imprt/svc/BulkDataImportR4Test.java | 5 + .../jpa/dao/r4/BaseComboParamsR4Test.java | 3 + .../r4/FhirResourceDaoR4ContainedTest.java | 3 + .../dao/r4/FhirResourceDaoR4CreateTest.java | 11 +- .../r4/FhirResourceDaoR4QueryCountTest.java | 2 +- ...irResourceDaoR4VersionedReferenceTest.java | 4 +- .../fhir/jpa/dao/r4/FhirSystemDaoR4Test.java | 5 +- .../provider/r4/ResourceProviderR4Test.java | 2 + .../uhn/fhir/jpa/reindex/ReindexTaskTest.java | 53 +++ ...idateCodeWithRemoteTerminologyR4Test.java} | 167 +++------ .../ValidateWithRemoteTerminologyTest.java | 261 ++++++++++++++ .../encounter/profile-encounter-custom.json | 49 +++ ...idateCode-CodeSystem-encounter-status.json | 59 ++++ .../validateCode-CodeSystem-v2-0203.json | 25 ++ .../validateCode-CodeSystem-v3-ActCode.json | 46 +++ ...alidateCode-ValueSet-encounter-status.json | 59 ++++ ...validateCode-ValueSet-identifier-type.json | 52 +++ ...dateCode-ValueSet-v3-ActEncounterCode.json | 59 ++++ .../validateCode-CodeSystem-ICD9CM.json | 48 +++ ...ateCode-CodeSystem-observation-status.json | 25 ++ .../validateCode-ValueSet-codes.json | 48 +++ ...idateCode-ValueSet-observation-status.json | 25 ++ .../procedure/profile-procedure-slicing.json | 79 +++++ .../procedure/profile-procedure.json | 50 +++ ...dateCode-CodeSystem-absent-or-unknown.json | 46 +++ .../validateCode-CodeSystem-event-status.json | 59 ++++ ...alidateCode-CodeSystem-snomed-invalid.json | 48 +++ .../validateCode-CodeSystem-snomed-valid.json | 25 ++ ...-ValueSet-absent-or-unknown-procedure.json | 59 ++++ .../validateCode-ValueSet-event-status.json | 25 ++ ...ValueSet-procedure-code-invalid-slice.json | 48 +++ ...eCode-ValueSet-procedure-code-invalid.json | 67 ++++ ...ateCode-ValueSet-procedure-code-valid.json | 59 ++++ .../releases/V7_4_0/data/H2_EMBEDDED.sql | 30 ++ .../releases/V7_4_0/data/MSSQL_2012.sql | 30 ++ .../releases/V7_4_0/data/ORACLE_12C.sql | 30 ++ .../releases/V7_4_0/data/POSTGRES_9_4.sql | 30 ++ .../util/CompositeInterceptorBroadcaster.java | 2 +- .../CompositeInterceptorBroadcasterTest.java | 161 +++++++++ .../fhir/jpa/migrate/tasks/api/Builder.java | 2 +- .../jobs/reindex/v1/ReindexV1Config.java | 2 +- .../batch2/model/BatchWorkChunkStatusDTO.java | 1 + .../config/dstu3/EvaluateOperationConfig.java | 5 + .../cr/config/r4/EvaluateOperationConfig.java | 5 + .../fhir/cr/r4/HapiFhirRepositoryR4Test.java | 20 ++ .../fhir/parser/JsonParserDstu2_1Test.java | 13 +- .../uhn/fhir/parser/XmlParserDstu2_1Test.java | 56 +-- .../uhn/fhir/parser/CustomTypeDstu2Test.java | 14 +- .../uhn/fhir/parser/JsonParserDstu2Test.java | 7 +- .../uhn/fhir/parser/XmlParserDstu2Test.java | 57 ++-- .../uhn/fhir/parser/JsonParserDstu3Test.java | 13 +- .../uhn/fhir/parser/XmlParserDstu3Test.java | 56 +-- .../parser/JsonParserHl7OrgDstu2Test.java | 30 +- .../fhir/parser/XmlParserHl7OrgDstu2Test.java | 32 +- .../ca/uhn/fhir/parser/JsonParserR4Test.java | 53 ++- .../ca/uhn/fhir/util/FhirTerserR4Test.java | 42 ++- .../ca/uhn/fhir/test/utilities/UuidUtils.java | 47 +++ .../validation/IValidationProviders.java | 123 +++++++ .../validation/IValidationProvidersDstu3.java | 137 ++++++++ .../validation/IValidationProvidersR4.java | 133 +++----- .../fhir/test/utilities/UuidUtilsTest.java | 29 ++ .../CommonCodeSystemsTerminologyService.java | 2 +- ...oryTerminologyServerValidationSupport.java | 35 +- ...teTerminologyServiceValidationSupport.java | 36 +- ...ownCodeSystemWarningValidationSupport.java | 2 +- .../support/ValidationSupportUtils.java | 11 + .../VersionSpecificWorkerContextWrapper.java | 105 ++---- .../hapi/validation/ILookupCodeTest.java | 15 +- .../IRemoteTerminologyLookupCodeTest.java | 1 + .../IRemoteTerminologyValidateCodeTest.java | 59 ++++ .../hapi/validation/IValidateCodeTest.java | 146 ++++---- .../hapi/validation/IValidationProviders.java | 39 --- ...rsionSpecificWorkerContextWrapperTest.java | 31 +- .../FhirInstanceValidatorDstu2Test.java | 8 +- .../FhirInstanceValidatorDstu3Test.java | 5 +- .../IValidateCodeProvidersDstu3.java | 159 --------- ...estionnaireResponseValidatorDstu3Test.java | 9 +- .../RemoteTerminologyLookupCodeDstu3Test.java | 16 +- ...gyLookupCodeWithResponseFileDstu3Test.java | 22 +- ...emoteTerminologyValidateCodeDstu3Test.java | 52 +-- .../fhir/r4/utils/FhirPathEngineR4Test.java | 4 + .../FhirInstanceValidatorR4Test.java | 4 +- .../RemoteTerminologyLookupCodeR4Test.java | 21 +- ...ologyLookupCodeWithResponseFileR4Test.java | 22 +- .../RemoteTerminologyValidateCodeR4Test.java | 101 +++--- .../FhirInstanceValidatorR4BTest.java | 4 +- .../FhirInstanceValidatorR5Test.java | 5 +- ...nOutcome-ValueSet-custom-issue-detail.json | 22 ++ .../ca/uhn/fhir/tinder/Configuration.java | 2 +- .../tinder/TinderGenericSingleFileMojo.java | 2 +- .../fhir/tinder/TinderJpaRestServerMojo.java | 2 +- .../fhir/tinder/ant/TinderGeneratorTask.java | 2 +- .../ca/uhn/fhir/tinder/model/BaseElement.java | 4 +- .../fhir/tinder/model/SearchParameter.java | 2 +- .../tinder/parser/BaseStructureParser.java | 6 +- .../parser/ResourceGeneratorUsingModel.java | 2 +- pom.xml | 18 +- 135 files changed, 3801 insertions(+), 1048 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs-again.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6451-bulk-import-job-status-not-changing-after-activation.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6467-hibernate-search-lucene-version-incompatibility.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6489-change-index-add-concurrency-default.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6502-profile-can-be-reference-param.yaml rename hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/{provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java => validation/ValidateCodeWithRemoteTerminologyR4Test.java} (59%) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/UuidUtils.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java rename hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java => hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java (55%) create mode 100644 hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/UuidUtilsTest.java delete mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java delete mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java create mode 100644 hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index 71d561c1db3..fdbc4ca31d7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -440,74 +440,259 @@ public interface IValidationSupport { return "Unknown " + getFhirContext().getVersion().getVersion() + " Validation Support"; } + /** + * Defines codes in system http://hl7.org/fhir/issue-severity. + */ + /* this enum would not be needed if we design/refactor to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult */ enum IssueSeverity { /** * The issue caused the action to fail, and no further checking could be performed. */ - FATAL, + FATAL("fatal"), /** * The issue is sufficiently important to cause the action to fail. */ - ERROR, + ERROR("error"), /** * The issue is not important enough to cause the action to fail, but may cause it to be performed suboptimally or in a way that is not as desired. */ - WARNING, + WARNING("warning"), /** * The issue has no relation to the degree of success of the action. */ - INFORMATION + INFORMATION("information"), + /** + * The operation was successful. + */ + SUCCESS("success"); + // the spec for OperationOutcome mentions that a code from http://hl7.org/fhir/issue-severity is required + + private final String myCode; + + IssueSeverity(String theCode) { + myCode = theCode; + } + /** + * Provide mapping to a code in system http://hl7.org/fhir/issue-severity. + * @return the code + */ + public String getCode() { + return myCode; + } + /** + * Creates a {@link IssueSeverity} object from the given code. + * @return the {@link IssueSeverity} + */ + public static IssueSeverity fromCode(String theCode) { + switch (theCode) { + case "fatal": + return FATAL; + case "error": + return ERROR; + case "warning": + return WARNING; + case "information": + return INFORMATION; + case "success": + return SUCCESS; + default: + return null; + } + } } - enum CodeValidationIssueCode { - NOT_FOUND, - CODE_INVALID, - INVALID, - OTHER - } + /** + * Defines codes in system http://hl7.org/fhir/issue-type. + * The binding is enforced as a part of validation logic in the FHIR Core Validation library where an exception is thrown. + * Only a sub-set of these codes are defined as constants because they relate to validation, + * If there are additional ones that come up, for Remote Terminology they are currently supported via + * {@link IValidationSupport.CodeValidationIssue#CodeValidationIssue(String, IssueSeverity, String)} + * while for internal validators, more constants can be added to make things easier and consistent. + * This maps to resource OperationOutcome.issue.code. + */ + /* this enum would not be needed if we design/refactor to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult */ + class CodeValidationIssueCode { + public static final CodeValidationIssueCode NOT_FOUND = new CodeValidationIssueCode("not-found"); + public static final CodeValidationIssueCode CODE_INVALID = new CodeValidationIssueCode("code-invalid"); + public static final CodeValidationIssueCode INVALID = new CodeValidationIssueCode("invalid"); - enum CodeValidationIssueCoding { - VS_INVALID, - NOT_FOUND, - NOT_IN_VS, + private final String myCode; - INVALID_CODE, - INVALID_DISPLAY, - OTHER - } - - class CodeValidationIssue { - - private final String myMessage; - private final IssueSeverity mySeverity; - private final CodeValidationIssueCode myCode; - private final CodeValidationIssueCoding myCoding; - - public CodeValidationIssue( - String theMessage, - IssueSeverity mySeverity, - CodeValidationIssueCode theCode, - CodeValidationIssueCoding theCoding) { - this.myMessage = theMessage; - this.mySeverity = mySeverity; - this.myCode = theCode; - this.myCoding = theCoding; + // this is intentionally not exposed + CodeValidationIssueCode(String theCode) { + myCode = theCode; } + /** + * Retrieve the corresponding code from system http://hl7.org/fhir/issue-type. + * @return the code + */ + public String getCode() { + return myCode; + } + } + + /** + * Holds information about the details of a {@link CodeValidationIssue}. + * This maps to resource OperationOutcome.issue.details. + */ + /* this enum would not be needed if we design/refactor to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult */ + class CodeValidationIssueDetails { + private final String myText; + private List myCodings; + + public CodeValidationIssueDetails(String theText) { + myText = theText; + } + + // intentionally not exposed + void addCoding(CodeValidationIssueCoding theCoding) { + getCodings().add(theCoding); + } + + public CodeValidationIssueDetails addCoding(String theSystem, String theCode) { + if (myCodings == null) { + myCodings = new ArrayList<>(); + } + myCodings.add(new CodeValidationIssueCoding(theSystem, theCode)); + return this; + } + + public String getText() { + return myText; + } + + public List getCodings() { + if (myCodings == null) { + myCodings = new ArrayList<>(); + } + return myCodings; + } + } + + /** + * Defines codes that can be part of the details of an issue. + * There are some constants available (pre-defined) for codes for system http://hl7.org/fhir/tools/CodeSystem/tx-issue-type. + * This maps to resource OperationOutcome.issue.details.coding[0].code. + */ + class CodeValidationIssueCoding { + public static String TX_ISSUE_SYSTEM = "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type"; + public static CodeValidationIssueCoding VS_INVALID = + new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "vs-invalid"); + public static final CodeValidationIssueCoding NOT_FOUND = + new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "not-found"); + public static final CodeValidationIssueCoding NOT_IN_VS = + new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "not-in-vs"); + public static final CodeValidationIssueCoding INVALID_CODE = + new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "invalid-code"); + public static final CodeValidationIssueCoding INVALID_DISPLAY = + new CodeValidationIssueCoding(TX_ISSUE_SYSTEM, "vs-display"); + private final String mySystem, myCode; + + // this is intentionally not exposed + CodeValidationIssueCoding(String theSystem, String theCode) { + mySystem = theSystem; + myCode = theCode; + } + + /** + * Retrieve the corresponding code for the details of a validation issue. + * @return the code + */ + public String getCode() { + return myCode; + } + + /** + * Retrieve the system for the details of a validation issue. + * @return the system + */ + public String getSystem() { + return mySystem; + } + } + + /** + * This is a hapi-fhir internal version agnostic object holding information about a validation issue. + * An alternative (which requires significant refactoring) would be to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult instead. + */ + class CodeValidationIssue { + private final String myDiagnostics; + private final IssueSeverity mySeverity; + private final CodeValidationIssueCode myCode; + private CodeValidationIssueDetails myDetails; + + public CodeValidationIssue( + String theDiagnostics, IssueSeverity theSeverity, CodeValidationIssueCode theTypeCode) { + this(theDiagnostics, theSeverity, theTypeCode, null); + } + + public CodeValidationIssue(String theDiagnostics, IssueSeverity theSeverity, String theTypeCode) { + this(theDiagnostics, theSeverity, new CodeValidationIssueCode(theTypeCode), null); + } + + public CodeValidationIssue( + String theDiagnostics, + IssueSeverity theSeverity, + CodeValidationIssueCode theType, + CodeValidationIssueCoding theDetailsCoding) { + myDiagnostics = theDiagnostics; + mySeverity = theSeverity; + myCode = theType; + // reuse the diagnostics message as a detail text message + myDetails = new CodeValidationIssueDetails(theDiagnostics); + myDetails.addCoding(theDetailsCoding); + } + + /** + * @deprecated Please use {@link #getDiagnostics()} instead. + */ + @Deprecated(since = "7.4.6") public String getMessage() { - return myMessage; + return getDiagnostics(); + } + + public String getDiagnostics() { + return myDiagnostics; } public IssueSeverity getSeverity() { return mySeverity; } + /** + * @deprecated Please use {@link #getType()} instead. + */ + @Deprecated(since = "7.4.6") public CodeValidationIssueCode getCode() { + return getType(); + } + + public CodeValidationIssueCode getType() { return myCode; } + /** + * @deprecated Please use {@link #getDetails()} instead. That has support for multiple codings. + */ + @Deprecated(since = "7.4.6") public CodeValidationIssueCoding getCoding() { - return myCoding; + return myDetails != null + ? myDetails.getCodings().stream().findFirst().orElse(null) + : null; + } + + public void setDetails(CodeValidationIssueDetails theDetails) { + this.myDetails = theDetails; + } + + public CodeValidationIssueDetails getDetails() { + return myDetails; + } + + public boolean hasIssueDetailCode(@Nonnull String theCode) { + // this method is system agnostic at the moment but it can be restricted if needed + return myDetails.getCodings().stream().anyMatch(coding -> theCode.equals(coding.getCode())); } } @@ -671,6 +856,10 @@ public interface IValidationSupport { } } + /** + * This is a hapi-fhir internal version agnostic object holding information about the validation result. + * An alternative (which requires significant refactoring) would be to use org.hl7.fhir.r5.terminologies.utilities.ValidationResult. + */ class CodeValidationResult { public static final String SOURCE_DETAILS = "sourceDetails"; public static final String RESULT = "result"; @@ -686,7 +875,7 @@ public interface IValidationSupport { private String myDisplay; private String mySourceDetails; - private List myCodeValidationIssues; + private List myIssues; public CodeValidationResult() { super(); @@ -771,20 +960,45 @@ public interface IValidationSupport { return this; } + /** + * @deprecated Please use method {@link #getIssues()} instead. + */ + @Deprecated(since = "7.4.6") public List getCodeValidationIssues() { - if (myCodeValidationIssues == null) { - myCodeValidationIssues = new ArrayList<>(); - } - return myCodeValidationIssues; + return getIssues(); } + /** + * @deprecated Please use method {@link #setIssues(List)} instead. + */ + @Deprecated(since = "7.4.6") public CodeValidationResult setCodeValidationIssues(List theCodeValidationIssues) { - myCodeValidationIssues = new ArrayList<>(theCodeValidationIssues); + return setIssues(theCodeValidationIssues); + } + + /** + * @deprecated Please use method {@link #addIssue(CodeValidationIssue)} instead. + */ + @Deprecated(since = "7.4.6") + public CodeValidationResult addCodeValidationIssue(CodeValidationIssue theCodeValidationIssue) { + getCodeValidationIssues().add(theCodeValidationIssue); return this; } - public CodeValidationResult addCodeValidationIssue(CodeValidationIssue theCodeValidationIssue) { - getCodeValidationIssues().add(theCodeValidationIssue); + public List getIssues() { + if (myIssues == null) { + myIssues = new ArrayList<>(); + } + return myIssues; + } + + public CodeValidationResult setIssues(List theIssues) { + myIssues = new ArrayList<>(theIssues); + return this; + } + + public CodeValidationResult addIssue(CodeValidationIssue theCodeValidationIssue) { + getIssues().add(theCodeValidationIssue); return this; } @@ -811,17 +1025,19 @@ public interface IValidationSupport { public String getSeverityCode() { String retVal = null; if (getSeverity() != null) { - retVal = getSeverity().name().toLowerCase(); + retVal = getSeverity().getCode(); } return retVal; } /** - * Sets an issue severity as a string code. Value must be the name of - * one of the enum values in {@link IssueSeverity}. Value is case-insensitive. + * Sets an issue severity using a severity code. Please use method {@link #setSeverity(IssueSeverity)} instead. + * @param theSeverityCode the code + * @return the current {@link CodeValidationResult} instance */ - public CodeValidationResult setSeverityCode(@Nonnull String theIssueSeverity) { - setSeverity(IssueSeverity.valueOf(theIssueSeverity.toUpperCase())); + @Deprecated(since = "7.4.6") + public CodeValidationResult setSeverityCode(@Nonnull String theSeverityCode) { + setSeverity(IssueSeverity.fromCode(theSeverityCode)); return this; } @@ -838,6 +1054,11 @@ public interface IValidationSupport { if (isNotBlank(getSourceDetails())) { ParametersUtil.addParameterToParametersString(theContext, retVal, SOURCE_DETAILS, getSourceDetails()); } + /* + should translate issues as well, except that is version specific code, so it requires more refactoring + or replace the current class with org.hl7.fhir.r5.terminologies.utilities.ValidationResult + @see VersionSpecificWorkerContextWrapper#getIssuesForCodeValidation + */ return retVal; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index f0559373e0a..ce932333840 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -105,7 +105,6 @@ public abstract class BaseParser implements IParser { private static final Set notEncodeForContainedResource = new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated")); - private FhirTerser.ContainedResources myContainedResources; private boolean myEncodeElementsAppliesToChildResourcesOnly; private final FhirContext myContext; private Collection myDontEncodeElements; @@ -183,12 +182,15 @@ public abstract class BaseParser implements IParser { } private String determineReferenceText( - IBaseReference theRef, CompositeChildElement theCompositeChildElement, IBaseResource theResource) { + IBaseReference theRef, + CompositeChildElement theCompositeChildElement, + IBaseResource theResource, + EncodeContext theContext) { IIdType ref = theRef.getReferenceElement(); if (isBlank(ref.getIdPart())) { String reference = ref.getValue(); if (theRef.getResource() != null) { - IIdType containedId = getContainedResources().getResourceId(theRef.getResource()); + IIdType containedId = theContext.getContainedResources().getResourceId(theRef.getResource()); if (containedId != null && !containedId.isEmpty()) { if (containedId.isLocal()) { reference = containedId.getValue(); @@ -262,7 +264,8 @@ public abstract class BaseParser implements IParser { @Override public final void encodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException, DataFormatException { - EncodeContext encodeContext = new EncodeContext(this, myContext.getParserOptions()); + EncodeContext encodeContext = + new EncodeContext(this, myContext.getParserOptions(), new FhirTerser.ContainedResources()); encodeResourceToWriter(theResource, theWriter, encodeContext); } @@ -285,7 +288,8 @@ public abstract class BaseParser implements IParser { } else if (theElement instanceof IPrimitiveType) { theWriter.write(((IPrimitiveType) theElement).getValueAsString()); } else { - EncodeContext encodeContext = new EncodeContext(this, myContext.getParserOptions()); + EncodeContext encodeContext = + new EncodeContext(this, myContext.getParserOptions(), new FhirTerser.ContainedResources()); encodeToWriter(theElement, theWriter, encodeContext); } } @@ -404,10 +408,6 @@ public abstract class BaseParser implements IParser { return elementId; } - FhirTerser.ContainedResources getContainedResources() { - return myContainedResources; - } - @Override public Set getDontStripVersionsFromReferencesAtPaths() { return myDontStripVersionsFromReferencesAtPaths; @@ -539,10 +539,11 @@ public abstract class BaseParser implements IParser { return mySuppressNarratives; } - protected boolean isChildContained(BaseRuntimeElementDefinition childDef, boolean theIncludedResource) { + protected boolean isChildContained( + BaseRuntimeElementDefinition childDef, boolean theIncludedResource, EncodeContext theContext) { return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) - && getContainedResources().isEmpty() == false + && theContext.getContainedResources().isEmpty() == false && theIncludedResource == false; } @@ -788,7 +789,8 @@ public abstract class BaseParser implements IParser { */ if (next instanceof IBaseReference) { IBaseReference nextRef = (IBaseReference) next; - String refText = determineReferenceText(nextRef, theCompositeChildElement, theResource); + String refText = + determineReferenceText(nextRef, theCompositeChildElement, theResource, theEncodeContext); if (!StringUtils.equals(refText, nextRef.getReferenceElement().getValue())) { if (retVal == theValues) { @@ -980,7 +982,7 @@ public abstract class BaseParser implements IParser { return true; } - protected void containResourcesInReferences(IBaseResource theResource) { + protected void containResourcesInReferences(IBaseResource theResource, EncodeContext theContext) { /* * If a UUID is present in Bundle.entry.fullUrl but no value is present @@ -1003,7 +1005,7 @@ public abstract class BaseParser implements IParser { } } - myContainedResources = getContext().newTerser().containResources(theResource); + theContext.setContainedResources(getContext().newTerser().containResources(theResource)); } static class ChildNameAndDef { @@ -1034,8 +1036,12 @@ public abstract class BaseParser implements IParser { private final List myEncodeElementPaths; private final Set myEncodeElementsAppliesToResourceTypes; private final List myDontEncodeElementPaths; + private FhirTerser.ContainedResources myContainedResources; - public EncodeContext(BaseParser theParser, ParserOptions theParserOptions) { + public EncodeContext( + BaseParser theParser, + ParserOptions theParserOptions, + FhirTerser.ContainedResources theContainedResources) { Collection encodeElements = theParser.myEncodeElements; Collection dontEncodeElements = theParser.myDontEncodeElements; if (isSummaryMode()) { @@ -1058,6 +1064,8 @@ public abstract class BaseParser implements IParser { dontEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList()); } + myContainedResources = theContainedResources; + myEncodeElementsAppliesToResourceTypes = ParserUtil.determineApplicableResourceTypesForTerserPaths(myEncodeElementPaths); } @@ -1065,6 +1073,14 @@ public abstract class BaseParser implements IParser { private Map> getCompositeChildrenCache() { return myCompositeChildrenCache; } + + public FhirTerser.ContainedResources getContainedResources() { + return myContainedResources; + } + + public void setContainedResources(FhirTerser.ContainedResources theContainedResources) { + myContainedResources = theContainedResources; + } } protected class CompositeChildElement { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 0b25304ee5f..5731742ac1f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -54,6 +54,7 @@ import ca.uhn.fhir.parser.json.JsonLikeStructure; import ca.uhn.fhir.parser.json.jackson.JacksonStructure; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.util.ElementUtil; +import ca.uhn.fhir.util.FhirTerser; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.text.WordUtils; @@ -386,12 +387,14 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } case CONTAINED_RESOURCE_LIST: case CONTAINED_RESOURCES: { - List containedResources = getContainedResources().getContainedResources(); + List containedResources = + theEncodeContext.getContainedResources().getContainedResources(); if (containedResources.size() > 0) { beginArray(theEventWriter, theChildName); for (IBaseResource next : containedResources) { - IIdType resourceId = getContainedResources().getResourceId(next); + IIdType resourceId = + theEncodeContext.getContainedResources().getResourceId(next); String value = resourceId.getValue(); encodeResourceToJsonStreamWriter( theResDef, @@ -554,7 +557,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { if (nextValue == null || nextValue.isEmpty()) { if (nextValue instanceof BaseContainedDt) { - if (theContainedResource || getContainedResources().isEmpty()) { + if (theContainedResource + || theEncodeContext.getContainedResources().isEmpty()) { continue; } } else { @@ -838,7 +842,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { + theResource.getStructureFhirVersionEnum()); } - EncodeContext encodeContext = new EncodeContext(this, getContext().getParserOptions()); + EncodeContext encodeContext = + new EncodeContext(this, getContext().getParserOptions(), new FhirTerser.ContainedResources()); String resourceName = getContext().getResourceType(theResource); encodeContext.pushPath(resourceName, true); doEncodeResourceToJsonLikeWriter(theResource, theJsonLikeWriter, encodeContext); @@ -894,7 +899,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser { } if (!theContainedResource) { - containResourcesInReferences(theResource); + containResourcesInReferences(theResource, theEncodeContext); } RuntimeResourceDefinition resDef = getContext().getResourceDefinition(theResource); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java index b75ee9c897f..18572824fee 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/RDFParser.java @@ -191,7 +191,7 @@ public class RDFParser extends BaseParser { } if (!containedResource) { - containResourcesInReferences(resource); + containResourcesInReferences(resource, encodeContext); } if (!(resource instanceof IAnyResource)) { @@ -354,7 +354,7 @@ public class RDFParser extends BaseParser { try { if (element == null || element.isEmpty()) { - if (!isChildContained(childDef, includedResource)) { + if (!isChildContained(childDef, includedResource, theEncodeContext)) { return rdfModel; } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java index 7a5aaa021bf..71b83de8d8e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java @@ -295,7 +295,7 @@ public class XmlParser extends BaseParser { try { if (theElement == null || theElement.isEmpty()) { - if (isChildContained(childDef, theIncludedResource)) { + if (isChildContained(childDef, theIncludedResource, theEncodeContext)) { // We still want to go in.. } else { return; @@ -359,8 +359,10 @@ public class XmlParser extends BaseParser { * theEventWriter.writeStartElement("contained"); encodeResourceToXmlStreamWriter(next, theEventWriter, true, fixContainedResourceId(next.getId().getValue())); * theEventWriter.writeEndElement(); } */ - for (IBaseResource next : getContainedResources().getContainedResources()) { - IIdType resourceId = getContainedResources().getResourceId(next); + for (IBaseResource next : + theEncodeContext.getContainedResources().getContainedResources()) { + IIdType resourceId = + theEncodeContext.getContainedResources().getResourceId(next); theEventWriter.writeStartElement("contained"); String value = resourceId.getValue(); encodeResourceToXmlStreamWriter( @@ -682,7 +684,7 @@ public class XmlParser extends BaseParser { } if (!theContainedResource) { - containResourcesInReferences(theResource); + containResourcesInReferences(theResource, theEncodeContext); } theEventWriter.writeStartElement(resDef.getName()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java index e859a9ae569..c272656f47a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/repository/Repository.java @@ -28,6 +28,8 @@ import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; import com.google.common.annotations.Beta; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseConformance; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -231,6 +233,23 @@ public interface Repository { // Querying starts here + /** + * Searches this repository + * + * @see FHIR search + * + * @param a Bundle type + * @param a Resource type + * @param bundleType the class of the Bundle type to return + * @param resourceType the class of the Resource type to search + * @param searchParameters the searchParameters for this search + * @return a Bundle with the results of the search + */ + default B search( + Class bundleType, Class resourceType, Multimap> searchParameters) { + return this.search(bundleType, resourceType, searchParameters, Collections.emptyMap()); + } + /** * Searches this repository * @@ -264,9 +283,32 @@ public interface Repository { B search( Class bundleType, Class resourceType, - Map> searchParameters, + Multimap> searchParameters, Map headers); + /** + * Searches this repository + * + * @see FHIR search + * + * @param a Bundle type + * @param a Resource type + * @param bundleType the class of the Bundle type to return + * @param resourceType the class of the Resource type to search + * @param searchParameters the searchParameters for this search + * @param headers headers for this request, typically key-value pairs of HTTP headers + * @return a Bundle with the results of the search + */ + default B search( + Class bundleType, + Class resourceType, + Map> searchParameters, + Map headers) { + ArrayListMultimap> multimap = ArrayListMultimap.create(); + searchParameters.forEach(multimap::put); + return this.search(bundleType, resourceType, multimap, headers); + } + // Paging starts here /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java index 6eab1ce9ed3..eebb07b63c3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTerser.java @@ -38,7 +38,6 @@ import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions; import ca.uhn.fhir.model.base.composite.BaseContainedDt; import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.DataFormatException; import com.google.common.collect.Lists; @@ -61,6 +60,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; @@ -70,6 +70,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -77,16 +78,28 @@ import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.apache.commons.lang3.StringUtils.substring; public class FhirTerser { private static final Pattern COMPARTMENT_MATCHER_PATH = Pattern.compile("([a-zA-Z.]+)\\.where\\(resolve\\(\\) is ([a-zA-Z]+)\\)"); + private static final String USER_DATA_KEY_CONTAIN_RESOURCES_COMPLETED = FhirTerser.class.getName() + "_CONTAIN_RESOURCES_COMPLETED"; + private final FhirContext myContext; + /** + * This comparator sorts IBaseReferences, and places any that are missing an ID at the end. Those with an ID go to the front. + */ + private static final Comparator REFERENCES_WITH_IDS_FIRST = + Comparator.nullsLast(Comparator.comparing(ref -> { + if (ref.getResource() == null) return true; + if (ref.getResource().getIdElement() == null) return true; + if (ref.getResource().getIdElement().getValue() == null) return true; + return false; + })); + public FhirTerser(FhirContext theContext) { super(); myContext = theContext; @@ -1418,6 +1431,13 @@ public class FhirTerser { private void containResourcesForEncoding( ContainedResources theContained, IBaseResource theResource, boolean theModifyResource) { List allReferences = getAllPopulatedChildElementsOfType(theResource, IBaseReference.class); + + // Note that we process all contained resources that have arrived here with an ID contained resources first, so + // that we don't accidentally auto-assign an ID + // which may collide with a resource we have yet to process. + // See: https://github.com/hapifhir/hapi-fhir/issues/6403 + allReferences.sort(REFERENCES_WITH_IDS_FIRST); + for (IBaseReference next : allReferences) { IBaseResource resource = next.getResource(); if (resource == null && next.getReferenceElement().isLocal()) { @@ -1437,11 +1457,11 @@ public class FhirTerser { IBaseResource resource = next.getResource(); if (resource != null) { if (resource.getIdElement().isEmpty() || resource.getIdElement().isLocal()) { - if (theContained.getResourceId(resource) != null) { - // Prevent infinite recursion if there are circular loops in the contained resources + + IIdType id = theContained.addContained(resource); + if (id == null) { continue; } - IIdType id = theContained.addContained(resource); if (theModifyResource) { getContainedResourceList(theResource).add(resource); next.setReference(id.getValue()); @@ -1768,8 +1788,6 @@ public class FhirTerser { } public static class ContainedResources { - private long myNextContainedId = 1; - private List myResourceList; private IdentityHashMap myResourceToIdMap; private Map myExistingIdToContainedResourceMap; @@ -1782,6 +1800,11 @@ public class FhirTerser { } public IIdType addContained(IBaseResource theResource) { + if (this.getResourceId(theResource) != null) { + // Prevent infinite recursion if there are circular loops in the contained resources + return null; + } + IIdType existing = getResourceToIdMap().get(theResource); if (existing != null) { return existing; @@ -1789,16 +1812,7 @@ public class FhirTerser { IIdType newId = theResource.getIdElement(); if (isBlank(newId.getValue())) { - newId.setValue("#" + myNextContainedId++); - } else { - // Avoid auto-assigned contained IDs colliding with pre-existing ones - String idPart = newId.getValue(); - if (substring(idPart, 0, 1).equals("#")) { - idPart = idPart.substring(1); - if (StringUtils.isNumeric(idPart)) { - myNextContainedId = Long.parseLong(idPart) + 1; - } - } + newId.setValue("#" + UUID.randomUUID()); } getResourceToIdMap().put(theResource, newId); @@ -1862,45 +1876,5 @@ public class FhirTerser { public boolean hasExistingIdToContainedResource() { return myExistingIdToContainedResourceMap != null; } - - public void assignIdsToContainedResources() { - - if (!getContainedResources().isEmpty()) { - - /* - * The idea with the code block below: - * - * We want to preserve any IDs that were user-assigned, so that if it's really - * important to someone that their contained resource have the ID of #FOO - * or #1 we will keep that. - * - * For any contained resources where no ID was assigned by the user, we - * want to manually create an ID but make sure we don't reuse an existing ID. - */ - - Set ids = new HashSet<>(); - - // Gather any user assigned IDs - for (IBaseResource nextResource : getContainedResources()) { - if (getResourceToIdMap().get(nextResource) != null) { - ids.add(getResourceToIdMap().get(nextResource).getValue()); - } - } - - // Automatically assign IDs to the rest - for (IBaseResource nextResource : getContainedResources()) { - - while (getResourceToIdMap().get(nextResource) == null) { - String nextCandidate = "#" + myNextContainedId; - myNextContainedId++; - if (!ids.add(nextCandidate)) { - continue; - } - - getResourceToIdMap().put(nextResource, new IdDt(nextCandidate)); - } - } - } - } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index 8d4867594cf..0f89ed04d07 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -170,6 +170,7 @@ public enum VersionEnum { V7_5_0, V7_6_0, + V7_6_1, V7_7_0, V7_8_0; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java new file mode 100644 index 00000000000..93a7b054a39 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java @@ -0,0 +1,62 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.util.adapters; + +import jakarta.annotation.Nonnull; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +public class AdapterManager implements IAdapterManager { + public static final AdapterManager INSTANCE = new AdapterManager(); + + Set myAdapterFactories = new HashSet<>(); + + /** + * Hidden to force shared use of the public INSTANCE. + */ + AdapterManager() {} + + public @Nonnull Optional getAdapter(Object theObject, Class theTargetType) { + // todo this can be sped up with a cache of type->Factory. + return myAdapterFactories.stream() + .filter(nextFactory -> nextFactory.getAdapters().stream().anyMatch(theTargetType::isAssignableFrom)) + .flatMap(nextFactory -> { + var adapter = nextFactory.getAdapter(theObject, theTargetType); + // can't use Optional.stream() because of our Android target is API level 26/JDK 8. + if (adapter.isPresent()) { + return Stream.of(adapter.get()); + } else { + return Stream.empty(); + } + }) + .findFirst(); + } + + public void registerFactory(@Nonnull IAdapterFactory theFactory) { + myAdapterFactories.add(theFactory); + } + + public void unregisterFactory(@Nonnull IAdapterFactory theFactory) { + myAdapterFactories.remove(theFactory); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java new file mode 100644 index 00000000000..369b7ab324e --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java @@ -0,0 +1,48 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.util.adapters; + +import java.util.Optional; + +public class AdapterUtils { + + /** + * Main entry point for adapter calls. + * Implements three conversions: cast to the target type, use IAdaptable if present, or lastly try the AdapterManager.INSTANCE. + * @param theObject the object to be adapted + * @param theTargetType the type of the adapter requested + */ + static Optional adapt(Object theObject, Class theTargetType) { + if (theTargetType.isInstance(theObject)) { + //noinspection unchecked + return Optional.of((T) theObject); + } + + if (theObject instanceof IAdaptable) { + IAdaptable adaptable = (IAdaptable) theObject; + var adapted = adaptable.getAdapter(theTargetType); + if (adapted.isPresent()) { + return adapted; + } + } + + return AdapterManager.INSTANCE.getAdapter(theObject, theTargetType); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java new file mode 100644 index 00000000000..4310c294fba --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java @@ -0,0 +1,38 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.util.adapters; + +import jakarta.annotation.Nonnull; + +import java.util.Optional; + +/** + * Generic version of Eclipse IAdaptable interface. + */ +public interface IAdaptable { + /** + * Get an adapter of requested type. + * @param theTargetType the desired type of the adapter + * @return an adapter of theTargetType if possible, or empty. + */ + default @Nonnull Optional getAdapter(@Nonnull Class theTargetType) { + return AdapterUtils.adapt(this, theTargetType); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java new file mode 100644 index 00000000000..f4aa88edde5 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java @@ -0,0 +1,44 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.util.adapters; + +import java.util.Collection; +import java.util.Optional; + +/** + * Interface for external service that builds adaptors for targets. + */ +public interface IAdapterFactory { + /** + * Build an adaptor for the target. + * May return empty() even if the target type is listed in getAdapters() when + * the factory fails to convert a particular instance. + * + * @param theObject the object to be adapted. + * @param theAdapterType the target type + * @return the adapter, if possible. + */ + Optional getAdapter(Object theObject, Class theAdapterType); + + /** + * @return the collection of adapter target types handled by this factory. + */ + Collection> getAdapters(); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java new file mode 100644 index 00000000000..d199267f927 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java @@ -0,0 +1,29 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.util.adapters; + +import java.util.Optional; + +/** + * Get an adaptor + */ +public interface IAdapterManager { + Optional getAdapter(Object theTarget, Class theAdapter); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java new file mode 100644 index 00000000000..c053c1b0248 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java @@ -0,0 +1,39 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +/** + * Implements the Adapter pattern to allow external classes to extend/adapt existing classes. + * Useful for extending interfaces that are closed to modification, or restricted for classpath reasons. + *

+ * For clients, the main entry point is {@link ca.uhn.fhir.util.adapters.AdapterUtils#adapt(java.lang.Object, java.lang.Class)} + * which will attempt to cast to the target type, or build an adapter of the target type. + *

+ *

+ * For implementors, you can support adaptation via two mechanisms: + *

    + *
  • by implementing {@link ca.uhn.fhir.util.adapters.IAdaptable} directly on a class to provide supported adapters, + *
  • or when the class is closed to direct modification, you can implement + * an instance of {@link ca.uhn.fhir.util.adapters.IAdapterFactory} and register + * it with the public {@link ca.uhn.fhir.util.adapters.AdapterManager#INSTANCE}.
  • + *
+ * The AdapterUtils.adapt() supports both of these. + *

+ * Inspired by the Eclipse runtime. + */ +package ca.uhn.fhir.util.adapters; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java new file mode 100644 index 00000000000..ee621533587 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java @@ -0,0 +1,78 @@ +package ca.uhn.fhir.util.adapters; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AdapterManagerTest { + AdapterManager myAdapterManager = new AdapterManager(); + + @AfterAll + static void tearDown() { + assertThat(AdapterManager.INSTANCE.myAdapterFactories) + .withFailMessage("Don't dirty the public instance").isEmpty(); + } + + @Test + void testRegisterFactory_providesAdapter() { + // given + myAdapterManager.registerFactory(new StringToIntFactory()); + + // when + var result = myAdapterManager.getAdapter("22", Integer.class); + + // then + assertThat(result).contains(22); + } + + @Test + void testRegisterFactory_wrongTypeStillEmpty() { + // given + myAdapterManager.registerFactory(new StringToIntFactory()); + + // when + var result = myAdapterManager.getAdapter("22", Float.class); + + // then + assertThat(result).isEmpty(); + } + + @Test + void testUnregisterFactory_providesEmpty() { + // given active factory, now gone. + StringToIntFactory factory = new StringToIntFactory(); + myAdapterManager.registerFactory(factory); + myAdapterManager.getAdapter("22", Integer.class); + myAdapterManager.unregisterFactory(factory); + + // when + var result = myAdapterManager.getAdapter("22", Integer.class); + + // then + assertThat(result).isEmpty(); + } + + + static class StringToIntFactory implements IAdapterFactory { + @Override + public Optional getAdapter(Object theObject, Class theAdapterType) { + if (theObject instanceof String s) { + if (theAdapterType.isAssignableFrom(Integer.class)) { + @SuppressWarnings("unchecked") + T i = (T) Integer.valueOf(s); + return Optional.of(i); + } + } + return Optional.empty(); + } + + public Collection> getAdapters() { + return List.of(Integer.class); + } + } +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java new file mode 100644 index 00000000000..c7dfef8661b --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java @@ -0,0 +1,123 @@ +package ca.uhn.fhir.util.adapters; + +import jakarta.annotation.Nonnull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class AdapterUtilsTest { + + final private IAdapterFactory myTestFactory = new TestAdaptorFactory(); + + @AfterEach + void tearDown() { + AdapterManager.INSTANCE.unregisterFactory(myTestFactory); + } + + @Test + void testNullDoesNotAdapt() { + + // when + var adapted = AdapterUtils.adapt(null, InterfaceA.class); + + // then + assertThat(adapted).isEmpty(); + } + + @Test + void testAdaptObjectImplementingInterface() { + // given + var object = new ClassB(); + + // when + var adapted = AdapterUtils.adapt(object, InterfaceA.class); + + // then + assertThat(adapted) + .isPresent() + .get().isInstanceOf(InterfaceA.class); + assertThat(adapted.get()).withFailMessage("Use object since it implements interface").isSameAs(object); + } + + @Test + void testAdaptObjectImplementingAdaptorSupportingInterface() { + // given + var object = new SelfAdaptableClass(); + + // when + var adapted = AdapterUtils.adapt(object, InterfaceA.class); + + // then + assertThat(adapted) + .isPresent() + .get().isInstanceOf(InterfaceA.class); + } + + @Test + void testAdaptObjectViaAdapterManager() { + // given + var object = new ManagerAdaptableClass(); + AdapterManager.INSTANCE.registerFactory(myTestFactory); + + // when + var adapted = AdapterUtils.adapt(object, InterfaceA.class); + + // then + assertThat(adapted) + .isPresent() + .get().isInstanceOf(InterfaceA.class); + } + + interface InterfaceA { + + } + + static class ClassB implements InterfaceA { + + } + + /** class that can adapt itself to IAdaptable */ + static class SelfAdaptableClass implements IAdaptable { + + @Nonnull + @Override + public Optional getAdapter(@Nonnull Class theTargetType) { + if (theTargetType.isAssignableFrom(InterfaceA.class)) { + T value = theTargetType.cast(buildInterfaceAWrapper(this)); + return Optional.of(value); + } + return Optional.empty(); + } + } + + private static @Nonnull InterfaceA buildInterfaceAWrapper(Object theObject) { + return new InterfaceA() {}; + } + + /** Class that relies on an external IAdapterFactory */ + static class ManagerAdaptableClass { + } + + + static class TestAdaptorFactory implements IAdapterFactory { + + @Override + public Optional getAdapter(Object theObject, Class theAdapterType) { + if (theObject instanceof ManagerAdaptableClass && theAdapterType == InterfaceA.class) { + T adapter = theAdapterType.cast(buildInterfaceAWrapper(theObject)); + return Optional.of(adapter); + } + return Optional.empty(); + } + + @Override + public Collection> getAdapters() { + return Set.of(InterfaceA.class); + } + } +} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs-again.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs-again.yaml new file mode 100644 index 00000000000..8ef5947d66e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs-again.yaml @@ -0,0 +1,4 @@ +--- +type: change +jira: SMILE-9161 +title: "Contained resources which arrive without assigned IDs are now assigned GUIDs, as opposed to monotonically increasing numeric IDs. This avoids a whole class of issues related to processing order and collisions." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs.yaml new file mode 100644 index 00000000000..1c76ceaa3a8 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6403-json-parser-bugs.yaml @@ -0,0 +1,6 @@ +--- +type: fix +jira: SMILE-9161 +title: "Fixed a rare bug in the JSON Parser, wherein client-assigned contained resource IDs could collide with server-assigned contained IDs. For example if a +resource had a client-assigned contained ID of `#2`, and a contained resource with no ID, then depending on the processing order, the parser could occasionally +provide duplicate contained resource IDs, leading to non-deterministic behaviour." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml new file mode 100644 index 00000000000..4d0100526ad --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6419-fix-reindex-optimize-storage-all-versions-posgtres.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 6419 +title: "Previously, on Postgres, the `$reindex` operation with `optimizeStorage` set to `ALL_VERSIONS` would process +only a subset of versions if there were more than 100 versions to be processed for a resource. This has been fixed +so that all versions of the resource are now processed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml new file mode 100644 index 00000000000..ee409dc43ee --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6420-fix-reindex-optimize-storage-all-versions-for-a-single-resource.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 6420 +title: "Previously, when the `$reindex` operation is run for a single FHIR resource with `optimizeStorage` set to +`ALL_VERSIONS`, none of the versions of the resource were processed in `hfj_res_ver` table. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml new file mode 100644 index 00000000000..9b6dc6320ce --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6422-fixes-remote-terminology-issues.yaml @@ -0,0 +1,7 @@ +--- +type: fix +issue: 6422 +title: "Previously, since 7.4.4 the validation issue detail codes were not translated correctly for Remote Terminology +validateCode calls. The detail code used was `invalid-code` for all use-cases which resulted in profile binding strength +not being applied to the issue severity as expected when validating resources against a profile. +This has been fixed and issue detail codes are translated correctly." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml new file mode 100644 index 00000000000..86f16018012 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6440-fix-hooks-not-called-for-precheck-for-cached-search.yaml @@ -0,0 +1,8 @@ +--- +type: fix +issue: 6440 +title: "Previously, if an `IInterceptorBroadcaster` was set in a `RequestDetails` object, +`STORAGE_PRECHECK_FOR_CACHED_SEARCH` hooks that were registered to that `IInterceptorBroadcaster` were not +called. Also, if an `IInterceptorBroadcaster` was set in the `RequestDetails` object, the boolean return value of the hooks +registered to that `IInterceptorBroadcaster` were not taken into account. This second issue existed for all pointcuts +that returned a boolean type, not just for `STORAGE_PRECHECK_FOR_CACHED_SEARCH`. These issues have now been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml new file mode 100644 index 00000000000..841287f278d --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6445-repository-api-multimap.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 6445 +title: "Add Multimap versions of the search() methods to Repository to support queries like `Patient?_tag=a&_tag=b`" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6451-bulk-import-job-status-not-changing-after-activation.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6451-bulk-import-job-status-not-changing-after-activation.yaml new file mode 100644 index 00000000000..ce62822d370 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6451-bulk-import-job-status-not-changing-after-activation.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 6451 +jira: SMILE-9089 +title: "Previously, activating `BulkDataImport` job would not change jobs status to `RUNNING`, +causing it to be processed multiple times instead of single time. This has been fixed." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6467-hibernate-search-lucene-version-incompatibility.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6467-hibernate-search-lucene-version-incompatibility.yaml new file mode 100644 index 00000000000..f5c95876cd1 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6467-hibernate-search-lucene-version-incompatibility.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 6467 +title: "Fixed an incompatibility between Hibernate Search and Lucene versions that caused ValueSet expansion to fail +when Hibernate Search was configured to use Lucene." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6489-change-index-add-concurrency-default.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6489-change-index-add-concurrency-default.yaml new file mode 100644 index 00000000000..86a4c3d7a59 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/6489-change-index-add-concurrency-default.yaml @@ -0,0 +1,4 @@ +--- +type: perf +issue: 6489 +title: "Change the migrator to avoid table locks when adding an index. This allows systems to continue running during upgrade." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/changes.yaml index c900db52e43..ac97e01fc90 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/changes.yaml @@ -4,7 +4,7 @@ title: "The version of a few dependencies have been bumped to more recent versions (dependent HAPI modules listed in brackets):
    -
  • org.hl7.fhir.core (Base): 6.3.18 -> 6.3.25
  • +
  • org.hl7.fhir.core (Base): 6.3.18 -> 6.4.0
  • Bower/Moment.js (hapi-fhir-testpage-overlay): 2.27.0 -> 2.29.4
  • htmlunit (Base): 3.9.0 -> 3.11.0
  • Elasticsearch (Base): 8.11.1 -> 8.14.3
  • diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6502-profile-can-be-reference-param.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6502-profile-can-be-reference-param.yaml new file mode 100644 index 00000000000..a0a8d5d1d42 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6502-profile-can-be-reference-param.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 6502 +backport: 7.6.1 +title: "Support ReferenceParam in addition to UriParam for `_profile` in queries using the SearchParameterMap to match the change in the specification from DSTU3 to R4." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md index 8b2426a7d65..d409d254582 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/clinical_reasoning/questionnaires.md @@ -48,24 +48,24 @@ Additional parameters have been added to support CQL evaluation. The following parameters are supported for the `Questionnaire/$populate` operation: -| Parameter | Type | Description | -|---------------------|---------------|-------------| -| questionnaire | Questionnaire | The Questionnaire to populate. Used when the operation is invoked at the 'type' level. | -| canonical | canonical | The canonical identifier for the Questionnaire (optionally version-specific). | -| url | uri | Canonical URL of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. | -| version | string | Version of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. | -| subject | Reference | The resource that is to be the QuestionnaireResponse.subject. The QuestionnaireResponse instance will reference the provided subject. | -| context | | Resources containing information to be used to help populate the QuestionnaireResponse. | -| context.name | string | The name of the launchContext or root Questionnaire variable the passed content should be used as for population purposes. The name SHALL correspond to a launchContext or variable delared at the root of the Questionnaire. | -| context.reference | Reference | The actual resource (or resources) to use as the value of the launchContext or variable. | -| local | boolean | Whether the server should use what resources and other knowledge it has about the referenced subject when pre-populating answers to questions. | -| launchContext | Extension | The [Questionnaire Launch Context](https://hl7.org/fhir/uv/sdc/StructureDefinition-sdc-questionnaire-launchContext.html) extension containing Resources that provide context for form processing logic (pre-population) when creating/displaying/editing a QuestionnaireResponse. | -| parameters | Parameters | Any input parameters defined in libraries referenced by the Questionnaire. | -| useServerData | boolean | Whether to use data from the server performing the evaluation. | -| data | Bundle | Data to be made available during CQL evaluation. | -| dataEndpoint | Endpoint | An endpoint to use to access data referenced by retrieve operations in libraries referenced by the Questionnaire. | -| contentEndpoint | Endpoint | An endpoint to use to access content (i.e. libraries) referenced by the Questionnaire. | -| terminologyEndpoint | Endpoint | An endpoint to use to access terminology (i.e. valuesets, codesystems, and membership testing) referenced by the Questionnaire. | +| Parameter | Type | Description | +|---------------------|--------------------|-------------| +| questionnaire | Questionnaire | The Questionnaire to populate. Used when the operation is invoked at the 'type' level. | +| canonical | canonical | The canonical identifier for the Questionnaire (optionally version-specific). | +| url | uri | Canonical URL of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. | +| version | string | Version of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. | +| subject | Reference | The resource that is to be the QuestionnaireResponse.subject. The QuestionnaireResponse instance will reference the provided subject. | +| context | | Resources containing information to be used to help populate the QuestionnaireResponse. | +| context.name | string | The name of the launchContext or root Questionnaire variable the passed content should be used as for population purposes. The name SHALL correspond to a launchContext or variable declared at the root of the Questionnaire. | +| context.content | Reference/Resource | The actual resource (or reference) to use as the value of the launchContext or variable. | +| local | boolean | Whether the server should use what resources and other knowledge it has about the referenced subject when pre-populating answers to questions. | +| launchContext | Extension | The [Questionnaire Launch Context](https://hl7.org/fhir/uv/sdc/StructureDefinition-sdc-questionnaire-launchContext.html) extension containing Resources that provide context for form processing logic (pre-population) when creating/displaying/editing a QuestionnaireResponse. | +| parameters | Parameters | Any input parameters defined in libraries referenced by the Questionnaire. | +| useServerData | boolean | Whether to use data from the server performing the evaluation. | +| data | Bundle | Data to be made available during CQL evaluation. | +| dataEndpoint | Endpoint | An endpoint to use to access data referenced by retrieve operations in libraries referenced by the Questionnaire. | +| contentEndpoint | Endpoint | An endpoint to use to access content (i.e. libraries) referenced by the Questionnaire. | +| terminologyEndpoint | Endpoint | An endpoint to use to access terminology (i.e. valuesets, codesystems, and membership testing) referenced by the Questionnaire. | ## Extract 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 42eba825755..bf72dbf645c 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 @@ -217,6 +217,12 @@ public class BulkDataImportSvcImpl implements IBulkDataImportSvc, IHasScheduledJ String biJobId = null; try { biJobId = processJob(bulkImportJobEntity); + // set job status to RUNNING so it would not be processed again + myTxTemplate.execute(t -> { + bulkImportJobEntity.setStatus(BulkImportJobStatusEnum.RUNNING); + myJobDao.save(bulkImportJobEntity); + return null; + }); } catch (Exception e) { ourLog.error("Failure while preparing bulk export extract", e); myTxTemplate.execute(t -> { @@ -256,6 +262,7 @@ public class BulkDataImportSvcImpl implements IBulkDataImportSvc, IHasScheduledJ } @Override + @Transactional public JobInfo getJobStatus(String theBiJobId) { BulkImportJobEntity theJob = findJobByBiJobId(theBiJobId); return new JobInfo() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index c7fc08cae7c..95f8cdbe480 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -134,6 +134,7 @@ import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -1697,9 +1698,15 @@ public abstract class BaseHapiFhirResourceDao extends B if (theOptimizeStorageMode == ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) { int pageSize = 100; for (int page = 0; ((long) page * pageSize) < entity.getVersion(); page++) { + + // We need to sort the pages, because we are updating the same data we are paging through. + // If not sorted explicitly, a database like Postgres returns the same data multiple times on + // different pages as the underlying data gets updated. + PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("myId")); Slice historyEntities = myResourceHistoryTableDao.findForResourceIdAndReturnEntitiesAndFetchProvenance( - PageRequest.of(page, pageSize), entity.getId(), historyEntity.getVersion()); + pageRequest, entity.getId(), historyEntity.getVersion()); + for (ResourceHistoryTable next : historyEntities) { reindexOptimizeStorageHistoryEntity(entity, next); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index fde57c39836..d21dff545d3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -205,7 +205,7 @@ public abstract class BaseHapiFhirSystemDao extends B * * However, for realistic average workloads, this should reduce the number of round trips. */ - if (idChunk.size() >= 2) { + if (!idChunk.isEmpty()) { List entityChunk = prefetchResourceTableHistoryAndProvenance(idChunk); if (thePreFetchIndexes) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBatch2WorkChunkRepository.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBatch2WorkChunkRepository.java index e9611614e45..2b646207295 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBatch2WorkChunkRepository.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBatch2WorkChunkRepository.java @@ -150,6 +150,6 @@ public interface IBatch2WorkChunkRepository @Param("status") WorkChunkStatusEnum theStatus); @Query( - "SELECT new ca.uhn.fhir.batch2.model.BatchWorkChunkStatusDTO(e.myTargetStepId, e.myStatus, min(e.myStartTime), max(e.myEndTime), avg(e.myEndTime - e.myStartTime), count(*)) FROM Batch2WorkChunkEntity e WHERE e.myInstanceId=:instanceId GROUP BY e.myTargetStepId, e.myStatus") + "SELECT new ca.uhn.fhir.batch2.model.BatchWorkChunkStatusDTO(e.myTargetStepId, e.myStatus, min(e.myStartTime), max(e.myEndTime), avg(cast((e.myEndTime - e.myStartTime) as long)), count(*)) FROM Batch2WorkChunkEntity e WHERE e.myInstanceId=:instanceId GROUP BY e.myTargetStepId, e.myStatus") List fetchWorkChunkStatusForInstance(@Param("instanceId") String theInstanceId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 477999b3d3e..f169fba81d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -605,12 +605,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { .add(SearchParameterMap.class, theParams) .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - Object outcome = CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( + boolean canUseCache = CompositeInterceptorBroadcaster.doCallHooks( myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH, params); - if (Boolean.FALSE.equals(outcome)) { + if (!canUseCache) { return null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index 5eadee42cb1..e7d688624d2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -210,7 +210,7 @@ public class QueryStack { CoordsPredicateBuilder coordsBuilder = (CoordsPredicateBuilder) builder; List> params = theParams.get(theParamName); - if (params.size() > 0 && params.get(0).size() > 0) { + if (!params.isEmpty() && !params.get(0).isEmpty()) { IQueryParameterType param = params.get(0).get(0); ParsedLocationParam location = ParsedLocationParam.from(theParams, param); double latitudeValue = location.getLatitudeValue(); @@ -2134,6 +2134,10 @@ public class QueryStack { if (nextParam.getModifier() == TokenParamModifier.NOT) { paramInverted = true; } + } else if (nextOrParam instanceof ReferenceParam) { + ReferenceParam nextParam = (ReferenceParam) nextOrParam; + code = nextParam.getValue(); + system = null; } else { UriParam nextParam = (UriParam) nextOrParam; code = nextParam.getValue(); @@ -2160,8 +2164,10 @@ public class QueryStack { } } - UriParam nextParam = (UriParam) nextParamUncasted; - if (isNotBlank(nextParam.getValue())) { + if (nextParamUncasted instanceof ReferenceParam + && isNotBlank(((ReferenceParam) nextParamUncasted).getValue())) { + return true; + } else if (nextParamUncasted instanceof UriParam && isNotBlank(((UriParam) nextParamUncasted).getValue())) { return true; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java index 8cd06745910..972255d9376 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java @@ -1056,7 +1056,8 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { if (theExpansionOptions != null && !theExpansionOptions.isFailOnMissingCodeSystem() // Code system is unknown, therefore NOT_FOUND - && e.getCodeValidationIssue().getCoding() == CodeValidationIssueCoding.NOT_FOUND) { + && e.getCodeValidationIssue() + .hasIssueDetailCode(CodeValidationIssueCoding.NOT_FOUND.getCode())) { return; } throw new InternalErrorException(Msg.code(888) + e); @@ -2203,7 +2204,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { .setSeverity(IssueSeverity.ERROR) .setCodeSystemVersion(theCodeSystemVersion) .setMessage(theMessage) - .addCodeValidationIssue(new CodeValidationIssue( + .addIssue(new CodeValidationIssue( theMessage, IssueSeverity.ERROR, CodeValidationIssueCode.CODE_INVALID, diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java index 33a288ee811..ee0e08ef7ea 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.mdm.helper; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.Pointcut; @@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.function.Supplier; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; /** @@ -78,6 +80,7 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall //they are coming from an external HTTP Request. MockitoAnnotations.initMocks(this); when(myMockSrd.getInterceptorBroadcaster()).thenReturn(myMockInterceptorBroadcaster); + when(myMockInterceptorBroadcaster.callHooks(any(Pointcut.class), any(HookParams.class))).thenReturn(true); when(myMockSrd.getServletRequest()).thenReturn(myMockServletRequest); when(myMockSrd.getServer()).thenReturn(myMockRestfulServer); when(myMockSrd.getRequestId()).thenReturn("MOCK_REQUEST"); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImplTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImplTest.java index d7f85325a7f..ab890c96853 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImplTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/JpaJobPersistenceImplTest.java @@ -881,6 +881,11 @@ public class JpaJobPersistenceImplTest extends BaseJpaR4Test { public void testFetchInstanceAndWorkChunkStatus() { // Setup + Date date1 = new Date(); + Date date2 = new Date(); + + + List chunkIds = new ArrayList<>(); JobInstance instance = createInstance(); String instanceId = mySvc.storeNewInstance(instance); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java index dec666c0c15..7c3a6e67c46 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java @@ -17,6 +17,7 @@ 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; import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; import ca.uhn.fhir.jpa.bulk.imprt.model.JobFileRowProcessingModeEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings; @@ -166,6 +167,8 @@ public class BulkDataImportR4Test extends BaseJpaR4Test implements ITestDataBuil ActivateJobResult activateJobOutcome = mySvc.activateNextReadyJob(); assertTrue(activateJobOutcome.isActivated); + // validate that job changed status from READY to RUNNING + assertEquals(BulkImportJobStatusEnum.RUNNING, mySvc.getJobStatus(jobId).getStatus()); JobInstance instance = myBatch2JobHelper.awaitJobCompletion(activateJobOutcome.jobId, 60); assertNotNull(instance); @@ -196,6 +199,8 @@ public class BulkDataImportR4Test extends BaseJpaR4Test implements ITestDataBuil ActivateJobResult activateJobOutcome = mySvc.activateNextReadyJob(); assertTrue(activateJobOutcome.isActivated); + // validate that job changed status from READY to RUNNING + assertEquals(BulkImportJobStatusEnum.RUNNING, mySvc.getJobStatus(jobId).getStatus()); JobInstance instance = myBatch2JobHelper.awaitJobCompletion(activateJobOutcome.jobId); assertNotNull(instance); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java index f73670fbddc..2eb8a60aa2e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java @@ -62,6 +62,9 @@ public abstract class BaseComboParamsR4Test extends BaseJpaR4Test { myMessages.add("REUSING CACHED SEARCH"); return null; }); + + // allow searches to use cached results + when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH), ArgumentMatchers.any(HookParams.class))).thenReturn(true); } @AfterEach diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java index 636ffd02b40..6b10598174b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ContainedTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -23,6 +24,8 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.ServiceRequest; import org.hl7.fhir.r4.model.ServiceRequest.ServiceRequestIntent; import org.hl7.fhir.r4.model.ServiceRequest.ServiceRequestStatus; +import org.hl7.fhir.r4.model.Specimen; +import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index c09eed9cb62..c8b4bdbe41b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.ClasspathUtil; import org.apache.commons.lang3.StringUtils; @@ -81,6 +82,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.stream.Collectors; +import static ca.uhn.fhir.test.utilities.UuidUtils.HASH_UUID_PATTERN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -738,7 +740,8 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { String encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p); ourLog.info("Input: {}", encoded); - assertThat(encoded).contains("#1"); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); @@ -746,10 +749,12 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p); ourLog.info("Output: {}", encoded); - assertThat(encoded).contains("#1"); + String organizationUuidParsed = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuidParsed); + assertEquals(organizationUuid, organizationUuidParsed); Organization org = (Organization) p.getManagingOrganization().getResource(); - assertEquals("#1", org.getId()); + assertEquals("#" + organizationUuid, org.getId()); assertThat(org.getMeta().getTag()).hasSize(1); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 635e847d5dc..aa5fd85af49 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -2722,7 +2722,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output)); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); myCaptureQueriesListener.logInsertQueriesForCurrentThread(); assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java index 4ad723edfa3..f835c300c19 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java @@ -428,7 +428,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); // Make sure we're not introducing any extra DB operations - assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(3); + assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(2); // Read back and verify that reference is now versioned observation = myObservationDao.read(observationId); @@ -463,7 +463,7 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { IdType observationId = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); // Make sure we're not introducing any extra DB operations - assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(4); + assertThat(myCaptureQueriesListener.logSelectQueries()).hasSize(3); // Read back and verify that reference is now versioned observation = myObservationDao.read(observationId); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index 88a8def13c8..f36a093e147 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao.r4; +import static ca.uhn.fhir.test.utilities.UuidUtils.HASH_UUID_PATTERN; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -3219,8 +3220,8 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { String id = outcome.getEntry().get(0).getResponse().getLocation(); patient = myPatientDao.read(new IdType(id)); - assertEquals("#1", patient.getManagingOrganization().getReference()); - assertEquals("#1", patient.getContained().get(0).getId()); + assertThat(patient.getManagingOrganization().getReference()).containsPattern(HASH_UUID_PATTERN); + assertEquals(patient.getManagingOrganization().getReference(), patient.getContained().get(0).getId()); } @Nonnull diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 1ddb1cca9d3..8f3e849719f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -263,6 +263,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds()); } + + @Test public void testParameterWithNoValueThrowsError_InvalidChainOnCustomSearch() throws IOException { SearchParameter searchParameter = new SearchParameter(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java index c04a40f1ac7..290dfcdf9b4 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/reindex/ReindexTaskTest.java @@ -180,6 +180,59 @@ public class ReindexTaskTest extends BaseJpaR4Test { } + @Test + public void testOptimizeStorage_AllVersions_SingleResourceWithMultipleVersion() { + + // this difference of this test from testOptimizeStorage_AllVersions is that this one has only 1 resource + // (with multiple versions) in the db. There was a bug where if only one resource were being re-indexed, the + // resource wasn't processed for optimize storage. + + // Setup + IIdType patientId = createPatient(withActiveTrue()); + for (int i = 0; i < 10; i++) { + Patient p = new Patient(); + p.setId(patientId.toUnqualifiedVersionless()); + p.setActive(true); + p.addIdentifier().setValue(String.valueOf(i)); + myPatientDao.update(p, mySrd); + } + + // Move resource text to compressed storage, which we don't write to anymore but legacy + // data may exist that was previously stored there, so we're simulating that. + List allHistoryEntities = runInTransaction(() -> myResourceHistoryTableDao.findAll()); + allHistoryEntities.forEach(t->relocateResourceTextToCompressedColumn(t.getResourceId(), t.getVersion())); + + runInTransaction(()->{ + assertEquals(11, myResourceHistoryTableDao.count()); + for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) { + assertNull(history.getResourceTextVc()); + assertNotNull(history.getResource()); + } + }); + + // execute + JobInstanceStartRequest startRequest = new JobInstanceStartRequest(); + startRequest.setJobDefinitionId(JOB_REINDEX); + startRequest.setParameters( + new ReindexJobParameters() + .setOptimizeStorage(ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) + .setReindexSearchParameters(ReindexParameters.ReindexSearchParametersEnum.NONE) + ); + Batch2JobStartResponse startResponse = myJobCoordinator.startInstance(mySrd, startRequest); + myBatch2JobHelper.awaitJobCompletion(startResponse); + + // validate + runInTransaction(()->{ + assertEquals(11, myResourceHistoryTableDao.count()); + for (ResourceHistoryTable history : myResourceHistoryTableDao.findAll()) { + assertNotNull(history.getResourceTextVc()); + assertNull(history.getResource()); + } + }); + Patient patient = myPatientDao.read(patientId, mySrd); + assertTrue(patient.getActive()); + } + @Test public void testOptimizeStorage_AllVersions_CopyProvenanceEntityData() { // Setup diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java similarity index 59% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java rename to hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java index 4da204217f0..3787c611eb7 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ValidateCodeOperationWithRemoteTerminologyR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateCodeWithRemoteTerminologyR4Test.java @@ -1,29 +1,21 @@ -package ca.uhn.fhir.jpa.provider.r4; +package ca.uhn.fhir.jpa.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.JpaConfig; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; -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.annotation.RequiredParam; -import ca.uhn.fhir.rest.annotation.Search; -import ca.uhn.fhir.rest.param.UriParam; -import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import jakarta.servlet.http.HttpServletRequest; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; -import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.UriType; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.AfterEach; @@ -33,9 +25,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import java.util.ArrayList; -import java.util.List; - +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_VALIDATE_CODE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -43,15 +33,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -/* +/** * This set of integration tests that instantiates and injects an instance of * {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport} * into the ValidationSupportChain, which tests the logic of dynamically selecting the correct Remote Terminology - * implementation. It also exercises the code found in - * {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport#invokeRemoteValidateCode} + * implementation. It also exercises the validateCode output translation code found in + * {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport} */ -public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResourceProviderR4Test { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidateCodeOperationWithRemoteTerminologyR4Test.class); +public class ValidateCodeWithRemoteTerminologyR4Test extends BaseResourceProviderR4Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidateCodeWithRemoteTerminologyR4Test.class); private static final String DISPLAY = "DISPLAY"; private static final String DISPLAY_BODY_MASS_INDEX = "Body mass index (BMI) [Ratio]"; private static final String CODE_BODY_MASS_INDEX = "39156-5"; @@ -64,8 +54,8 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour protected static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); private RemoteTerminologyServiceValidationSupport mySvc; - private MyCodeSystemProvider myCodeSystemProvider; - private MyValueSetProvider myValueSetProvider; + private IValidationProviders.MyValidationProvider myCodeSystemProvider; + private IValidationProviders.MyValidationProvider myValueSetProvider; @Autowired @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) @@ -76,8 +66,8 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); myValidationSupportChain.addValidationSupport(0, mySvc); - myCodeSystemProvider = new MyCodeSystemProvider(); - myValueSetProvider = new MyValueSetProvider(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); + myValueSetProvider = new IValidationProvidersR4.MyValueSetProviderR4(); ourRestfulServerExtension.registerProvider(myCodeSystemProvider); ourRestfulServerExtension.registerProvider(myValueSetProvider); } @@ -103,11 +93,11 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour @Test public void validateCodeOperationOnCodeSystem_byCodingAndUrl_usingBuiltInCodeSystems() { - myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>(); - myCodeSystemProvider.myReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/v2-0247")); - myCodeSystemProvider.myReturnParams = new Parameters(); - myCodeSystemProvider.myReturnParams.addParameter("result", true); - myCodeSystemProvider.myReturnParams.addParameter("display", DISPLAY); + final String code = "P"; + final String system = CODE_SYSTEM_V2_0247_URI;; + + Parameters params = new Parameters().addParameter("result", true).addParameter("display", DISPLAY); + setupCodeSystemValidateCode(system, code, params); logAllConcepts(); @@ -115,8 +105,8 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour .operation() .onType(CodeSystem.class) .named(JpaConstants.OPERATION_VALIDATE_CODE) - .withParameter(Parameters.class, "coding", new Coding().setSystem(CODE_SYSTEM_V2_0247_URI).setCode("P")) - .andParameter("url", new UriType(CODE_SYSTEM_V2_0247_URI)) + .withParameter(Parameters.class, "coding", new Coding().setSystem(system).setCode(code)) + .andParameter("url", new UriType(system)) .execute(); String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); @@ -128,7 +118,7 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour @Test public void validateCodeOperationOnCodeSystem_byCodingAndUrlWhereCodeSystemIsUnknown_returnsFalse() { - myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>(); + myCodeSystemProvider.setShouldThrowExceptionForResourceNotFound(false); Parameters respParam = myClient .operation() @@ -166,21 +156,21 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour @Test public void validateCodeOperationOnValueSet_byUrlAndSystem_usingBuiltInCodeSystems() { - myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>(); - myCodeSystemProvider.myReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes")); - myValueSetProvider.myReturnValueSets = new ArrayList<>(); - myValueSetProvider.myReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/list-example-codes")); - myValueSetProvider.myReturnParams = new Parameters(); - myValueSetProvider.myReturnParams.addParameter("result", true); - myValueSetProvider.myReturnParams.addParameter("display", DISPLAY); + final String code = "alerts"; + final String system = "http://terminology.hl7.org/CodeSystem/list-example-use-codes"; + final String valueSetUrl = "http://hl7.org/fhir/ValueSet/list-example-codes"; + + Parameters params = new Parameters().addParameter("result", true).addParameter("display", DISPLAY); + setupValueSetValidateCode(valueSetUrl, system, code, params); + setupCodeSystemValidateCode(system, code, params); Parameters respParam = myClient .operation() .onType(ValueSet.class) .named(JpaConstants.OPERATION_VALIDATE_CODE) - .withParameter(Parameters.class, "code", new CodeType("alerts")) - .andParameter("system", new UriType("http://terminology.hl7.org/CodeSystem/list-example-use-codes")) - .andParameter("url", new UriType("http://hl7.org/fhir/ValueSet/list-example-codes")) + .withParameter(Parameters.class, "code", new CodeType(code)) + .andParameter("system", new UriType(system)) + .andParameter("url", new UriType(valueSetUrl)) .useHttpGet() .execute(); @@ -193,21 +183,20 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour @Test public void validateCodeOperationOnValueSet_byUrlSystemAndCode() { - myCodeSystemProvider.myReturnCodeSystems = new ArrayList<>(); - myCodeSystemProvider.myReturnCodeSystems.add((CodeSystem) new CodeSystem().setId("CodeSystem/list-example-use-codes")); - myValueSetProvider.myReturnValueSets = new ArrayList<>(); - myValueSetProvider.myReturnValueSets.add((ValueSet) new ValueSet().setId("ValueSet/list-example-codes")); - myValueSetProvider.myReturnParams = new Parameters(); - myValueSetProvider.myReturnParams.addParameter("result", true); - myValueSetProvider.myReturnParams.addParameter("display", DISPLAY_BODY_MASS_INDEX); + final String code = CODE_BODY_MASS_INDEX; + final String system = "http://terminology.hl7.org/CodeSystem/list-example-use-codes"; + final String valueSetUrl = "http://hl7.org/fhir/ValueSet/list-example-codes"; + + Parameters params = new Parameters().addParameter("result", true).addParameter("display", DISPLAY_BODY_MASS_INDEX); + setupValueSetValidateCode(valueSetUrl, system, code, params); Parameters respParam = myClient .operation() .onType(ValueSet.class) .named(JpaConstants.OPERATION_VALIDATE_CODE) - .withParameter(Parameters.class, "code", new CodeType(CODE_BODY_MASS_INDEX)) - .andParameter("url", new UriType("https://loinc.org")) - .andParameter("system", new UriType("http://loinc.org")) + .withParameter(Parameters.class, "code", new CodeType(code)) + .andParameter("url", new UriType(valueSetUrl)) + .andParameter("system", new UriType(system)) .execute(); String resp = myFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(respParam); @@ -219,7 +208,7 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour @Test public void validateCodeOperationOnValueSet_byCodingAndUrlWhereValueSetIsUnknown_returnsFalse() { - myValueSetProvider.myReturnValueSets = new ArrayList<>(); + myValueSetProvider.setShouldThrowExceptionForResourceNotFound(false); Parameters respParam = myClient .operation() @@ -238,70 +227,18 @@ public class ValidateCodeOperationWithRemoteTerminologyR4Test extends BaseResour " - Unknown or unusable ValueSet[" + UNKNOWN_VALUE_SYSTEM_URI + "]"); } - @SuppressWarnings("unused") - private static class MyCodeSystemProvider implements IResourceProvider { - private List myReturnCodeSystems; - private Parameters myReturnParams; + private void setupValueSetValidateCode(String theUrl, String theSystem, String theCode, IBaseParameters theResponseParams) { + ValueSet valueSet = myValueSetProvider.addTerminologyResource(theUrl); + myValueSetProvider.addTerminologyResource(theSystem); + myValueSetProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, valueSet.getUrl(), theCode, theResponseParams); - @Operation(name = "validate-code", idempotent = true, returnParameters = { - @OperationParam(name = "result", type = BooleanType.class, min = 1), - @OperationParam(name = "message", type = StringType.class), - @OperationParam(name = "display", type = StringType.class) - }) - public Parameters validateCode( - HttpServletRequest theServletRequest, - @IdParam(optional = true) IdType theId, - @OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl, - @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, - @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay - ) { - return myReturnParams; - } - - @Search - public List find(@RequiredParam(name = "url") UriParam theUrlParam) { - assert myReturnCodeSystems != null; - return myReturnCodeSystems; - } - - @Override - public Class getResourceType() { - return CodeSystem.class; - } + // we currently do this because VersionSpecificWorkerContextWrapper has logic to infer the system when missing + // based on the ValueSet by calling ValidationSupportUtils#extractCodeSystemForCode. + valueSet.getCompose().addInclude().setSystem(theSystem); } - @SuppressWarnings("unused") - private static class MyValueSetProvider implements IResourceProvider { - private Parameters myReturnParams; - private List myReturnValueSets; - - @Operation(name = "validate-code", idempotent = true, returnParameters = { - @OperationParam(name = "result", type = BooleanType.class, min = 1), - @OperationParam(name = "message", type = StringType.class), - @OperationParam(name = "display", type = StringType.class) - }) - public Parameters validateCode( - HttpServletRequest theServletRequest, - @IdParam(optional = true) IdType theId, - @OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl, - @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, - @OperationParam(name = "system", min = 0, max = 1) UriType theSystem, - @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay, - @OperationParam(name = "valueSet") ValueSet theValueSet - ) { - return myReturnParams; - } - - @Search - public List find(@RequiredParam(name = "url") UriParam theUrlParam) { - assert myReturnValueSets != null; - return myReturnValueSets; - } - - @Override - public Class getResourceType() { - return ValueSet.class; - } - + private void setupCodeSystemValidateCode(String theUrl, String theCode, IBaseParameters theResponseParams) { + CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl); + myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, theResponseParams); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java new file mode 100644 index 00000000000..79a656db39c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/validation/ValidateWithRemoteTerminologyTest.java @@ -0,0 +1,261 @@ +package ca.uhn.fhir.jpa.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.JpaConfig; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; +import ca.uhn.fhir.util.ClasspathUtil; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Procedure; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +import java.util.List; + +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_VALIDATE_CODE; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests resource validation with Remote Terminology bindings. + * To create a new test, you need to do 3 things: + * (1) the resource profile, if any custom one is needed should be stored in the FHIR repository + * (2) all the CodeSystem and ValueSet terminology resources need to be added to the corresponding resource provider. + * At the moment only placeholder CodeSystem/ValueSet resources are returned with id and url populated. For the moment + * there was no need to load the full resource, but that can be done if there is logic run which requires it. + * This is a minimal setup. + * (3) the Remote Terminology operation responses that are needed for the test need to be added to the corresponding + * resource provider. The intention is to record and use the responses of an actual terminology server + * e.g. OntoServer. + * This is done as a result of the fact that unit test cannot always catch bugs which are introduced as a result of + * changes in the OntoServer or FHIR Validator library, or both. + * @see #setupValueSetValidateCode + * @see #setupCodeSystemValidateCode + * The responses are in Parameters resource format where issues is an OperationOutcome resource. + */ +public class ValidateWithRemoteTerminologyTest extends BaseResourceProviderR4Test { + private static final FhirContext ourCtx = FhirContext.forR4Cached(); + + @RegisterExtension + protected static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); + private RemoteTerminologyServiceValidationSupport mySvc; + @Autowired + @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) + private ValidationSupportChain myValidationSupportChain; + private IValidationProviders.MyValidationProvider myCodeSystemProvider; + private IValidationProviders.MyValidationProvider myValueSetProvider; + + @BeforeEach + public void before() { + String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); + mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); + myValidationSupportChain.addValidationSupport(0, mySvc); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); + myValueSetProvider = new IValidationProvidersR4.MyValueSetProviderR4(); + ourRestfulServerExtension.registerProvider(myCodeSystemProvider); + ourRestfulServerExtension.registerProvider(myValueSetProvider); + } + + @AfterEach + public void after() { + myValidationSupportChain.removeValidationSupport(mySvc); + ourRestfulServerExtension.getRestfulServer().getInterceptorService().unregisterAllInterceptors(); + ourRestfulServerExtension.unregisterProvider(myCodeSystemProvider); + ourRestfulServerExtension.unregisterProvider(myValueSetProvider); + } + + @Test + public void validate_withProfileWithValidCodesFromAllBindingTypes_returnsNoErrors() { + // setup + final StructureDefinition profileEncounter = ClasspathUtil.loadResource(ourCtx, StructureDefinition.class, "validation/encounter/profile-encounter-custom.json"); + myClient.update().resource(profileEncounter).execute(); + + final String statusCode = "planned"; + final String classCode = "IMP"; + final String identifierTypeCode = "VN"; + + final String statusSystem = "http://hl7.org/fhir/encounter-status"; // implied system + final String classSystem = "http://terminology.hl7.org/CodeSystem/v3-ActCode"; + final String identifierTypeSystem = "http://terminology.hl7.org/CodeSystem/v2-0203"; + + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/encounter-status", "http://hl7.org/fhir/encounter-status", statusCode, "validation/encounter/validateCode-ValueSet-encounter-status.json"); + setupValueSetValidateCode("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode", "http://terminology.hl7.org/CodeSystem/v3-ActCode", classCode, "validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/identifier-type", "http://hl7.org/fhir/identifier-type", identifierTypeCode, "validation/encounter/validateCode-ValueSet-identifier-type.json"); + + setupCodeSystemValidateCode(statusSystem, statusCode, "validation/encounter/validateCode-CodeSystem-encounter-status.json"); + setupCodeSystemValidateCode(classSystem, classCode, "validation/encounter/validateCode-CodeSystem-v3-ActCode.json"); + setupCodeSystemValidateCode(identifierTypeSystem, identifierTypeCode, "validation/encounter/validateCode-CodeSystem-v2-0203.json"); + + Encounter encounter = new Encounter(); + encounter.getMeta().addProfile("http://example.ca/fhir/StructureDefinition/profile-encounter"); + + // required binding + encounter.setStatus(Encounter.EncounterStatus.fromCode(statusCode)); + + // preferred binding + encounter.getClass_() + .setSystem(classSystem) + .setCode(classCode) + .setDisplay("inpatient encounter"); + + // extensible binding + encounter.addIdentifier() + .getType().addCoding() + .setSystem(identifierTypeSystem) + .setCode(identifierTypeCode) + .setDisplay("Visit number"); + + // execute + List errors = getValidationErrors(encounter); + + // verify + assertThat(errors).isEmpty(); + } + + @Test + public void validate_withInvalidCode_returnsErrors() { + // setup + final String statusCode = "final"; + final String code = "10xx"; + + final String statusSystem = "http://hl7.org/fhir/observation-status"; + final String loincSystem = "http://loinc.org"; + final String system = "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM"; + + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-status", statusSystem, statusCode, "validation/observation/validateCode-ValueSet-observation-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/observation-codes", loincSystem, statusCode, "validation/observation/validateCode-ValueSet-codes.json"); + + setupCodeSystemValidateCode(statusSystem, statusCode, "validation/observation/validateCode-CodeSystem-observation-status.json"); + setupCodeSystemValidateCode(system, code, "validation/observation/validateCode-CodeSystem-ICD9CM.json"); + + Observation obs = new Observation(); + obs.setStatus(Observation.ObservationStatus.fromCode(statusCode)); + obs.getCode().addCoding().setCode(code).setSystem(system); + + // execute + List errors = getValidationErrors(obs); + assertThat(errors).hasSize(1); + + // verify + assertThat(errors.get(0)) + .contains("Unknown code '10xx' in the CodeSystem 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM"); + } + + @Test + public void validate_withProfileWithInvalidCode_returnsErrors() { + // setup + String profile = "http://example.ca/fhir/StructureDefinition/profile-procedure"; + StructureDefinition profileProcedure = ClasspathUtil.loadResource(myFhirContext, StructureDefinition.class, "validation/procedure/profile-procedure.json"); + myClient.update().resource(profileProcedure).execute(); + + final String statusCode = "completed"; + final String procedureCode1 = "417005"; + final String procedureCode2 = "xx417005"; + + final String statusSystem = "http://hl7.org/fhir/event-status"; + final String snomedSystem = "http://snomed.info/sct"; + + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode1, "validation/procedure/validateCode-ValueSet-procedure-code-valid.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode2, "validation/procedure/validateCode-ValueSet-procedure-code-invalid.json"); + + setupCodeSystemValidateCode(statusSystem, statusCode, "validation/procedure/validateCode-CodeSystem-event-status.json"); + setupCodeSystemValidateCode(snomedSystem, procedureCode1, "validation/procedure/validateCode-CodeSystem-snomed-valid.json"); + setupCodeSystemValidateCode(snomedSystem, procedureCode2, "validation/procedure/validateCode-CodeSystem-snomed-invalid.json"); + + Procedure procedure = new Procedure(); + procedure.setSubject(new Reference("Patient/P1")); + procedure.setStatus(Procedure.ProcedureStatus.fromCode(statusCode)); + procedure.getCode().addCoding().setSystem(snomedSystem).setCode(procedureCode1); + procedure.getCode().addCoding().setSystem(snomedSystem).setCode(procedureCode2); + procedure.getMeta().addProfile(profile); + + // execute + List errors = getValidationErrors(procedure); + // TODO: there is currently some duplication in the errors returned. This needs to be investigated and fixed. + // assertThat(errors).hasSize(1); + + // verify + // note that we're not selecting an explicit versions (using latest) so the message verification does not include it. + assertThat(StringUtils.join("", errors)) + .contains("Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct'") + .doesNotContain("The provided code 'http://snomed.info/sct#xx417005' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code") + .doesNotContain("http://snomed.info/sct#417005"); + } + + @Test + public void validate_withProfileWithSlicingWithValidCode_returnsNoErrors() { + // setup + String profile = "http://example.ca/fhir/StructureDefinition/profile-procedure-with-slicing"; + StructureDefinition profileProcedure = ClasspathUtil.loadResource(myFhirContext, StructureDefinition.class, "validation/procedure/profile-procedure-slicing.json"); + myClient.update().resource(profileProcedure).execute(); + + final String statusCode = "completed"; + final String procedureCode = "no-procedure-info"; + + final String statusSystem = "http://hl7.org/fhir/event-status"; + final String snomedSystem = "http://snomed.info/sct"; + final String absentUnknownSystem = "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips"; + + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/event-status", statusSystem, statusCode, "validation/procedure/validateCode-ValueSet-event-status.json"); + setupValueSetValidateCode("http://hl7.org/fhir/ValueSet/procedure-code", snomedSystem, procedureCode, "validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json"); + setupValueSetValidateCode("http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips", absentUnknownSystem, procedureCode, "validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json"); + + setupCodeSystemValidateCode(statusSystem, statusCode, "validation/procedure/validateCode-CodeSystem-event-status.json"); + setupCodeSystemValidateCode(absentUnknownSystem, procedureCode, "validation/procedure/validateCode-CodeSystem-absent-or-unknown.json"); + + Procedure procedure = new Procedure(); + procedure.setSubject(new Reference("Patient/P1")); + procedure.setStatus(Procedure.ProcedureStatus.fromCode(statusCode)); + procedure.getCode().addCoding().setSystem(absentUnknownSystem).setCode(procedureCode); + procedure.getMeta().addProfile(profile); + + // execute + List errors = getValidationErrors(procedure); + assertThat(errors).hasSize(0); + } + + private void setupValueSetValidateCode(String theUrl, String theSystem, String theCode, String theTerminologyResponseFile) { + ValueSet valueSet = myValueSetProvider.addTerminologyResource(theUrl); + myCodeSystemProvider.addTerminologyResource(theSystem); + myValueSetProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, valueSet.getUrl(), theCode, ourCtx, theTerminologyResponseFile); + + // we currently do this because VersionSpecificWorkerContextWrapper has logic to infer the system when missing + // based on the ValueSet by calling ValidationSupportUtils#extractCodeSystemForCode. + valueSet.getCompose().addInclude().setSystem(theSystem); + + // you will notice each of these calls require also a call to setupCodeSystemValidateCode + // that is necessary because VersionSpecificWorkerContextWrapper#validateCodeInValueSet + // which also attempts a validateCode against the CodeSystem after the validateCode against the ValueSet + } + + private void setupCodeSystemValidateCode(String theUrl, String theCode, String theTerminologyResponseFile) { + CodeSystem codeSystem = myCodeSystemProvider.addTerminologyResource(theUrl); + myCodeSystemProvider.addTerminologyResponse(OPERATION_VALIDATE_CODE, codeSystem.getUrl(), theCode, ourCtx, theTerminologyResponseFile); + } + + private List getValidationErrors(IBaseResource theResource) { + MethodOutcome resultProcedure = myClient.validate().resource(theResource).execute(); + OperationOutcome operationOutcome = (OperationOutcome) resultProcedure.getOperationOutcome(); + return operationOutcome.getIssue().stream() + .filter(issue -> issue.getSeverity() == OperationOutcome.IssueSeverity.ERROR) + .map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .toList(); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json new file mode 100644 index 00000000000..a553a61a1c2 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/profile-encounter-custom.json @@ -0,0 +1,49 @@ +{ + "resourceType": "StructureDefinition", + "id": "profile-encounter", + "url": "http://example.ca/fhir/StructureDefinition/profile-encounter", + "version": "0.11.0", + "name": "EncounterProfile", + "title": "Encounter Profile", + "status": "active", + "date": "2022-10-15T12:00:00+00:00", + "publisher": "Example Organization", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Encounter", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Encounter", + "derivation": "constraint", + "differential": { + "element": [ + { + "id": "Encounter.identifier.type.coding", + "path": "Encounter.identifier.type.coding", + "min": 1, + "max": "1", + "mustSupport": true + }, + { + "id": "Encounter.identifier.type.coding.system", + "path": "Encounter.identifier.type.coding.system", + "min": 1, + "fixedUri": "http://terminology.hl7.org/CodeSystem/v2-0203", + "mustSupport": true + }, + { + "id": "Encounter.identifier.type.coding.code", + "path": "Encounter.identifier.type.coding.code", + "min": 1, + "fixedCode": "VN", + "mustSupport": true + }, + { + "id": "Encounter.identifier.type.coding.display", + "path": "Encounter.identifier.type.coding.display", + "min": 1, + "fixedString": "Visit number", + "mustSupport": true + } + ] + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json new file mode 100644 index 00000000000..2399dc870ec --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-encounter-status.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "planned" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/encounter-status" + }, + { + "name": "version", + "valueString": "5.0.0-ballot" + }, + { + "name": "display", + "valueString": "Planned" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot" + } + }, + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to draft CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json new file mode 100644 index 00000000000..10747c14ee3 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v2-0203.json @@ -0,0 +1,25 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "VN" + }, + { + "name": "system", + "valueUri": "http://terminology.hl7.org/CodeSystem/v2-0203" + }, + { + "name": "version", + "valueString": "3.0.0" + }, + { + "name": "display", + "valueString": "Visit number" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json new file mode 100644 index 00000000000..b692847e0fb --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-CodeSystem-v3-ActCode.json @@ -0,0 +1,46 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "IMP" + }, + { + "name": "system", + "valueUri": "http://terminology.hl7.org/CodeSystem/v3-ActCode" + }, + { + "name": "version", + "valueString": "2018-08-12" + }, + { + "name": "display", + "valueString": "inpatient encounter" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to draft CodeSystem http://terminology.hl7.org/CodeSystem/v3-ActCode|2018-08-12" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json new file mode 100644 index 00000000000..2399dc870ec --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-encounter-status.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "planned" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/encounter-status" + }, + { + "name": "version", + "valueString": "5.0.0-ballot" + }, + { + "name": "display", + "valueString": "Planned" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot" + } + }, + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to draft CodeSystem http://hl7.org/fhir/encounter-status|5.0.0-ballot" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json new file mode 100644 index 00000000000..b0767dc2f18 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-identifier-type.json @@ -0,0 +1,52 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": false + }, + { + "name": "code", + "valueCode": "VN" + }, + { + "name": "system", + "valueUri": "http://terminology.hl7.org/CodeSystem/v2-0203" + }, + { + "name": "version", + "valueString": "3.0.0" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "not-in-vs" + } + ], + "text": "The provided code 'http://terminology.hl7.org/CodeSystem/v2-0203#VN' was not found in the value set 'http://hl7.org/fhir/ValueSet/identifier-type|5.0.0-ballot'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + } + ] + } + }, + { + "name": "message", + "valueString": "The provided code 'http://terminology.hl7.org/CodeSystem/v2-0203#VN' was not found in the value set 'http://hl7.org/fhir/ValueSet/identifier-type|5.0.0-ballot'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json new file mode 100644 index 00000000000..083dbffae43 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/encounter/validateCode-ValueSet-v3-ActEncounterCode.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "IMP" + }, + { + "name": "system", + "valueUri": "http://terminology.hl7.org/CodeSystem/v3-ActCode" + }, + { + "name": "version", + "valueString": "2018-08-12" + }, + { + "name": "display", + "valueString": "inpatient encounter" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use ValueSet http://terminology.hl7.org/ValueSet/v3-ActEncounterCode|2014-03-26" + } + }, + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to draft CodeSystem http://terminology.hl7.org/CodeSystem/v3-ActCode|2018-08-12" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json new file mode 100644 index 00000000000..831ac6660fa --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-ICD9CM.json @@ -0,0 +1,48 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": false + }, + { + "name": "code", + "valueCode": "10xx" + }, + { + "name": "system", + "valueUri": "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "invalid-code" + } + ], + "text": "Unknown code '10xx' in the CodeSystem 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM' version '0.1.0'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + } + ] + } + }, + { + "name": "message", + "valueString": "Unknown code '10xx' in the CodeSystem 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM' version '0.1.0'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json new file mode 100644 index 00000000000..7914321876c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-CodeSystem-observation-status.json @@ -0,0 +1,25 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "final" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/observation-status" + }, + { + "name": "version", + "valueString": "5.0.0-ballot" + }, + { + "name": "display", + "valueString": "Final" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json new file mode 100644 index 00000000000..4571362033f --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-codes.json @@ -0,0 +1,48 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": false + }, + { + "name": "code", + "valueCode": "10xx" + }, + { + "name": "system", + "valueUri": "http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "not-in-vs" + } + ], + "text": "The provided code 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM#10xx' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-codes|5.0.0-ballot'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + } + ] + } + }, + { + "name": "message", + "valueString": "The provided code 'http://fhir.infoway-inforoute.ca/io/psca/CodeSystem/ICD9CM#10xx' was not found in the value set 'http://hl7.org/fhir/ValueSet/observation-codes|5.0.0-ballot'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json new file mode 100644 index 00000000000..7914321876c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/observation/validateCode-ValueSet-observation-status.json @@ -0,0 +1,25 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "final" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/observation-status" + }, + { + "name": "version", + "valueString": "5.0.0-ballot" + }, + { + "name": "display", + "valueString": "Final" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json new file mode 100644 index 00000000000..8bc05c70cf0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure-slicing.json @@ -0,0 +1,79 @@ +{ + "resourceType": "StructureDefinition", + "id": "profile-procedure-with-slicing", + "url": "http://example.ca/fhir/StructureDefinition/profile-procedure-with-slicing", + "version": "0.11.0", + "name": "ProcedureProfile", + "title": "Procedure Profile", + "status": "active", + "date": "2022-10-15T12:00:00+00:00", + "publisher": "Example Organization", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Procedure", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Procedure", + "derivation": "constraint", + "differential": { + "element": [ + { + "id": "Procedure.code.coding", + "path": "Procedure.code.coding", + "slicing": { + "discriminator": [ + { + "type": "pattern", + "path": "$this" + } + ], + "description": "Discriminated by the bound value set", + "rules": "open" + }, + "mustSupport": true, + "binding": { + "strength": "preferred", + "valueSet": "http://hl7.org/fhir/ValueSet/procedure-code" + } + }, + { + "id": "Procedure.code.coding.display.extension:translation", + "path": "Procedure.code.coding.display.extension", + "sliceName": "translation" + }, + { + "id": "Procedure.code.coding.display.extension:translation.extension", + "path": "Procedure.code.coding.display.extension.extension", + "min": 2 + }, + { + "id": "Procedure.code.coding:absentOrUnknownProcedure", + "path": "Procedure.code.coding", + "sliceName": "absentOrUnknownProcedure", + "short": "Optional slice for representing a code for absent problem or for unknown procedure", + "definition": "Code representing the statement \"absent problem\" or the statement \"procedures unknown\"", + "mustSupport": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "absentOrUnknownProcedure" + } + ], + "strength": "required", + "description": "A code to identify absent or unknown procedures", + "valueSet": "http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips" + } + }, + { + "id": "Procedure.code.coding:absentOrUnknownProcedure.display.extension:translation", + "path": "Procedure.code.coding.display.extension", + "sliceName": "translation" + }, + { + "id": "Procedure.code.coding:absentOrUnknownProcedure.display.extension:translation.extension", + "path": "Procedure.code.coding.display.extension.extension", + "min": 2 + } + ] + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json new file mode 100644 index 00000000000..5315694dece --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/profile-procedure.json @@ -0,0 +1,50 @@ +{ + "resourceType": "StructureDefinition", + "id": "profile-procedure", + "url": "http://example.ca/fhir/StructureDefinition/profile-procedure", + "version": "0.11.0", + "name": "ProcedureProfile", + "title": "Procedure Profile", + "status": "active", + "date": "2022-10-15T12:00:00+00:00", + "publisher": "Example Organization", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Procedure", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Procedure", + "derivation": "constraint", + "differential": { + "element": [ + { + "id": "Procedure.code.coding", + "path": "Procedure.code.coding", + "slicing": { + "discriminator": [ + { + "type": "pattern", + "path": "$this" + } + ], + "description": "Discriminated by the bound value set", + "rules": "open" + }, + "mustSupport": true, + "binding": { + "strength": "preferred", + "valueSet": "http://hl7.org/fhir/ValueSet/procedure-code" + } + }, + { + "id": "Procedure.code.coding.display.extension:translation", + "path": "Procedure.code.coding.display.extension", + "sliceName": "translation" + }, + { + "id": "Procedure.code.coding.display.extension:translation.extension", + "path": "Procedure.code.coding.display.extension.extension", + "min": 2 + } + ] + } +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json new file mode 100644 index 00000000000..4d7b20f0881 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-absent-or-unknown.json @@ -0,0 +1,46 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "no-procedure-info" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips" + }, + { + "name": "version", + "valueString": "1.1.0" + }, + { + "name": "display", + "valueString": "No information about past history of procedures" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use CodeSystem http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips|1.1.0" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json new file mode 100644 index 00000000000..620624a991e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-event-status.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "completed" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/event-status" + }, + { + "name": "version", + "valueString": "5.0.0-ballot" + }, + { + "name": "display", + "valueString": "Completed" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use CodeSystem http://hl7.org/fhir/event-status|5.0.0-ballot" + } + }, + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to experimental CodeSystem http://hl7.org/fhir/event-status|5.0.0-ballot" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json new file mode 100644 index 00000000000..f6a86048d6e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-invalid.json @@ -0,0 +1,48 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": false + }, + { + "name": "code", + "valueCode": "xx417005" + }, + { + "name": "system", + "valueUri": "http://snomed.info/sct" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "invalid-code" + } + ], + "text": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + } + ] + } + }, + { + "name": "message", + "valueString": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json new file mode 100644 index 00000000000..a602bfda9f0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-CodeSystem-snomed-valid.json @@ -0,0 +1,25 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "417005" + }, + { + "name": "system", + "valueUri": "http://snomed.info/sct" + }, + { + "name": "version", + "valueString": "http://snomed.info/sct/32506021000036107/version/20241031" + }, + { + "name": "display", + "valueString": "Hospital re-admission" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json new file mode 100644 index 00000000000..aaee02a0023 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-absent-or-unknown-procedure.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "no-procedure-info" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips" + }, + { + "name": "version", + "valueString": "1.1.0" + }, + { + "name": "display", + "valueString": "No information about past history of procedures" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use CodeSystem http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips|1.1.0" + } + }, + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to trial-use ValueSet http://hl7.org/fhir/uv/ips/ValueSet/absent-or-unknown-procedures-uv-ips|1.1.0" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json new file mode 100644 index 00000000000..aaad08b83e8 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-event-status.json @@ -0,0 +1,25 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "final" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/procedure-status" + }, + { + "name": "version", + "valueString": "5.0.0-ballot" + }, + { + "name": "display", + "valueString": "Final" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json new file mode 100644 index 00000000000..4dcb4791944 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid-slice.json @@ -0,0 +1,48 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": false + }, + { + "name": "code", + "valueCode": "no-procedure-info" + }, + { + "name": "system", + "valueUri": "http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "not-in-vs" + } + ], + "text": "The provided code 'http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips#no-procedure-info' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + } + ] + } + }, + { + "name": "message", + "valueString": "The provided code 'http://hl7.org/fhir/uv/ips/CodeSystem/absent-unknown-uv-ips#no-procedure-info' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json new file mode 100644 index 00000000000..fac3785fe2d --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-invalid.json @@ -0,0 +1,67 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": false + }, + { + "name": "code", + "valueCode": "xx417005" + }, + { + "name": "system", + "valueUri": "http://snomed.info/sct" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "not-in-vs" + } + ], + "text": "The provided code 'http://snomed.info/sct#xx417005' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + }, + { + "severity": "error", + "code": "code-invalid", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "invalid-code" + } + ], + "text": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'" + }, + "location": [ + "code" + ], + "expression": [ + "code" + ] + } + ] + } + }, + { + "name": "message", + "valueString": "Unknown code 'xx417005' in the CodeSystem 'http://snomed.info/sct' version 'http://snomed.info/sct/32506021000036107/version/20241031'; The provided code 'http://snomed.info/sct#xx417005' was not found in the value set 'http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot'" + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json new file mode 100644 index 00000000000..4554379edad --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/validation/procedure/validateCode-ValueSet-procedure-code-valid.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "result", + "valueBoolean": true + }, + { + "name": "code", + "valueCode": "417005" + }, + { + "name": "system", + "valueUri": "http://snomed.info/sct" + }, + { + "name": "version", + "valueString": "http://snomed.info/sct/32506021000036107/version/20241031" + }, + { + "name": "display", + "valueString": "Hospital re-admission" + }, + { + "name": "issues", + "resource": { + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to draft ValueSet http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot" + } + }, + { + "severity": "information", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", + "code": "status-check" + } + ], + "text": "Reference to experimental ValueSet http://hl7.org/fhir/ValueSet/procedure-code|5.0.0-ballot" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/H2_EMBEDDED.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/H2_EMBEDDED.sql index fd2eb6c3c98..095d79d0fb3 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/H2_EMBEDDED.sql +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/H2_EMBEDDED.sql @@ -51,3 +51,33 @@ INSERT INTO TRM_CONCEPT_DESIG ( 54, 150 ); + +INSERT INTO HFJ_RES_LINK ( + PID, + PARTITION_DATE, + PARTITION_ID, + SRC_PATH, + SRC_RESOURCE_ID, + SOURCE_RESOURCE_TYPE, + TARGET_RESOURCE_ID, + TARGET_RESOURCE_TYPE, + TARGET_RESOURCE_URL, + TARGET_RESOURCE_VERSION, + SP_UPDATED, + TARGET_RES_PARTITION_ID, + TARGET_RES_PARTITION_DATE +) VALUES ( + 702, + '2024-11-05', + 1, + 'Observation.subject.where(resolve() is Patient)', + 1656, + 'Observation', + 1906, + 'Patient', + 'http://localhost:8000/Patient/123', + 1, + '2024-11-01 18:01:12.921', + 1, + '2024-11-05' +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/MSSQL_2012.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/MSSQL_2012.sql index fd2eb6c3c98..82df6924393 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/MSSQL_2012.sql +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/MSSQL_2012.sql @@ -51,3 +51,33 @@ INSERT INTO TRM_CONCEPT_DESIG ( 54, 150 ); + +INSERT INTO HFJ_RES_LINK ( + PID, + PARTITION_DATE, + PARTITION_ID, + SRC_PATH, + SRC_RESOURCE_ID, + SOURCE_RESOURCE_TYPE, + TARGET_RESOURCE_ID, + TARGET_RESOURCE_TYPE, + TARGET_RESOURCE_URL, + TARGET_RESOURCE_VERSION, + SP_UPDATED, + TARGET_RES_PARTITION_ID, + TARGET_RES_PARTITION_DATE +) VALUES ( + 702, + '2024-11-05', + 1, + 'Observation.subject.where(resolve() is Patient)', + 1653, + 'Observation', + 1906, + 'Patient', + 'http://localhost:8000/Patient/123', + 1, + '2024-11-01 18:01:12.921', + 1, + '2024-11-05' +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/ORACLE_12C.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/ORACLE_12C.sql index 442d6661919..ecb31b3d587 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/ORACLE_12C.sql +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/ORACLE_12C.sql @@ -51,3 +51,33 @@ INSERT INTO TRM_CONCEPT_DESIG ( 54, 150 ); + +INSERT INTO HFJ_RES_LINK ( + PID, + PARTITION_DATE, + PARTITION_ID, + SRC_PATH, + SRC_RESOURCE_ID, + SOURCE_RESOURCE_TYPE, + TARGET_RESOURCE_ID, + TARGET_RESOURCE_TYPE, + TARGET_RESOURCE_URL, + TARGET_RESOURCE_VERSION, + SP_UPDATED, + TARGET_RES_PARTITION_ID, + TARGET_RES_PARTITION_DATE +) VALUES ( + 702, + SYSDATE, + 1, + 'Observation.subject.where(resolve() is Patient)', + 1653, + 'Observation', + 1906, + 'Patient', + 'http://localhost:8000/Patient/123', + 1, + SYSDATE, + 1, + SYSDATE +); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/POSTGRES_9_4.sql b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/POSTGRES_9_4.sql index fd2eb6c3c98..82df6924393 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/POSTGRES_9_4.sql +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/migration/releases/V7_4_0/data/POSTGRES_9_4.sql @@ -51,3 +51,33 @@ INSERT INTO TRM_CONCEPT_DESIG ( 54, 150 ); + +INSERT INTO HFJ_RES_LINK ( + PID, + PARTITION_DATE, + PARTITION_ID, + SRC_PATH, + SRC_RESOURCE_ID, + SOURCE_RESOURCE_TYPE, + TARGET_RESOURCE_ID, + TARGET_RESOURCE_TYPE, + TARGET_RESOURCE_URL, + TARGET_RESOURCE_VERSION, + SP_UPDATED, + TARGET_RES_PARTITION_ID, + TARGET_RES_PARTITION_DATE +) VALUES ( + 702, + '2024-11-05', + 1, + 'Observation.subject.where(resolve() is Patient)', + 1653, + 'Observation', + 1906, + 'Patient', + 'http://localhost:8000/Patient/123', + 1, + '2024-11-01 18:01:12.921', + 1, + '2024-11-05' +); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java index 39d75793f28..6670ab15a4c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java @@ -81,7 +81,7 @@ public class CompositeInterceptorBroadcaster { } if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null && retVal) { IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); - interceptorBroadcaster.callHooks(thePointcut, theParams); + retVal = interceptorBroadcaster.callHooks(thePointcut, theParams); } return retVal; } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java new file mode 100644 index 00000000000..2a670d15811 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java @@ -0,0 +1,161 @@ +package ca.uhn.fhir.rest.server.util; + +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CompositeInterceptorBroadcasterTest { + + @Mock + private IInterceptorBroadcaster myModuleBroadcasterMock; + @Mock + private IInterceptorBroadcaster myReqDetailsBroadcasterMock; + @Mock + private Pointcut myPointcutMock; + @Mock + private HookParams myHookParamsMock; + @Mock + private RequestDetails myRequestDetailsMock; + + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_RequestDetailsBroadcasterReturnsTrue_ThenReturnsTrue() { + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, + myPointcutMock, myHookParamsMock); + + assertThat(retVal).isTrue(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_RequestDetailsBroadcasterReturnsFalse_ThenReturnsFalse() { + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, + myPointcutMock, myHookParamsMock); + + assertThat(retVal).isFalse(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsFalse_ThenSkipsBroadcasterInRequestDetails_And_ReturnsFalse() { + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, + myPointcutMock, myHookParamsMock); + + assertThat(retVal).isFalse(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myReqDetailsBroadcasterMock, never()).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_NullRequestDetailsBroadcaster_ThenReturnsTrue() { + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(null); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, myPointcutMock, + myHookParamsMock); + + assertThat(retVal).isTrue(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsFalse_And_NullRequestDetailsBroadcaster_ThenReturnsFalse() { + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(null); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, myPointcutMock, + myHookParamsMock); + + assertThat(retVal).isFalse(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_NullRequestDetails_ThenReturnsTrue() { + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, null, myPointcutMock, myHookParamsMock); + + assertThat(retVal).isTrue(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenModuleBroadcasterReturnsFalse_And_NullRequestDetails_ThenReturnsFalse() { + + when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, null, myPointcutMock, myHookParamsMock); + + assertThat(retVal).isFalse(); + + verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + @Test + void doCallHooks_WhenNullModuleBroadcaster_And_NullRequestDetails_ThenReturnsTrue() { + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, null, myPointcutMock, myHookParamsMock); + + assertThat(retVal).isTrue(); + } + + @Test + void doCallHooks_WhenNullModuleBroadcaster_And_RequestDetailsBroadcasterReturnsTrue_ThenReturnsTrue() { + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); + when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, myRequestDetailsMock, myPointcutMock, myHookParamsMock); + + assertThat(retVal).isTrue(); + verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } + + + @Test + void doCallHooks_WhenNullModuleBroadcaster_And_RequestDetailsBroadcasterReturnsFalse_ThenReturnsFalse() { + when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); + when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + + boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, myRequestDetailsMock, myPointcutMock, myHookParamsMock); + + assertThat(retVal).isFalse(); + verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + } +} diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java index 88269f41756..c3489d3f631 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/Builder.java @@ -397,7 +397,7 @@ public class Builder { private final String myVersion; private final boolean myUnique; private String[] myIncludeColumns; - private boolean myOnline; + private boolean myOnline = true; public BuilderAddIndexUnique(String theVersion, boolean theUnique) { myVersion = theVersion; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java index fe8f736aef5..4dd95638937 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/reindex/v1/ReindexV1Config.java @@ -92,7 +92,7 @@ public class ReindexV1Config { "Load IDs of resources to reindex", ResourceIdListWorkChunkJson.class, myReindexLoadIdsStep) - .addLastStep("reindex-start", "Start the resource reindex", reindexStepV1()) + .addLastStep("reindex", "Start the resource reindex", reindexStepV1()) .build(); } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/BatchWorkChunkStatusDTO.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/BatchWorkChunkStatusDTO.java index 4f00448aa76..6c1918f28ee 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/BatchWorkChunkStatusDTO.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/model/BatchWorkChunkStatusDTO.java @@ -26,6 +26,7 @@ public class BatchWorkChunkStatusDTO { public final WorkChunkStatusEnum status; public final Date start; public final Date stop; + public final Double avg; public final Long totalChunks; diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/dstu3/EvaluateOperationConfig.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/dstu3/EvaluateOperationConfig.java index 22867e527ed..6598ab08d9c 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/dstu3/EvaluateOperationConfig.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/dstu3/EvaluateOperationConfig.java @@ -21,15 +21,20 @@ package ca.uhn.fhir.cr.config.dstu3; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.cr.config.CrProcessorConfig; import ca.uhn.fhir.cr.config.ProviderLoader; import ca.uhn.fhir.cr.config.ProviderSelector; import ca.uhn.fhir.rest.server.RestfulServer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.util.Arrays; import java.util.Map; +@Configuration +@Import(CrProcessorConfig.class) public class EvaluateOperationConfig { @Bean ca.uhn.fhir.cr.dstu3.library.LibraryEvaluateProvider dstu3LibraryEvaluateProvider() { diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/EvaluateOperationConfig.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/EvaluateOperationConfig.java index c0d0ae48fac..8cf8ecf7653 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/EvaluateOperationConfig.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/config/r4/EvaluateOperationConfig.java @@ -21,15 +21,20 @@ package ca.uhn.fhir.cr.config.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.cr.config.CrProcessorConfig; import ca.uhn.fhir.cr.config.ProviderLoader; import ca.uhn.fhir.cr.config.ProviderSelector; import ca.uhn.fhir.rest.server.RestfulServer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import java.util.Arrays; import java.util.Map; +@Configuration +@Import(CrProcessorConfig.class) public class EvaluateOperationConfig { @Bean ca.uhn.fhir.cr.r4.library.LibraryEvaluateProvider r4LibraryEvaluateProvider() { diff --git a/hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/r4/HapiFhirRepositoryR4Test.java b/hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/r4/HapiFhirRepositoryR4Test.java index 76996239ee5..14482228371 100644 --- a/hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/r4/HapiFhirRepositoryR4Test.java +++ b/hapi-fhir-storage-cr/src/test/java/ca/uhn/fhir/cr/r4/HapiFhirRepositoryR4Test.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -54,6 +55,25 @@ public class HapiFhirRepositoryR4Test extends BaseCrR4TestServer { assertTrue(crudTest(repository)); } + @Test + void _profileCanBeReferenceParam() { + // as per https://www.hl7.org/fhir/r4/search.html#all _profile is a reference param + var repository = new HapiFhirRepository(myDaoRegistry, setupRequestDetails(), myRestfulServer); + var profileToFind = "http://www.a-test-profile.com"; + var encounterWithProfile = new Encounter(); + encounterWithProfile.getMeta().addProfile(profileToFind); + repository.create(encounterWithProfile); + repository.create(new Encounter()); + Map> map = new HashMap<>(); + map.put("_profile", Collections.singletonList(new ReferenceParam(profileToFind))); + assertDoesNotThrow(() -> { + var returnBundle = repository.search(Bundle.class, Encounter.class, map); + assertTrue(returnBundle.hasEntry()); + assertEquals(1,returnBundle.getEntry().size()); + assertEquals(profileToFind, returnBundle.getEntryFirstRep().getResource().getMeta().getProfile().get(0).getValue()); + }); + } + Boolean crudTest(HapiFhirRepository theRepository) { diff --git a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2_1Test.java b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2_1Test.java index e03d8d6c187..18c74d8617b 100644 --- a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2_1Test.java +++ b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2_1Test.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; import ca.uhn.fhir.parser.PatientWithExtendedContactDstu3.CustomContactComponent; import ca.uhn.fhir.parser.XmlParserDstu2_1Test.TestPatientFor327; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; @@ -407,6 +408,8 @@ public class JsonParserDstu2_1Test { String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).containsSubsequence( @@ -415,14 +418,14 @@ public class JsonParserDstu2_1Test { "\"contained\": [", "{", "\"resourceType\": \"Condition\",", - "\"id\": \"1\"", + "\"id\": \"" + conditionUuid + "\"", "}", "],", "\"extension\": [", "{", "\"url\": \"test\",", "\"valueReference\": {", - "\"reference\": \"#1\"", + "\"reference\": \"#" + conditionUuid + "\"", "}", "}", "],", @@ -632,19 +635,21 @@ public class JsonParserDstu2_1Test { String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).containsSubsequence( "\"resourceType\": \"Patient\"", "\"contained\": [", "\"resourceType\": \"Condition\"", - "\"id\": \"1\"", + "\"id\": \"" + conditionUuid + "\"", "\"bodySite\": [", "\"text\": \"BODY SITE\"", "\"extension\": [", "\"url\": \"testCondition\",", "\"valueReference\": {", - "\"reference\": \"#1\"", + "\"reference\": \"#" + conditionUuid + "\"", "\"birthDate\": \"2016-04-14\"", "}" ); diff --git a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2_1Test.java b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2_1Test.java index 7b3fbd7a459..4c6fea901d8 100644 --- a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2_1Test.java +++ b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2_1Test.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.parser.FooMessageHeaderWithExplicitField.FooMessageSourceComp import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; import ca.uhn.fhir.parser.PatientWithCustomCompositeExtension.FooParentExtension; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; @@ -378,8 +379,11 @@ public class XmlParserDstu2_1Test { String encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); + assertThat(encoded).contains(""); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // Create a bundle with just the patient resource Bundle b = new Bundle(); @@ -388,35 +392,35 @@ public class XmlParserDstu2_1Test { // Encode the bundle encoded = xmlParser.encodeResourceToString(b); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "")); - assertThat(encoded).contains(""); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "")); + assertThat(encoded).contains(""); assertThat(encoded).containsSubsequence(Arrays.asList("", "")); assertThat(encoded).doesNotContainPattern("(?s).*.*"); // Re-parse the bundle patient = (Patient) xmlParser.parseResource(xmlParser.encodeResourceToString(patient)); - assertEquals("#1", patient.getManagingOrganization().getReference()); + assertEquals("#" + organizationUuid, patient.getManagingOrganization().getReference()); assertNotNull(patient.getManagingOrganization().getResource()); org = (Organization) patient.getManagingOrganization().getResource(); - assertEquals("#1", org.getIdElement().getValue()); + assertEquals("#" + organizationUuid, org.getIdElement().getValue()); assertEquals("Contained Test Organization", org.getName()); // And re-encode a second time encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared patient.getContained().clear(); patient.getManagingOrganization().setReference(null); encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared and a manually set local ID patient.getContained().clear(); @@ -447,6 +451,8 @@ public class XmlParserDstu2_1Test { String output = parser.encodeResourceToString(dr); ourLog.info(output); + String observationUuid = UuidUtils.findFirstUUID(output); + assertNotNull(observationUuid); //@formatter:off assertThat(output).containsSubsequence( @@ -456,7 +462,7 @@ public class XmlParserDstu2_1Test { "", "", "", - "", + "", "", "", "", @@ -465,7 +471,7 @@ public class XmlParserDstu2_1Test { "", "", "", - "", + "", "", ""); //@formatter:on @@ -477,7 +483,7 @@ public class XmlParserDstu2_1Test { dr = (CustomDiagnosticReport) parser.parseResource(output); assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); - assertEquals("#1", dr.getResult().get(0).getReference()); + assertEquals("#" + observationUuid, dr.getResult().get(0).getReference()); obs = (CustomObservation) dr.getResult().get(0).getResource(); assertEquals(ObservationStatus.FINAL, obs.getStatus()); @@ -500,19 +506,21 @@ public class XmlParserDstu2_1Test { String output = parser.encodeResourceToString(dr); ourLog.info(output); + String observationUuid = UuidUtils.findFirstUUID(output); + assertNotNull(observationUuid); //@formatter:off assertThat(output).containsSubsequence( "", "", "", - "", + "", "", "", "", "", "", - "", + "", "", ""); //@formatter:on @@ -524,7 +532,7 @@ public class XmlParserDstu2_1Test { dr = (DiagnosticReport) parser.parseResource(output); assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); - assertEquals("#1", dr.getResult().get(0).getReference()); + assertEquals("#" + observationUuid, dr.getResult().get(0).getReference()); obs = (Observation) dr.getResult().get(0).getResource(); assertEquals(ObservationStatus.FINAL, obs.getStatus()); @@ -832,18 +840,20 @@ public class XmlParserDstu2_1Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).containsSubsequence( "", "", "", - "", + "", "", "", "", "", - "", + "", "", "", "", @@ -911,10 +921,12 @@ public class XmlParserDstu2_1Test { IParser p = ourCtx.newXmlParser().setPrettyPrint(true); String encoded = p.encodeResourceToString(medicationPrescript); ourLog.info(encoded); + String medicationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(medicationUuid); //@formatter:on - assertThat(encoded).containsSubsequence("", "", "", "", "", "", - "", "", "", "", "", "", "", "", + assertThat(encoded).containsSubsequence("", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", ""); //@formatter:off } @@ -1185,13 +1197,15 @@ public class XmlParserDstu2_1Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).containsSubsequence( "", "", "", - "", + "", "", "", "", @@ -1199,7 +1213,7 @@ public class XmlParserDstu2_1Test { "", "", "", - "", + "", "", "", "", diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu2Test.java index 5862838b5b1..e3f230440f1 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/CustomTypeDstu2Test.java @@ -16,6 +16,7 @@ import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.CustomResource364Dstu2.CustomResource364CustomDate; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ElementUtil; import ca.uhn.fhir.util.TestUtil; import org.junit.jupiter.api.AfterAll; @@ -54,20 +55,23 @@ public class CustomTypeDstu2Test { String string = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(mo); ourLog.info(string); - + + String medicationUuid = UuidUtils.findFirstUUID(string); + assertNotNull(medicationUuid); + //@formatter:on assertThat(string).containsSubsequence( "", " ", - " ", - " ", + " ", + " ", " ", " ", " ", " ", " ", " ", - " ", + " ", " ", ""); //@formatter:on @@ -76,7 +80,7 @@ public class CustomTypeDstu2Test { medication = (Medication) mo.getMedication().getResource(); assertNotNull(medication); - assertEquals("#1", medication.getId().getValue()); + assertEquals("#" + medicationUuid, medication.getId().getValue()); assertEquals("MED TEXT", medication.getCode().getText()); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java index 85fa64620f5..ef773727395 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserDstu2Test.java @@ -50,6 +50,7 @@ import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; import ca.uhn.fhir.parser.testprofile.CommunicationProfile; import ca.uhn.fhir.parser.testprofile.PatientProfile; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.TestUtil; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; @@ -1705,14 +1706,16 @@ public class JsonParserDstu2Test { String enc = parser.encodeResourceToString(o); ourLog.info(enc); + String patientUuid = UuidUtils.findFirstUUID(enc); + assertNotNull(patientUuid); //@formatter:off assertThat(enc).containsSubsequence( "\"resourceType\": \"Observation\"", "\"contained\": [", "\"resourceType\": \"Patient\",", - "\"id\": \"1\"", - "\"reference\": \"#1\"" + "\"id\": \"" + patientUuid + "\"", + "\"reference\": \"#" + patientUuid + "\"" ); //@formatter:on diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java index 9d48d2fc830..b5586fd6445 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java @@ -62,6 +62,7 @@ import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; @@ -512,8 +513,10 @@ public class XmlParserDstu2Test { String encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); assertThat(encoded).contains(""); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // Create a bundle with just the patient resource ca.uhn.fhir.model.dstu2.resource.Bundle b = new ca.uhn.fhir.model.dstu2.resource.Bundle(); @@ -522,35 +525,37 @@ public class XmlParserDstu2Test { // Encode the bundle encoded = xmlParser.encodeResourceToString(b); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "")); - assertThat(encoded).contains(""); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "")); + assertThat(encoded).contains(""); assertThat(encoded).containsSubsequence(Arrays.asList("", "")); assertThat(encoded).doesNotContainPattern("(?s).*.*"); // Re-parse the bundle patient = (Patient) xmlParser.parseResource(xmlParser.encodeResourceToString(patient)); - assertEquals("#1", patient.getManagingOrganization().getReference().getValue()); + assertEquals("#" + organizationUuid, patient.getManagingOrganization().getReference().getValue()); assertNotNull(patient.getManagingOrganization().getResource()); org = (Organization) patient.getManagingOrganization().getResource(); - assertEquals("#1", org.getId().getValue()); + assertEquals("#" + organizationUuid, org.getId().getValue()); assertEquals("Contained Test Organization", org.getName()); // And re-encode a second time encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared patient.getContained().getContainedResources().clear(); patient.getManagingOrganization().setReference((String) null); encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared and a manually set local ID patient.getContained().getContainedResources().clear(); @@ -581,6 +586,8 @@ public class XmlParserDstu2Test { String output = parser.encodeResourceToString(dr); ourLog.info(output); + String observationUuid = UuidUtils.findFirstUUID(output); + assertNotNull(observationUuid); //@formatter:off assertThat(output).containsSubsequence( @@ -590,7 +597,7 @@ public class XmlParserDstu2Test { "", "", "", - "", + "", "", "", "", @@ -599,7 +606,7 @@ public class XmlParserDstu2Test { "", "", "", - "", + "", "", ""); //@formatter:on @@ -611,7 +618,7 @@ public class XmlParserDstu2Test { dr = (CustomDiagnosticReportDstu2) parser.parseResource(output); assertEquals(DiagnosticReportStatusEnum.FINAL, dr.getStatusElement().getValueAsEnum()); - assertEquals("#1", dr.getResult().get(0).getReference().getValueAsString()); + assertEquals("#" + observationUuid, dr.getResult().get(0).getReference().getValueAsString()); obs = (CustomObservationDstu2) dr.getResult().get(0).getResource(); assertEquals(ObservationStatusEnum.FINAL, obs.getStatusElement().getValueAsEnum()); @@ -665,19 +672,21 @@ public class XmlParserDstu2Test { String output = parser.encodeResourceToString(dr); ourLog.info(output); + String observationUuid = UuidUtils.findFirstUUID(output); + assertNotNull(observationUuid); //@formatter:off assertThat(output).containsSubsequence( "", "", "", - "", + "", "", "", "", "", "", - "", + "", "", ""); //@formatter:on @@ -689,7 +698,7 @@ public class XmlParserDstu2Test { dr = (DiagnosticReport) parser.parseResource(output); assertEquals(DiagnosticReportStatusEnum.FINAL, dr.getStatusElement().getValueAsEnum()); - assertEquals("#1", dr.getResult().get(0).getReference().getValueAsString()); + assertEquals("#" + observationUuid, dr.getResult().get(0).getReference().getValueAsString()); obs = (Observation) dr.getResult().get(0).getResource(); assertEquals(ObservationStatusEnum.FINAL, obs.getStatusElement().getValueAsEnum()); @@ -1305,10 +1314,12 @@ public class XmlParserDstu2Test { IParser p = ourCtx.newXmlParser().setPrettyPrint(true); String encoded = p.encodeResourceToString(medicationPrescript); ourLog.info(encoded); + String medicationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(medicationUuid); //@formatter:on - assertThat(encoded).containsSubsequence("", "", "", "", "", "", - "", "", "", "", "", "", "", "", + assertThat(encoded).containsSubsequence("", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", ""); //@formatter:off } @@ -1561,13 +1572,15 @@ public class XmlParserDstu2Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).containsSubsequence( "", "", "", - "", + "", "", "", "", @@ -1575,7 +1588,7 @@ public class XmlParserDstu2Test { "", "", "", - "", + "", "", "", "", @@ -2535,15 +2548,17 @@ public class XmlParserDstu2Test { String enc = parser.encodeResourceToString(o); ourLog.info(enc); + String patientUuid = UuidUtils.findFirstUUID(enc); + assertNotNull(patientUuid); //@formatter:off assertThat(enc).containsSubsequence( "", "", "", - "", + "", "", - "" + "" ); //@formatter:on diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java index 268a5bbcc7b..a0de44b0fde 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/JsonParserDstu3Test.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.parser.PatientWithExtendedContactDstu3.CustomContactComponent import ca.uhn.fhir.parser.XmlParserDstu3Test.TestPatientFor327; import ca.uhn.fhir.parser.json.BaseJsonLikeValue.ScalarType; import ca.uhn.fhir.parser.json.BaseJsonLikeValue.ValueType; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; @@ -648,6 +649,8 @@ public class JsonParserDstu3Test { String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).contains( @@ -656,14 +659,14 @@ public class JsonParserDstu3Test { "\"contained\": [", "{", "\"resourceType\": \"Condition\",", - "\"id\": \"1\"", + "\"id\": \"" + conditionUuid + "\"", "}", "],", "\"extension\": [", "{", "\"url\": \"test\",", "\"valueReference\": {", - "\"reference\": \"#1\"", + "\"reference\": \"#" + conditionUuid + "\"", "}", "}", "],", @@ -920,19 +923,21 @@ public class JsonParserDstu3Test { String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).contains( "\"resourceType\": \"Patient\"", "\"contained\": [", "\"resourceType\": \"Condition\"", - "\"id\": \"1\"", + "\"id\": \"" + conditionUuid + "\"", "\"bodySite\": [", "\"text\": \"BODY SITE\"", "\"extension\": [", "\"url\": \"testCondition\",", "\"valueReference\": {", - "\"reference\": \"#1\"", + "\"reference\": \"#" + conditionUuid + "\"", "\"birthDate\": \"2016-04-14\"", "}" ); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java index 58e1477e361..2388fa72413 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.parser.FooMessageHeaderWithExplicitField.FooMessageSourceComponent; import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; import ca.uhn.fhir.parser.PatientWithCustomCompositeExtension.FooParentExtension; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; @@ -560,8 +561,11 @@ public class XmlParserDstu3Test { String encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); + assertThat(encoded).contains(""); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // Create a bundle with just the patient resource Bundle b = new Bundle(); @@ -570,35 +574,35 @@ public class XmlParserDstu3Test { // Encode the bundle encoded = xmlParser.encodeResourceToString(b); ourLog.info(encoded); - assertThat(encoded).contains(Arrays.asList("", "", "")); - assertThat(encoded).contains(""); + assertThat(encoded).contains(Arrays.asList("", "", "")); + assertThat(encoded).contains(""); assertThat(encoded).contains(Arrays.asList("", "")); assertThat(encoded).doesNotContainPattern("(?s).*.*"); // Re-parse the bundle patient = (Patient) xmlParser.parseResource(xmlParser.encodeResourceToString(patient)); - assertEquals("#1", patient.getManagingOrganization().getReference()); + assertEquals("#" + organizationUuid, patient.getManagingOrganization().getReference()); assertNotNull(patient.getManagingOrganization().getResource()); org = (Organization) patient.getManagingOrganization().getResource(); - assertEquals("#1", org.getIdElement().getValue()); + assertEquals("#" + organizationUuid, org.getIdElement().getValue()); assertEquals("Contained Test Organization", org.getName()); // And re-encode a second time encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).contains(Arrays.asList("", "", "", "")); + assertThat(encoded).contains(Arrays.asList("", "", "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared patient.getContained().clear(); patient.getManagingOrganization().setReference(null); encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).contains(Arrays.asList("", "", "", "")); + assertThat(encoded).contains(Arrays.asList("", "", "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared and a manually set local ID patient.getContained().clear(); @@ -629,6 +633,8 @@ public class XmlParserDstu3Test { String output = parser.encodeResourceToString(dr); ourLog.info(output); + String observationUuid = UuidUtils.findFirstUUID(output); + assertNotNull(observationUuid); assertThat(output).contains( "", @@ -637,7 +643,7 @@ public class XmlParserDstu3Test { "", "", "", - "", + "", "", "", "", @@ -646,7 +652,7 @@ public class XmlParserDstu3Test { "", "", "", - "", + "", "", ""); @@ -657,7 +663,7 @@ public class XmlParserDstu3Test { dr = (CustomDiagnosticReport) parser.parseResource(output); assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); - assertEquals("#1", dr.getResult().get(0).getReference()); + assertEquals("#" + observationUuid, dr.getResult().get(0).getReference()); obs = (CustomObservation) dr.getResult().get(0).getResource(); assertEquals(ObservationStatus.FINAL, obs.getStatus()); @@ -680,18 +686,20 @@ public class XmlParserDstu3Test { String output = parser.encodeResourceToString(dr); ourLog.info(output); + String observationUuid = UuidUtils.findFirstUUID(output); + assertNotNull(observationUuid); assertThat(output).contains( "", "", "", - "", + "", "", "", "", "", "", - "", + "", "", ""); @@ -702,7 +710,7 @@ public class XmlParserDstu3Test { dr = (DiagnosticReport) parser.parseResource(output); assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); - assertEquals("#1", dr.getResult().get(0).getReference()); + assertEquals("#" + observationUuid, dr.getResult().get(0).getReference()); obs = (Observation) dr.getResult().get(0).getResource(); assertEquals(ObservationStatus.FINAL, obs.getStatus()); @@ -1282,17 +1290,19 @@ public class XmlParserDstu3Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); assertThat(encoded).contains( "", "", "", - "", + "", "", "", "", "", - "", + "", "", "", "", @@ -1359,10 +1369,12 @@ public class XmlParserDstu3Test { IParser p = ourCtx.newXmlParser().setPrettyPrint(true); String encoded = p.encodeResourceToString(medicationPrescript); ourLog.info(encoded); + String medicationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(medicationUuid); assertThat(encoded).contains - ("", "", "", "", "", "", - "", "", "", "", "", "", "", "", + ("", "", "", "", "", "", + "", "", "", "", "", "", "", "", "", "", ""); } @@ -1726,12 +1738,14 @@ public class XmlParserDstu3Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); assertThat(encoded).contains( "", "", "", - "", + "", "", "", "", @@ -1739,7 +1753,7 @@ public class XmlParserDstu3Test { "", "", "", - "", + "", "", "", "", diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java index 6889414f15b..6a5ead67c1c 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/JsonParserHl7OrgDstu2Test.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.annotation.Child; import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.test.utilities.UuidUtils; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.TestUtil; import net.sf.json.JSON; @@ -58,6 +59,7 @@ import java.io.StringReader; import java.util.Arrays; import java.util.List; +import static ca.uhn.fhir.test.utilities.UuidUtils.UUID_PATTERN; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -408,7 +410,10 @@ public class JsonParserHl7OrgDstu2Test { String encoded = jsonParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"1\"", "\"identifier\"", "\"reference\": \"#1\"")); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); + + assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"" + organizationUuid + "\"", "\"identifier\"", "\"reference\": \"#" + organizationUuid + "\"")); // Create a bundle with just the patient resource Bundle b = new Bundle(); @@ -417,21 +422,21 @@ public class JsonParserHl7OrgDstu2Test { // Encode the bundle encoded = jsonParser.encodeResourceToString(b); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"1\"", "\"identifier\"", "\"reference\": \"#1\"")); + assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"" + organizationUuid + "\"", "\"identifier\"", "\"reference\": \"#" + organizationUuid + "\"")); // Re-parse the bundle patient = (Patient) jsonParser.parseResource(jsonParser.encodeResourceToString(patient)); - assertEquals("#1", patient.getManagingOrganization().getReference()); + assertEquals("#" + organizationUuid, patient.getManagingOrganization().getReference()); assertNotNull(patient.getManagingOrganization().getResource()); org = (Organization) patient.getManagingOrganization().getResource(); - assertEquals("#1", org.getIdElement().getValue()); + assertEquals("#" + organizationUuid, org.getIdElement().getValue()); assertEquals("Contained Test Organization", org.getName()); // And re-encode a second time encoded = jsonParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"1\"", "\"identifier\"", "\"reference\": \"#1\"")); + assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"" + organizationUuid + "\"", "\"identifier\"", "\"reference\": \"#" + organizationUuid + "\"")); assertThat(encoded).doesNotContainPattern("(?s)\"contained\":.*\\[.*\"contained\":"); // And re-encode once more, with the references cleared @@ -439,7 +444,7 @@ public class JsonParserHl7OrgDstu2Test { patient.getManagingOrganization().setReference(null); encoded = jsonParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"1\"", "\"identifier\"", "\"reference\": \"#1\"")); + assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\": [", "\"id\": \"" + organizationUuid + "\"", "\"identifier\"", "\"reference\": \"#" + organizationUuid + "\"")); assertThat(encoded).doesNotContainPattern("(?s).*\"contained\":.*\\[.*\"contained\":"); // And re-encode once more, with the references cleared and a manually set local ID @@ -472,13 +477,16 @@ public class JsonParserHl7OrgDstu2Test { // Encode the buntdle String encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(b); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\"", "resourceType\": \"Organization", "id\": \"1\"")); - assertThat(encoded).contains("reference\": \"#1\""); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); + + assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\"", "resourceType\": \"Organization", "id\": \"" + organizationUuid + "\"")); + assertThat(encoded).contains("reference\": \"#" + organizationUuid + "\""); encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\"", "resourceType\": \"Organization", "id\": \"1\"")); - assertThat(encoded).contains("reference\": \"#1\""); + assertThat(encoded).containsSubsequence(Arrays.asList("\"contained\"", "resourceType\": \"Organization", "id\": \"" + organizationUuid + "\"")); + assertThat(encoded).contains("reference\": \"#" + organizationUuid + "\""); } @Test @@ -696,7 +704,7 @@ public class JsonParserHl7OrgDstu2Test { String enc = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(list); ourLog.info(enc); - assertThat(enc).contains("\"id\": \"1\""); + assertThat(enc).containsPattern("\"id\": \"" + UUID_PATTERN); List_ parsed = ourCtx.newJsonParser().parseResource(List_.class,enc); assertEquals(Patient.class, parsed.getEntry().get(0).getItem().getResource().getClass()); diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserHl7OrgDstu2Test.java index 4356a997a06..d9582e02ba2 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserHl7OrgDstu2Test.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.parser.JsonParserHl7OrgDstu2Test.MyPatientWithOneDeclaredAddressExtension; import ca.uhn.fhir.parser.JsonParserHl7OrgDstu2Test.MyPatientWithOneDeclaredExtension; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.test.utilities.UuidUtils; import net.sf.json.JSON; import net.sf.json.JSONSerializer; import org.apache.commons.io.IOUtils; @@ -216,8 +217,11 @@ public class XmlParserHl7OrgDstu2Test { String encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); + String organizationUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(organizationUuid); + assertThat(encoded).contains(""); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // Create a bundle with just the patient resource Bundle b = new Bundle(); @@ -226,37 +230,37 @@ public class XmlParserHl7OrgDstu2Test { // Encode the bundle encoded = xmlParser.encodeResourceToString(b); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", "")); - assertThat(encoded).contains(""); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", "")); + assertThat(encoded).contains(""); assertThat(encoded).containsSubsequence(Arrays.asList("", "")); assertThat(encoded).doesNotContainPattern("(?s).*.*"); // Re-parse the bundle patient = (Patient) xmlParser.parseResource(xmlParser.encodeResourceToString(patient)); - assertEquals("#1", patient.getManagingOrganization().getReferenceElement().getValue()); + assertEquals("#" + organizationUuid, patient.getManagingOrganization().getReferenceElement().getValue()); assertNotNull(patient.getManagingOrganization().getResource()); org = (Organization) patient.getManagingOrganization().getResource(); - assertEquals("#1", org.getIdElement().getValue()); + assertEquals("#" + organizationUuid, org.getIdElement().getValue()); assertEquals("Contained Test Organization", org.getName()); // And re-encode a second time encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", - "", "")); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", + "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared patient.getContained().clear(); patient.getManagingOrganization().setReference(null); encoded = xmlParser.encodeResourceToString(patient); ourLog.info(encoded); - assertThat(encoded).containsSubsequence(Arrays.asList("", "", - "", "")); + assertThat(encoded).containsSubsequence(Arrays.asList("", "", + "", "")); assertThat(encoded).doesNotContainPattern("(?s).*"); - assertThat(encoded).contains(""); + assertThat(encoded).contains(""); // And re-encode once more, with the references cleared and a manually set // local ID @@ -969,13 +973,15 @@ public class XmlParserHl7OrgDstu2Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); ourLog.info(encoded); + String conditionUuid = UuidUtils.findFirstUUID(encoded); + assertNotNull(conditionUuid); //@formatter:off assertThat(encoded).containsSubsequence( "", "", "", - "", + "", "", "", "", @@ -983,7 +989,7 @@ public class XmlParserHl7OrgDstu2Test { "", "", "", - "", + "", "", "", "", diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java index 25100484649..b80251d2910 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.test.BaseTest; +import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; @@ -23,10 +24,12 @@ import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.Device; +import org.hl7.fhir.r4.model.DiagnosticReport; import org.hl7.fhir.r4.model.DocumentReference; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Medication; import org.hl7.fhir.r4.model.MedicationDispense; @@ -43,6 +46,7 @@ import org.hl7.fhir.r4.model.PrimitiveType; import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Specimen; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.codesystems.DataAbsentReason; @@ -55,7 +59,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jakarta.annotation.Nonnull; +import org.testcontainers.shaded.com.trilead.ssh2.packets.PacketDisconnect; + import java.io.IOException; +import java.sql.Ref; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; @@ -261,6 +268,36 @@ public class JsonParserR4Test extends BaseTest { idx = encoded.indexOf("\"Medication\"", idx + 1); assertEquals(-1, idx); + } + + @Test + public void testDuplicateContainedResourcesAcrossABundleAreReplicated() { + Bundle b = new Bundle(); + Specimen specimen = new Specimen(); + Practitioner practitioner = new Practitioner(); + DiagnosticReport report = new DiagnosticReport(); + report.addSpecimen(new Reference(specimen)); + b.addEntry().setResource(report).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("/DiagnosticReport"); + + Observation obs = new Observation(); + obs.addPerformer(new Reference(practitioner)); + obs.setSpecimen(new Reference(specimen)); + + b.addEntry().setResource(obs).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("/Observation"); + + String encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(b); + //Then: Diag should contain one local contained specimen + assertThat(encoded).contains("[{\"resource\":{\"resourceType\":\"DiagnosticReport\",\"contained\":[{\"resourceType\":\"Specimen\",\"id\":\""+ specimen.getId().replaceFirst("#", "") +"\"}]"); + //Then: Obs should contain one local contained specimen, and one local contained pract + assertThat(encoded).contains("\"resource\":{\"resourceType\":\"Observation\",\"contained\":[{\"resourceType\":\"Specimen\",\"id\":\""+ specimen.getId().replaceFirst("#", "") +"\"},{\"resourceType\":\"Practitioner\",\"id\":\"" + practitioner.getId().replaceAll("#","") + "\"}]"); + assertThat(encoded).contains("\"performer\":[{\"reference\":\""+practitioner.getId()+"\"}],\"specimen\":{\"reference\":\""+specimen.getId()+"\"}"); + + //Also, reverting the operation should work too! + Bundle bundle = ourCtx.newJsonParser().parseResource(Bundle.class, encoded); + IBaseResource resource1 = ((DiagnosticReport) bundle.getEntry().get(0).getResource()).getSpecimenFirstRep().getResource(); + IBaseResource resource = ((Observation) bundle.getEntry().get(1).getResource()).getSpecimen().getResource(); + assertThat(resource1.getIdElement().getIdPart()).isEqualTo(resource.getIdElement().getIdPart()); + assertThat(resource1).isNotSameAs(resource); } @@ -279,8 +316,9 @@ public class JsonParserR4Test extends BaseTest { ourCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(true); encoded = ourCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(md); - assertEquals("{\"resourceType\":\"MedicationDispense\",\"contained\":[{\"resourceType\":\"Medication\",\"id\":\"1\",\"code\":{\"text\":\"MED\"}}],\"identifier\":[{\"value\":\"DISPENSE\"}],\"medicationReference\":{\"reference\":\"#1\"}}", encoded); - + String guidWithHash = med.getId(); + String withoutHash = guidWithHash.replace("#", ""); + assertThat(encoded).contains("{\"resourceType\":\"MedicationDispense\",\"contained\":[{\"resourceType\":\"Medication\",\"id\":\"" + withoutHash + "\",\"code\":{\"text\":\"MED\"}}],\"identifier\":[{\"value\":\"DISPENSE\"}],\"medicationReference\":{\"reference\":\"" + guidWithHash +"\"}}"); //Note we dont check exact ID since its a GUID } @Test @@ -571,7 +609,7 @@ public class JsonParserR4Test extends BaseTest { obs = ourCtx.newJsonParser().parseResource(Observation.class, encoded); assertEquals("#1", obs.getContained().get(0).getId()); - assertEquals("#2", obs.getContained().get(1).getId()); + assertEquals(enc.getId(), obs.getContained().get(1).getId()); pt = (Patient) obs.getSubject().getResource(); assertEquals("FAM", pt.getNameFirstRep().getFamily()); @@ -600,7 +638,7 @@ public class JsonParserR4Test extends BaseTest { obs = ourCtx.newJsonParser().parseResource(Observation.class, encoded); assertEquals("#1", obs.getContained().get(0).getId()); - assertEquals("#2", obs.getContained().get(1).getId()); + assertEquals(pt.getId(), obs.getContained().get(1).getId()); pt = (Patient) obs.getSubject().getResource(); assertEquals("FAM", pt.getNameFirstRep().getFamily()); @@ -620,13 +658,14 @@ public class JsonParserR4Test extends BaseTest { ourLog.info(encoded); mr = ourCtx.newJsonParser().parseResource(MedicationRequest.class, encoded); - mr.setMedication(new Reference(new Medication().setStatus(Medication.MedicationStatus.ACTIVE))); + Medication med = new Medication().setStatus(Medication.MedicationStatus.ACTIVE); + mr.setMedication(new Reference(med)); encoded = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(mr); ourLog.info(encoded); mr = ourCtx.newJsonParser().parseResource(MedicationRequest.class, encoded); - assertEquals("#1", mr.getContained().get(0).getId()); - assertEquals("#2", mr.getContained().get(1).getId()); + assertEquals(pract.getId(), mr.getContained().get(0).getId()); + assertEquals(med.getId(), mr.getContained().get(1).getId()); } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java index 87a33805db6..0f2e122fd6f 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/FhirTerserR4Test.java @@ -6,7 +6,10 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.api.annotation.Block; import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.parser.JsonParser; import com.google.common.collect.Lists; +import org.apache.jena.base.Sys; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseReference; @@ -47,6 +50,8 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -62,6 +67,7 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import static ca.uhn.fhir.test.utilities.UuidUtils.HASH_UUID_PATTERN; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -188,12 +194,12 @@ public class FhirTerserR4Test { FhirTerser.ContainedResources contained = myCtx.newTerser().containResources(mr, FhirTerser.OptionsEnum.MODIFY_RESOURCE, FhirTerser.OptionsEnum.STORE_AND_REUSE_RESULTS); - assertEquals("#1", mr.getContained().get(0).getId()); - assertEquals("#2", mr.getContained().get(1).getId()); + assertThat(mr.getContained().get(0).getId()).containsPattern(HASH_UUID_PATTERN); + assertThat(mr.getContained().get(1).getId()).containsPattern(HASH_UUID_PATTERN); assertEquals(ResourceType.Medication, mr.getContained().get(0).getResourceType()); assertEquals(ResourceType.Practitioner, mr.getContained().get(1).getResourceType()); - assertEquals("#1", mr.getMedicationReference().getReference()); - assertEquals("#2", mr.getRequester().getReference()); + assertEquals(mr.getContained().get(0).getId(), mr.getMedicationReference().getReference()); + assertEquals(mr.getContained().get(1).getId(), mr.getRequester().getReference()); FhirTerser.ContainedResources secondPass = myCtx.newTerser().containResources(mr, FhirTerser.OptionsEnum.MODIFY_RESOURCE, FhirTerser.OptionsEnum.STORE_AND_REUSE_RESULTS); assertThat(secondPass).isSameAs(contained); @@ -212,12 +218,12 @@ public class FhirTerserR4Test { myCtx.newTerser().containResources(medAdmin, FhirTerser.OptionsEnum.MODIFY_RESOURCE, FhirTerser.OptionsEnum.STORE_AND_REUSE_RESULTS); - assertEquals("#1", medAdmin.getContained().get(0).getId()); - assertEquals("#2", medAdmin.getContained().get(1).getId()); + assertThat(medAdmin.getContained().get(0).getId()).containsPattern(HASH_UUID_PATTERN); + assertThat(medAdmin.getContained().get(1).getId()).containsPattern(HASH_UUID_PATTERN); assertEquals(ResourceType.Medication, medAdmin.getContained().get(0).getResourceType()); assertEquals(ResourceType.Substance, medAdmin.getContained().get(1).getResourceType()); - assertEquals("#1", medAdmin.getMedicationReference().getReference()); - assertEquals("#2", ((Medication) (medAdmin.getContained().get(0))).getIngredientFirstRep().getItemReference().getReference()); + assertEquals(medAdmin.getContained().get(0).getId(), medAdmin.getMedicationReference().getReference()); + assertEquals(medAdmin.getContained().get(1).getId(), ((Medication) (medAdmin.getContained().get(0))).getIngredientFirstRep().getItemReference().getReference()); } @@ -1545,23 +1551,29 @@ public class FhirTerserR4Test { @Test void copyingAndParsingCreatesDuplicateContainedResources() { - var input = new Library(); + var library = new Library(); var params = new Parameters(); var id = "#expansion-parameters-ecr"; params.setId(id); params.addParameter("system-version", new StringType("test2")); var paramsExt = new Extension(); + paramsExt.setUrl("test").setValue(new Reference(id)); - input.addContained(params); - input.addExtension(paramsExt); + library.addContained(params); + library.addExtension(paramsExt); + final var parser = FhirContext.forR4Cached().newJsonParser(); - var stringified = parser.encodeResourceToString(input); + var stringified = parser.encodeResourceToString(library); + + var parsed = parser.parseResource(stringified); var copy = ((Library) parsed).copy(); + assertEquals(1, copy.getContained().size()); - var stringifiedCopy = parser.encodeResourceToString(copy); - var parsedCopy = parser.parseResource(stringifiedCopy); - assertEquals(1, ((Library) parsedCopy).getContained().size()); + + String stringifiedCopy = FhirContext.forR4Cached().newJsonParser().encodeResourceToString(copy); + Library parsedCopy = (Library) parser.parseResource(stringifiedCopy); + assertEquals(1, parsedCopy.getContained().size()); } /** diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/UuidUtils.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/UuidUtils.java new file mode 100644 index 00000000000..7cd9e85f213 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/UuidUtils.java @@ -0,0 +1,47 @@ +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.test.utilities; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UuidUtils { + + private UuidUtils() { + } + + public static final String UUID_PATTERN = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"; + public static final String HASH_UUID_PATTERN = "#" + UUID_PATTERN; + private static final Pattern COMPILED_UUID_PATTERN = Pattern.compile(UUID_PATTERN); + + /** + * Extracts first UUID from String. + * Returns null if no UUID present in the String. + */ + public static String findFirstUUID(String input) { + Matcher matcher = COMPILED_UUID_PATTERN.matcher(input); + + if (matcher.find()) { + return matcher.group(); + } + return null; + } + +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java new file mode 100644 index 00000000000..d9933708e47 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProviders.java @@ -0,0 +1,123 @@ +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.test.utilities.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.util.ClasspathUtil; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IDomainResource; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public interface IValidationProviders { + String CODE_SYSTEM = "http://code.system/url"; + String CODE_SYSTEM_VERSION = "1.0.0"; + String CODE_SYSTEM_NAME = "Test Code System"; + String CODE = "CODE"; + String VALUE_SET_URL = "http://value.set/url"; + String DISPLAY = "Explanation for code TestCode."; + String LANGUAGE = "en"; + String ERROR_MESSAGE = "This is an error message"; + + interface IMyValidationProvider extends IResourceProvider { + void addException(String theOperation, String theUrl, String theCode, Exception theException); +

    void addTerminologyResponse(String theOperation, String theUrl, String theCode, P theReturnParams); + IBaseParameters addTerminologyResponse(String theOperation, String theUrl, String theCode, FhirContext theFhirContext, String theTerminologyResponseFile); + } + + abstract class MyValidationProvider implements IMyValidationProvider { + private final Map myExceptionMap = new HashMap<>(); + private boolean myShouldThrowExceptionForResourceNotFound = true; + private final Map myTerminologyResponseMap = new HashMap<>(); + private final Map myTerminologyResourceMap = new HashMap<>(); + + static String getInputKey(String theOperation, String theUrl, String theCode) { + return theOperation + "-" + theUrl + "#" + theCode; + } + + public void setShouldThrowExceptionForResourceNotFound(boolean theShouldThrowExceptionForResourceNotFound) { + myShouldThrowExceptionForResourceNotFound = theShouldThrowExceptionForResourceNotFound; + } + + public void addException(String theOperation, String theUrl, String theCode, Exception theException) { + String inputKey = getInputKey(theOperation, theUrl, theCode); + myExceptionMap.put(inputKey, theException); + } + + abstract Class getParameterType(); + + @Override + public

    void addTerminologyResponse(String theOperation, String theUrl, String theCode, P theReturnParams) { + myTerminologyResponseMap.put(getInputKey(theOperation, theUrl, theCode), theReturnParams); + } + + public IBaseParameters addTerminologyResponse(String theOperation, String theUrl, String theCode, FhirContext theFhirContext, String theTerminologyResponseFile) { + IBaseParameters responseParams = ClasspathUtil.loadResource(theFhirContext, getParameterType(), theTerminologyResponseFile); + addTerminologyResponse(theOperation, theUrl, theCode, responseParams); + return responseParams; + } + + protected void addTerminologyResource(String theUrl, T theResource) { + myTerminologyResourceMap.put(theUrl, theResource); + } + + public abstract T addTerminologyResource(String theUrl); + + protected IBaseParameters getTerminologyResponse(String theOperation, String theUrl, String theCode) throws Exception { + String inputKey = getInputKey(theOperation, theUrl, theCode); + if (myExceptionMap.containsKey(inputKey)) { + throw myExceptionMap.get(inputKey); + } + IBaseParameters params = myTerminologyResponseMap.get(inputKey); + if (params == null) { + throw new IllegalStateException("Test setup incomplete. Missing return params for " + inputKey); + } + return params; + } + + protected T getTerminologyResource(UriParam theUrlParam) { + if (theUrlParam.isEmpty()) { + throw new IllegalStateException("CodeSystem url should not be null."); + } + String urlValue = theUrlParam.getValue(); + if (!myTerminologyResourceMap.containsKey(urlValue) && myShouldThrowExceptionForResourceNotFound) { + throw new IllegalStateException("Test setup incomplete. CodeSystem not found " + urlValue); + } + return myTerminologyResourceMap.get(urlValue); + } + + @Search + public List find(@RequiredParam(name = "url") UriParam theUrlParam) { + T resource = getTerminologyResource(theUrlParam); + return resource != null ? List.of(resource) : List.of(); + } + } + + interface IMyLookupCodeProvider extends IResourceProvider { + void setLookupCodeResult(IValidationSupport.LookupCodeResult theLookupCodeResult); + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java new file mode 100644 index 00000000000..11c5244df41 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersDstu3.java @@ -0,0 +1,137 @@ +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.test.utilities.validation; + +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 jakarta.servlet.http.HttpServletRequest; +import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.UriType; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; + +public interface IValidationProvidersDstu3 { + @SuppressWarnings("unused") + class MyCodeSystemProviderDstu3 extends IValidationProviders.MyValidationProvider { + @Operation(name = "$validate-code", idempotent = true, returnParameters = { + @OperationParam(name = "result", type = BooleanType.class, min = 1), + @OperationParam(name = "message", type = StringType.class), + @OperationParam(name = "display", type = StringType.class) + }) + public IBaseParameters validateCode( + HttpServletRequest theServletRequest, + @IdParam(optional = true) IdType theId, + @OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl, + @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, + @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay + ) throws Exception { + String url = theCodeSystemUrl != null ? theCodeSystemUrl.getValue() : null; + String code = theCode != null ? theCode.getValue() : null; + return getTerminologyResponse("$validate-code", url, code); + } + + @Operation(name = "$lookup", idempotent = true, returnParameters= { + @OperationParam(name = "name", type = StringType.class, min = 1), + @OperationParam(name = "version", type = StringType.class), + @OperationParam(name = "display", type = StringType.class, min = 1), + @OperationParam(name = "abstract", type = BooleanType.class, min = 1), + @OperationParam(name = "property", type = StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED) + }) + public IBaseParameters lookup( + HttpServletRequest theServletRequest, + @OperationParam(name = "code", max = 1) CodeType theCode, + @OperationParam(name = "system",max = 1) UriType theSystem, + @OperationParam(name = "coding", max = 1) Coding theCoding, + @OperationParam(name = "version", max = 1) StringType theVersion, + @OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage, + @OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, + RequestDetails theRequestDetails + ) throws Exception { + String url = theSystem != null ? theSystem.getValue() : null; + String code = theCode != null ? theCode.getValue() : null; + return getTerminologyResponse("$lookup", url, code); + } + @Override + public Class getResourceType() { + return CodeSystem.class; + } + @Override + Class getParameterType() { + return Parameters.class; + } + @Override + public CodeSystem addTerminologyResource(String theUrl) { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setId(theUrl.substring(0, theUrl.lastIndexOf("/"))); + codeSystem.setUrl(theUrl); + addTerminologyResource(theUrl, codeSystem); + return codeSystem; + } + } + + @SuppressWarnings("unused") + class MyValueSetProviderDstu3 extends IValidationProviders.MyValidationProvider { + @Operation(name = "$validate-code", idempotent = true, returnParameters = { + @OperationParam(name = "result", type = BooleanType.class, min = 1), + @OperationParam(name = "message", type = StringType.class), + @OperationParam(name = "display", type = StringType.class) + }) + public IBaseParameters validateCode( + HttpServletRequest theServletRequest, + @IdParam(optional = true) IdType theId, + @OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl, + @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, + @OperationParam(name = "system", min = 0, max = 1) UriType theSystem, + @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay, + @OperationParam(name = "valueSet") ValueSet theValueSet + ) throws Exception { + String url = theValueSetUrl != null ? theValueSetUrl.getValue() : null; + String code = theCode != null ? theCode.getValue() : null; + return getTerminologyResponse("$validate-code", url, code); + } + @Override + public Class getResourceType() { + return ValueSet.class; + } + @Override + Class getParameterType() { + return Parameters.class; + } + @Override + public ValueSet addTerminologyResource(String theUrl) { + ValueSet valueSet = new ValueSet(); + valueSet.setId(theUrl.substring(0, theUrl.lastIndexOf("/"))); + valueSet.setUrl(theUrl); + addTerminologyResource(theUrl, valueSet); + return valueSet; + } + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java similarity index 55% rename from hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java rename to hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java index fd03a8163ff..87a3dacb1fc 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/IValidateCodeProvidersR4.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/IValidationProvidersR4.java @@ -1,12 +1,29 @@ -package org.hl7.fhir.r4.validation; +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.test.utilities.validation; -import ca.uhn.fhir.jpa.model.util.JpaConstants; 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 jakarta.servlet.http.HttpServletRequest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.BooleanType; @@ -21,38 +38,29 @@ import org.hl7.fhir.r4.model.ValueSet; import java.util.List; -public interface IValidateCodeProvidersR4 { +public interface IValidationProvidersR4 { @SuppressWarnings("unused") - class MyCodeSystemProviderR4 implements IValidationProviders.IMyCodeSystemProvider { - private UriType mySystemUrl; - private CodeType myCode; - private StringType myDisplay; - private Exception myException; - private Parameters myReturnParams; + class MyCodeSystemProviderR4 extends IValidationProviders.MyValidationProvider { - @Operation(name = "validate-code", idempotent = true, returnParameters = { + @Operation(name = "$validate-code", idempotent = true, returnParameters = { @OperationParam(name = "result", type = BooleanType.class, min = 1), @OperationParam(name = "message", type = StringType.class), @OperationParam(name = "display", type = StringType.class) }) - public Parameters validateCode( + public IBaseParameters validateCode( HttpServletRequest theServletRequest, @IdParam(optional = true) IdType theId, @OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl, @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay ) throws Exception { - mySystemUrl = theCodeSystemUrl; - myCode = theCode; - myDisplay = theDisplay; - if (myException != null) { - throw myException; - } - return myReturnParams; + String url = theCodeSystemUrl != null ? theCodeSystemUrl.getValue() : null; + String code = theCode != null ? theCode.getValue() : null; + return getTerminologyResponse("$validate-code", url, code); } - @Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= { + @Operation(name = "$lookup", idempotent = true, returnParameters= { @OperationParam(name = "name", type = StringType.class, min = 1), @OperationParam(name = "version", type = StringType.class), @OperationParam(name = "display", type = StringType.class, min = 1), @@ -69,54 +77,39 @@ public interface IValidateCodeProvidersR4 { @OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, RequestDetails theRequestDetails ) throws Exception { - mySystemUrl = theSystem; - myCode = theCode; - if (myException != null) { - throw myException; - } - return myReturnParams; + String url = theSystem != null ? theSystem.getValue() : null; + String code = theCode != null ? theCode.getValue() : null; + return getTerminologyResponse("$lookup", url, code); } - @Override public Class getResourceType() { return CodeSystem.class; } - public void setException(Exception theException) { - myException = theException; - } @Override - public void setReturnParams(IBaseParameters theParameters) { - myReturnParams = (Parameters) theParameters; + Class getParameterType() { + return Parameters.class; } + @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } - public String getDisplay() { - return myDisplay != null ? myDisplay.getValue() : null; + public CodeSystem addTerminologyResource(String theUrl) { + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setId(theUrl.substring(0, theUrl.lastIndexOf("/"))); + codeSystem.setUrl(theUrl); + addTerminologyResource(theUrl, codeSystem); + return codeSystem; } } @SuppressWarnings("unused") - class MyValueSetProviderR4 implements IValidationProviders.IMyValueSetProvider { - private Exception myException; - private Parameters myReturnParams; - private UriType mySystemUrl; - private UriType myValueSetUrl; - private CodeType myCode; - private StringType myDisplay; + class MyValueSetProviderR4 extends IValidationProviders.MyValidationProvider { - @Operation(name = "validate-code", idempotent = true, returnParameters = { + @Operation(name = "$validate-code", idempotent = true, returnParameters = { @OperationParam(name = "result", type = BooleanType.class, min = 1), @OperationParam(name = "message", type = StringType.class), @OperationParam(name = "display", type = StringType.class) }) - public Parameters validateCode( + public IBaseParameters validateCode( HttpServletRequest theServletRequest, @IdParam(optional = true) IdType theId, @OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl, @@ -125,41 +118,25 @@ public interface IValidateCodeProvidersR4 { @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay, @OperationParam(name = "valueSet") ValueSet theValueSet ) throws Exception { - mySystemUrl = theSystem; - myValueSetUrl = theValueSetUrl; - myCode = theCode; - myDisplay = theDisplay; - if (myException != null) { - throw myException; - } - return myReturnParams; + String url = theValueSetUrl != null ? theValueSetUrl.getValue() : null; + String code = theCode != null ? theCode.getValue() : null; + return getTerminologyResponse("$validate-code", url, code); } - @Override public Class getResourceType() { return ValueSet.class; } - public void setException(Exception theException) { - myException = theException; + @Override + Class getParameterType() { + return Parameters.class; } @Override - public void setReturnParams(IBaseParameters theParameters) { - myReturnParams = (Parameters) theParameters; - } - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } - @Override - public String getValueSet() { - return myValueSetUrl != null ? myValueSetUrl.getValueAsString() : null; - } - public String getDisplay() { - return myDisplay != null ? myDisplay.getValue() : null; + public ValueSet addTerminologyResource(String theUrl) { + ValueSet valueSet = new ValueSet(); + valueSet.setId(theUrl.substring(0, theUrl.lastIndexOf("/"))); + valueSet.setUrl(theUrl); + addTerminologyResource(theUrl, valueSet); + return valueSet; } } } diff --git a/hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/UuidUtilsTest.java b/hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/UuidUtilsTest.java new file mode 100644 index 00000000000..c7c26bdf107 --- /dev/null +++ b/hapi-fhir-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/UuidUtilsTest.java @@ -0,0 +1,29 @@ +package ca.uhn.fhir.test.utilities; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class UuidUtilsTest { + + @Test + void testFindsUuid() { + String xml = ""; + String uuid = UuidUtils.findFirstUUID(xml); + assertEquals("cdb6dfa1-74b7-4ea9-88e0-d3afaef8c016", uuid); + } + + @Test + void testFindsFirstUuid() { + String xml = ""; + String uuid = UuidUtils.findFirstUUID(xml); + assertEquals("cdb6dfa1-74b7-4ea9-88e0-d3afaef8c016", uuid); + } + + @Test + void testNoUuidReturnsNull() { + String xml = ""; + assertNull(UuidUtils.findFirstUUID(xml)); + } +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index 1b12e20ec62..7a05e197ae6 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -190,7 +190,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { return new CodeValidationResult() .setSeverity(IssueSeverity.ERROR) .setMessage(theMessage) - .setCodeValidationIssues(Collections.singletonList(new CodeValidationIssue( + .setIssues(Collections.singletonList(new CodeValidationIssue( theMessage, IssueSeverity.ERROR, CodeValidationIssueCode.INVALID, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java index 67e553e3d3b..a617e04c41b 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/InMemoryTerminologyServerValidationSupport.java @@ -28,7 +28,6 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.Enumerations; -import org.hl7.fhir.utilities.validation.ValidationMessage; import java.util.ArrayList; import java.util.Collections; @@ -258,7 +257,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu theValidationSupportContext, theValueSet, theCodeSystemUrlAndVersion, theCode); } catch (ExpansionCouldNotBeCompletedInternallyException e) { CodeValidationResult codeValidationResult = new CodeValidationResult(); - codeValidationResult.setSeverityCode("error"); + codeValidationResult.setSeverity(IssueSeverity.ERROR); String msg = "Failed to expand ValueSet '" + vsUrl + "' (in-memory). Could not validate code " + theCodeSystemUrlAndVersion + "#" + theCode; @@ -267,7 +266,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } codeValidationResult.setMessage(msg); - codeValidationResult.addCodeValidationIssue(e.getCodeValidationIssue()); + codeValidationResult.addIssue(e.getCodeValidationIssue()); return codeValidationResult; } @@ -551,18 +550,18 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu if (valueSetResult != null) { codeValidationResult = valueSetResult; } else { - ValidationMessage.IssueSeverity severity; + IValidationSupport.IssueSeverity severity; String message; CodeValidationIssueCode issueCode = CodeValidationIssueCode.CODE_INVALID; CodeValidationIssueCoding issueCoding = CodeValidationIssueCoding.INVALID_CODE; if ("fragment".equals(codeSystemResourceContentMode)) { - severity = ValidationMessage.IssueSeverity.WARNING; + severity = IValidationSupport.IssueSeverity.WARNING; message = "Unknown code in fragment CodeSystem '" + getFormattedCodeSystemAndCodeForMessage( theCodeSystemUrlAndVersionToValidate, theCodeToValidate) + "'"; } else { - severity = ValidationMessage.IssueSeverity.ERROR; + severity = IValidationSupport.IssueSeverity.ERROR; message = "Unknown code '" + getFormattedCodeSystemAndCodeForMessage( theCodeSystemUrlAndVersionToValidate, theCodeToValidate) @@ -574,10 +573,9 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu } codeValidationResult = new CodeValidationResult() - .setSeverityCode(severity.toCode()) + .setSeverity(severity) .setMessage(message) - .addCodeValidationIssue(new CodeValidationIssue( - message, getIssueSeverityFromCodeValidationIssue(severity), issueCode, issueCoding)); + .addIssue(new CodeValidationIssue(message, severity, issueCode, issueCoding)); } return codeValidationResult; @@ -589,19 +587,6 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu + theCodeToValidate; } - private IValidationSupport.IssueSeverity getIssueSeverityFromCodeValidationIssue( - ValidationMessage.IssueSeverity theSeverity) { - switch (theSeverity) { - case ERROR: - return IValidationSupport.IssueSeverity.ERROR; - case WARNING: - return IValidationSupport.IssueSeverity.WARNING; - case INFORMATION: - return IValidationSupport.IssueSeverity.INFORMATION; - } - return null; - } - private CodeValidationResult findCodeInExpansion( String theCodeToValidate, String theDisplayToValidate, @@ -1123,8 +1108,8 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu new CodeValidationIssue( theMessage, IssueSeverity.ERROR, - CodeValidationIssueCode.OTHER, - CodeValidationIssueCoding.OTHER)); + CodeValidationIssueCode.INVALID, + CodeValidationIssueCoding.VS_INVALID)); } for (org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent next : subExpansion.getExpansion().getContains()) { @@ -1376,7 +1361,7 @@ public class InMemoryTerminologyServerValidationSupport implements IValidationSu .setCodeSystemVersion(theCodeSystemVersion) .setDisplay(theExpectedDisplay); if (issueSeverity != null) { - codeValidationResult.setCodeValidationIssues(Collections.singletonList(new CodeValidationIssue( + codeValidationResult.setIssues(Collections.singletonList(new CodeValidationIssue( message, theIssueSeverityForCodeDisplayMismatch, CodeValidationIssueCode.INVALID, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java index 370f8b423dd..398eacc52a2 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/RemoteTerminologyServiceValidationSupport.java @@ -28,6 +28,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Base; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Parameters; @@ -631,7 +632,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup return new CodeValidationResult() .setSeverity(severity) .setMessage(theMessage) - .addCodeValidationIssue(new CodeValidationIssue( + .addIssue(new CodeValidationIssue( theMessage, severity, theIssueCode, CodeValidationIssueCoding.INVALID_CODE)); } @@ -680,13 +681,13 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup createCodeValidationIssues( (IBaseOperationOutcome) issuesValue.get(), fhirContext.getVersion().getVersion()) - .ifPresent(i -> i.forEach(result::addCodeValidationIssue)); + .ifPresent(i -> i.forEach(result::addIssue)); } else { // create a validation issue out of the message // this is a workaround to overcome an issue in the FHIR Validator library // where ValueSet bindings are only reading issues but not messages // @see https://github.com/hapifhir/org.hl7.fhir.core/issues/1766 - result.addCodeValidationIssue(createCodeValidationIssue(result.getMessage())); + result.addIssue(createCodeValidationIssue(result.getMessage())); } return result; } @@ -717,23 +718,42 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup private static Collection createCodeValidationIssuesR4(OperationOutcome theOperationOutcome) { return theOperationOutcome.getIssue().stream() - .map(issueComponent -> - createCodeValidationIssue(issueComponent.getDetails().getText())) + .map(issueComponent -> { + String diagnostics = issueComponent.getDiagnostics(); + IssueSeverity issueSeverity = + IssueSeverity.fromCode(issueComponent.getSeverity().toCode()); + String issueTypeCode = issueComponent.getCode().toCode(); + CodeableConcept details = issueComponent.getDetails(); + CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode); + CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText()); + details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode())); + issue.setDetails(issueDetails); + return issue; + }) .collect(Collectors.toList()); } private static Collection createCodeValidationIssuesDstu3( org.hl7.fhir.dstu3.model.OperationOutcome theOperationOutcome) { return theOperationOutcome.getIssue().stream() - .map(issueComponent -> - createCodeValidationIssue(issueComponent.getDetails().getText())) + .map(issueComponent -> { + String diagnostics = issueComponent.getDiagnostics(); + IssueSeverity issueSeverity = + IssueSeverity.fromCode(issueComponent.getSeverity().toCode()); + String issueTypeCode = issueComponent.getCode().toCode(); + org.hl7.fhir.dstu3.model.CodeableConcept details = issueComponent.getDetails(); + CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode); + CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText()); + details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode())); + issue.setDetails(issueDetails); + return issue; + }) .collect(Collectors.toList()); } private static CodeValidationIssue createCodeValidationIssue(String theMessage) { return new CodeValidationIssue( theMessage, - // assume issue type is OperationOutcome.IssueType#CODEINVALID as it is the only match IssueSeverity.ERROR, CodeValidationIssueCode.INVALID, CodeValidationIssueCoding.INVALID_CODE); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java index 1898292c451..265debed058 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.java @@ -87,7 +87,7 @@ public class UnknownCodeSystemWarningValidationSupport extends BaseValidationSup result.setSeverity(null); result.setMessage(null); } else { - result.addCodeValidationIssue(new CodeValidationIssue( + result.addIssue(new CodeValidationIssue( theMessage, myNonExistentCodeSystemSeverity, CodeValidationIssueCode.NOT_FOUND, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java index 7321f33d8c8..7093ab2a7ff 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportUtils.java @@ -11,6 +11,17 @@ public final class ValidationSupportUtils { private ValidationSupportUtils() {} + /** + * This method extracts a code system that can be (potentially) associated with a code when + * performing validation against a ValueSet. This method was created for internal purposes. + * Please use this method with care because it will only cover some + * use-cases (e.g. standard bindings) while for others it may not return correct results or return null. + * An incorrect result could be considered if the resource declares a code with a system, and you're calling + * this method to check a binding against a ValueSet that has nothing to do with that system. + * @param theValueSet the valueSet + * @param theCode the code + * @return the system which can be associated with the code + */ public static String extractCodeSystemForCode(IBaseResource theValueSet, String theCode) { if (theValueSet instanceof org.hl7.fhir.dstu3.model.ValueSet) { return extractCodeSystemForCodeDSTU3((org.hl7.fhir.dstu3.model.ValueSet) theValueSet, theCode); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index f0f3f41e7f9..393d8ce1dc5 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -62,6 +62,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import static ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY; import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toSet; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -296,7 +297,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo theResult.getCodeSystemVersion(), conceptDefinitionComponent, display, - getIssuesForCodeValidation(theResult.getCodeValidationIssues())); + getIssuesForCodeValidation(theResult.getIssues())); } if (retVal == null) { @@ -307,73 +308,36 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } private List getIssuesForCodeValidation( - List codeValidationIssues) { - List issues = new ArrayList<>(); + List theIssues) { + List issueComponents = new ArrayList<>(); - for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeValidationIssues) { + for (IValidationSupport.CodeValidationIssue issue : theIssues) { + OperationOutcome.IssueSeverity severity = + OperationOutcome.IssueSeverity.fromCode(issue.getSeverity().getCode()); + OperationOutcome.IssueType issueType = + OperationOutcome.IssueType.fromCode(issue.getType().getCode()); + String diagnostics = issue.getDiagnostics(); - CodeableConcept codeableConcept = new CodeableConcept().setText(codeValidationIssue.getMessage()); - codeableConcept.addCoding( - "http://hl7.org/fhir/tools/CodeSystem/tx-issue-type", - getIssueCodingFromCodeValidationIssue(codeValidationIssue), - null); + IValidationSupport.CodeValidationIssueDetails details = issue.getDetails(); + CodeableConcept codeableConcept = new CodeableConcept().setText(details.getText()); + details.getCodings().forEach(detailCoding -> codeableConcept + .addCoding() + .setSystem(detailCoding.getSystem()) + .setCode(detailCoding.getCode())); - OperationOutcome.OperationOutcomeIssueComponent issue = + OperationOutcome.OperationOutcomeIssueComponent issueComponent = new OperationOutcome.OperationOutcomeIssueComponent() - .setSeverity(getIssueSeverityFromCodeValidationIssue(codeValidationIssue)) - .setCode(getIssueTypeFromCodeValidationIssue(codeValidationIssue)) - .setDetails(codeableConcept); - issue.getDetails().setText(codeValidationIssue.getMessage()); - issue.addExtension() + .setSeverity(severity) + .setCode(issueType) + .setDetails(codeableConcept) + .setDiagnostics(diagnostics); + issueComponent + .addExtension() .setUrl("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id") .setValue(new StringType("Terminology_PassThrough_TX_Message")); - issues.add(issue); + issueComponents.add(issueComponent); } - return issues; - } - - private String getIssueCodingFromCodeValidationIssue(IValidationSupport.CodeValidationIssue codeValidationIssue) { - switch (codeValidationIssue.getCoding()) { - case VS_INVALID: - return "vs-invalid"; - case NOT_FOUND: - return "not-found"; - case NOT_IN_VS: - return "not-in-vs"; - case INVALID_CODE: - return "invalid-code"; - case INVALID_DISPLAY: - return "invalid-display"; - } - return null; - } - - private OperationOutcome.IssueType getIssueTypeFromCodeValidationIssue( - IValidationSupport.CodeValidationIssue codeValidationIssue) { - switch (codeValidationIssue.getCode()) { - case NOT_FOUND: - return OperationOutcome.IssueType.NOTFOUND; - case CODE_INVALID: - return OperationOutcome.IssueType.CODEINVALID; - case INVALID: - return OperationOutcome.IssueType.INVALID; - } - return null; - } - - private OperationOutcome.IssueSeverity getIssueSeverityFromCodeValidationIssue( - IValidationSupport.CodeValidationIssue codeValidationIssue) { - switch (codeValidationIssue.getSeverity()) { - case FATAL: - return OperationOutcome.IssueSeverity.FATAL; - case ERROR: - return OperationOutcome.IssueSeverity.ERROR; - case WARNING: - return OperationOutcome.IssueSeverity.WARNING; - case INFORMATION: - return OperationOutcome.IssueSeverity.INFORMATION; - } - return null; + return issueComponents; } @Override @@ -851,25 +815,22 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo .getRootValidationSupport() .validateCodeInValueSet( myValidationSupportContext, theValidationOptions, theSystem, theCode, theDisplay, theValueSet); - if (result != null) { + if (result != null && theSystem != null) { /* We got a value set result, which could be successful, or could contain errors/warnings. The code might also be invalid in the code system, so we will check that as well and add those issues to our result. */ IValidationSupport.CodeValidationResult codeSystemResult = validateCodeInCodeSystem(theValidationOptions, theSystem, theCode, theDisplay); - final boolean valueSetResultContainsInvalidDisplay = result.getCodeValidationIssues().stream() - .anyMatch(codeValidationIssue -> codeValidationIssue.getCoding() - == IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY); + final boolean valueSetResultContainsInvalidDisplay = result.getIssues().stream() + .anyMatch(VersionSpecificWorkerContextWrapper::hasInvalidDisplayDetailCode); if (codeSystemResult != null) { - for (IValidationSupport.CodeValidationIssue codeValidationIssue : - codeSystemResult.getCodeValidationIssues()) { + for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeSystemResult.getIssues()) { /* Value set validation should already have checked the display name. If we get INVALID_DISPLAY issues from code system validation, they will only repeat what was already caught. */ - if (codeValidationIssue.getCoding() != IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY - || !valueSetResultContainsInvalidDisplay) { - result.addCodeValidationIssue(codeValidationIssue); + if (!hasInvalidDisplayDetailCode(codeValidationIssue) || !valueSetResultContainsInvalidDisplay) { + result.addIssue(codeValidationIssue); } } } @@ -877,6 +838,10 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return result; } + private static boolean hasInvalidDisplayDetailCode(IValidationSupport.CodeValidationIssue theIssue) { + return theIssue.hasIssueDetailCode(INVALID_DISPLAY.getCode()); + } + private IValidationSupport.CodeValidationResult validateCodeInCodeSystem( ConceptValidationOptions theValidationOptions, String theSystem, String theCode, String theDisplay) { return myValidationSupportContext diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java index bb7eaf1c17b..eac448ad0eb 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/ILookupCodeTest.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult; import ca.uhn.fhir.context.support.IValidationSupport.StringConceptProperty; import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.junit.jupiter.api.Test; @@ -21,12 +22,12 @@ import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_GROUP; import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_STRING; import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM_NAME; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM_VERSION; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.DISPLAY; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.LANGUAGE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_NAME; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.DISPLAY; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.LANGUAGE; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createConceptProperty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -189,8 +190,6 @@ public interface ILookupCodeTest { // verify assertNotNull(outcome); - assertEquals(theRequest.getCode(), getLookupCodeProvider().getCode()); - assertEquals(theRequest.getSystem(), getLookupCodeProvider().getSystem()); assertEquals(theExpectedResult.isFound(), outcome.isFound()); assertEquals(theExpectedResult.getErrorMessage(), outcome.getErrorMessage()); assertEquals(theExpectedResult.getCodeSystemDisplayName(), outcome.getCodeSystemDisplayName()); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java index 5ba79bd3e6f..95c4fd7d3b9 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyLookupCodeTest.java @@ -2,6 +2,7 @@ package org.hl7.fhir.common.hapi.validation; import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult; import ca.uhn.fhir.context.support.LookupCodeRequest; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.junit.jupiter.api.Test; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java index cb6bb02ac07..21c5d0dc5e7 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IRemoteTerminologyValidateCodeTest.java @@ -1,17 +1,76 @@ package org.hl7.fhir.common.hapi.validation; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.junit.jupiter.api.Test; import java.util.Collection; import java.util.List; import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createCodeValidationIssues; + public interface IRemoteTerminologyValidateCodeTest extends IValidateCodeTest { default List getCodeValidationIssues(IBaseOperationOutcome theOperationOutcome) { // this method should be removed once support for issues is fully implemented across all validator types Optional> issues = RemoteTerminologyServiceValidationSupport.createCodeValidationIssues(theOperationOutcome, getService().getFhirContext().getVersion().getVersion()); return issues.map(theCodeValidationIssues -> theCodeValidationIssues.stream().toList()).orElseGet(List::of); } + + @Test + default void createCodeValidationIssues_withCodeSystemOutcomeForInvalidCode_returnsAsExpected() { + IBaseOperationOutcome outcome = getCodeSystemInvalidCodeOutcome(); + FhirVersionEnum versionEnum = getService().getFhirContext().getVersion().getVersion(); + Optional> issuesOptional = createCodeValidationIssues(outcome, versionEnum); + assertThat(issuesOptional).isPresent(); + assertThat(issuesOptional.get()).hasSize(1); + IValidationSupport.CodeValidationIssue issue = issuesOptional.get().iterator().next(); + assertThat(issue.getType().getCode()).isEqualTo("code-invalid"); + assertThat(issue.getSeverity().getCode()).isEqualTo("error"); + assertThat(issue.getDetails().getCodings()).hasSize(1); + IValidationSupport.CodeValidationIssueCoding issueCoding = issue.getDetails().getCodings().get(0); + assertThat(issueCoding.getSystem()).isEqualTo("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type"); + assertThat(issueCoding.getCode()).isEqualTo("invalid-code"); + assertThat(issue.getDetails().getText()).isEqualTo("Unknown code 'CODE' in the CodeSystem 'http://code.system/url' version '1.0.0'"); + assertThat(issue.getDiagnostics()).isNull(); + } + + @Test + default void createCodeValidationIssues_withValueSetOutcomeForInvalidCode_returnsAsExpected() { + IBaseOperationOutcome outcome = getValueSetInvalidCodeOutcome(); + FhirVersionEnum versionEnum = getService().getFhirContext().getVersion().getVersion(); + Optional> issuesOptional = createCodeValidationIssues(outcome, versionEnum); + assertThat(issuesOptional).isPresent(); + assertThat(issuesOptional.get()).hasSize(2); + IValidationSupport.CodeValidationIssue issue = issuesOptional.get().iterator().next(); + assertThat(issue.getType().getCode()).isEqualTo("code-invalid"); + assertThat(issue.getSeverity().getCode()).isEqualTo("error"); + assertThat(issue.getDetails().getCodings()).hasSize(1); + IValidationSupport.CodeValidationIssueCoding issueCoding = issue.getDetails().getCodings().get(0); + assertThat(issueCoding.getSystem()).isEqualTo("http://hl7.org/fhir/tools/CodeSystem/tx-issue-type"); + assertThat(issueCoding.getCode()).isEqualTo("not-in-vs"); + assertThat(issue.getDetails().getText()).isEqualTo("The provided code 'http://code.system/url#CODE' was not found in the value set 'http://value.set/url%7C1.0.0'"); + assertThat(issue.getDiagnostics()).isNull(); + } + + @Test + default void createCodeValidationIssues_withValueSetOutcomeWithCustomDetailCode_returnsAsExpected() { + IBaseOperationOutcome outcome = getValueSetCustomDetailCodeOutcome(); + FhirVersionEnum versionEnum = getService().getFhirContext().getVersion().getVersion(); + Optional> issuesOptional = createCodeValidationIssues(outcome, versionEnum); + assertThat(issuesOptional).isPresent(); + assertThat(issuesOptional.get()).hasSize(1); + IValidationSupport.CodeValidationIssue issue = issuesOptional.get().iterator().next(); + assertThat(issue.getType().getCode()).isEqualTo("processing"); + assertThat(issue.getSeverity().getCode()).isEqualTo("information"); + assertThat(issue.getDetails().getCodings()).hasSize(1); + IValidationSupport.CodeValidationIssueCoding issueCoding = issue.getDetails().getCodings().get(0); + assertThat(issueCoding.getSystem()).isEqualTo("http://example.com/custom-issue-type"); + assertThat(issueCoding.getCode()).isEqualTo("valueset-is-draft"); + assertThat(issue.getDetails().getText()).isNull(); + assertThat(issue.getDiagnostics()).isEqualTo("The ValueSet status is marked as draft."); + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java index 52dbf1177a8..53411b440fe 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidateCodeTest.java @@ -4,6 +4,9 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.util.ClasspathUtil; +import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -16,12 +19,13 @@ import java.util.List; import java.util.stream.Stream; import static ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity.ERROR; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.CODE_SYSTEM_VERSION; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.DISPLAY; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.ERROR_MESSAGE; -import static org.hl7.fhir.common.hapi.validation.IValidationProviders.VALUE_SET_URL; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_VALIDATE_CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.DISPLAY; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.ERROR_MESSAGE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.VALUE_SET_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -31,21 +35,35 @@ import static org.junit.jupiter.api.Assertions.fail; public interface IValidateCodeTest { - IValidationProviders.IMyCodeSystemProvider getCodeSystemProvider(); - IValidationProviders.IMyValueSetProvider getValueSetProvider(); + IValidationProviders.IMyValidationProvider getCodeSystemProvider(); + IValidationProviders.IMyValidationProvider getValueSetProvider(); IValidationSupport getService(); IBaseParameters createParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource); String getCodeSystemError(); String getValueSetError(); IBaseOperationOutcome getCodeSystemInvalidCodeOutcome(); IBaseOperationOutcome getValueSetInvalidCodeOutcome(); + IBaseOperationOutcome getValueSetCustomDetailCodeOutcome(); + + default IBaseOperationOutcome getCodeSystemInvalidCodeOutcome(Class theResourceClass) { + return getOutcome(theResourceClass, "/terminology/OperationOutcome-CodeSystem-invalid-code.json"); + } + default IBaseOperationOutcome getValueSetInvalidCodeOutcome(Class theResourceClass) { + return getOutcome(theResourceClass, "/terminology/OperationOutcome-ValueSet-invalid-code.json"); + } + default IBaseOperationOutcome getValueSetCustomDetailCodeOutcome(Class theResourceClass) { + return getOutcome(theResourceClass, "/terminology/OperationOutcome-ValueSet-custom-issue-detail.json"); + } + default IBaseOperationOutcome getOutcome(Class theResourceClass, String theFile) { + return ClasspathUtil.loadResource(getService().getFhirContext(), theResourceClass, theFile); + } default void createCodeSystemReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { - getCodeSystemProvider().setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource)); + getCodeSystemProvider().addTerminologyResponse(OPERATION_VALIDATE_CODE, CODE_SYSTEM, CODE, createParameters(theResult, theDisplay, theMessage, theIssuesResource)); } default void createValueSetReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { - getValueSetProvider().setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource)); + getValueSetProvider().addTerminologyResponse(OPERATION_VALIDATE_CODE, VALUE_SET_URL, CODE, createParameters(theResult, theDisplay, theMessage, theIssuesResource)); } @Test @@ -91,8 +109,8 @@ public interface IValidateCodeTest { String theValidationMessage, String theCodeSystem, String theValueSetUrl) { - getCodeSystemProvider().setException(theException); - getValueSetProvider().setException(theException); + getCodeSystemProvider().addException(OPERATION_VALIDATE_CODE, theCodeSystem, CODE, theException); + getValueSetProvider().addException(OPERATION_VALIDATE_CODE, theValueSetUrl, CODE, theException); CodeValidationResult outcome = getService().validateCode(null, null, theCodeSystem, CODE, DISPLAY, theValueSetUrl); verifyErrorResultFromException(outcome, theValidationMessage, theServerMessage); @@ -105,7 +123,7 @@ public interface IValidateCodeTest { for (String message : theMessages) { assertTrue(outcome.getMessage().contains(message)); } - assertFalse(outcome.getCodeValidationIssues().isEmpty()); + assertFalse(outcome.getIssues().isEmpty()); } @Test @@ -130,11 +148,7 @@ public interface IValidateCodeTest { assertEquals(DISPLAY, outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - assertTrue(outcome.getCodeValidationIssues().isEmpty()); - - assertEquals(CODE, getValueSetProvider().getCode()); - assertEquals(DISPLAY, getValueSetProvider().getDisplay()); - assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet()); + assertTrue(outcome.getIssues().isEmpty()); } @Test @@ -147,9 +161,7 @@ public interface IValidateCodeTest { assertEquals(DISPLAY, outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - assertTrue(outcome.getCodeValidationIssues().isEmpty()); - - assertEquals(CODE, getCodeSystemProvider().getCode()); + assertTrue(outcome.getIssues().isEmpty()); } @Test @@ -165,10 +177,7 @@ public interface IValidateCodeTest { assertNull(outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - assertTrue(outcome.getCodeValidationIssues().isEmpty()); - - assertEquals(CODE, getCodeSystemProvider().getCode()); - assertEquals(CODE_SYSTEM, getCodeSystemProvider().getSystem()); + assertTrue(outcome.getIssues().isEmpty()); } @Test @@ -184,15 +193,11 @@ public interface IValidateCodeTest { assertEquals(DISPLAY, outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - assertTrue(outcome.getCodeValidationIssues().isEmpty()); - - assertEquals(CODE, getCodeSystemProvider().getCode()); - assertEquals(DISPLAY, getCodeSystemProvider().getDisplay()); - assertEquals(CODE_SYSTEM, getCodeSystemProvider().getSystem()); + assertTrue(outcome.getIssues().isEmpty()); } @Test - default void validateCode_withCodeSystemError_returnsCorrectly() { + default void validateCode_withCodeSystemErrorWithDiagnosticsWithIssues_returnsCorrectly() { IBaseOperationOutcome invalidCodeOutcome = getCodeSystemInvalidCodeOutcome(); createCodeSystemReturnParameters(false, null, ERROR_MESSAGE, invalidCodeOutcome); @@ -204,12 +209,12 @@ public interface IValidateCodeTest { // assertEquals(CODE, outcome.getCode()); assertEquals(ERROR, outcome.getSeverity()); assertEquals(getCodeSystemError(), outcome.getMessage()); - assertFalse(outcome.getCodeValidationIssues().isEmpty()); + assertFalse(outcome.getIssues().isEmpty()); verifyIssues(invalidCodeOutcome, outcome); } @Test - default void validateCode_withCodeSystemErrorAndIssues_returnsCorrectly() { + default void validateCode_withCodeSystemErrorWithDiagnosticsWithoutIssues_returnsCorrectly() { createCodeSystemReturnParameters(false, null, ERROR_MESSAGE, null); CodeValidationResult outcome = getService() @@ -223,10 +228,32 @@ public interface IValidateCodeTest { assertNull(outcome.getDisplay()); assertEquals(ERROR, outcome.getSeverity()); assertEquals(expectedError, outcome.getMessage()); - assertFalse(outcome.getCodeValidationIssues().isEmpty()); - assertEquals(1, outcome.getCodeValidationIssues().size()); - assertEquals(expectedError, outcome.getCodeValidationIssues().get(0).getMessage()); - assertEquals(ERROR, outcome.getCodeValidationIssues().get(0).getSeverity()); + assertFalse(outcome.getIssues().isEmpty()); + assertEquals(1, outcome.getIssues().size()); + assertEquals(expectedError, outcome.getIssues().get(0).getDiagnostics()); + assertEquals(ERROR, outcome.getIssues().get(0).getSeverity()); + } + + @Test + default void validateCode_withCodeSystemErrorWithoutDiagnosticsWithIssues_returnsCorrectly() { + IBaseOperationOutcome invalidCodeOutcome = getCodeSystemInvalidCodeOutcome(); + createCodeSystemReturnParameters(false, null, null, invalidCodeOutcome); + + CodeValidationResult outcome = getService() + .validateCode(null, null, CODE_SYSTEM, CODE, null, null); + + String expectedError = getCodeSystemError(); + assertNotNull(outcome); + assertEquals(CODE_SYSTEM, outcome.getCodeSystemName()); + assertEquals(CODE_SYSTEM_VERSION, outcome.getCodeSystemVersion()); + // assertEquals(CODE, outcome.getCode()); + assertNull(outcome.getDisplay()); + assertEquals(ERROR, outcome.getSeverity()); + assertNull(outcome.getMessage()); + assertFalse(outcome.getIssues().isEmpty()); + assertEquals(1, outcome.getIssues().size()); + assertNull(outcome.getIssues().get(0).getDiagnostics()); + assertEquals(ERROR, outcome.getIssues().get(0).getSeverity()); } @Test @@ -242,10 +269,7 @@ public interface IValidateCodeTest { assertNull(outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - assertTrue(outcome.getCodeValidationIssues().isEmpty()); - - assertEquals(CODE, getValueSetProvider().getCode()); - assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet()); + assertTrue(outcome.getIssues().isEmpty()); } @Test @@ -261,11 +285,7 @@ public interface IValidateCodeTest { assertEquals(DISPLAY, outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - assertTrue(outcome.getCodeValidationIssues().isEmpty()); - - assertEquals(CODE, getValueSetProvider().getCode()); - assertEquals(DISPLAY, getValueSetProvider().getDisplay()); - assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet()); + assertTrue(outcome.getIssues().isEmpty()); } @Test @@ -283,13 +303,9 @@ public interface IValidateCodeTest { assertEquals(DISPLAY, outcome.getDisplay()); assertEquals(ERROR, outcome.getSeverity()); assertEquals(expectedError, outcome.getMessage()); - assertEquals(1, outcome.getCodeValidationIssues().size()); - assertEquals(expectedError, outcome.getCodeValidationIssues().get(0).getMessage()); - assertEquals(ERROR, outcome.getCodeValidationIssues().get(0).getSeverity()); - - assertEquals(CODE, getValueSetProvider().getCode()); - assertEquals(DISPLAY, getValueSetProvider().getDisplay()); - assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet()); + assertEquals(1, outcome.getIssues().size()); + assertEquals(expectedError, outcome.getIssues().get(0).getDiagnostics()); + assertEquals(ERROR, outcome.getIssues().get(0).getSeverity()); } @Test @@ -306,24 +322,28 @@ public interface IValidateCodeTest { assertEquals(DISPLAY, outcome.getDisplay()); assertEquals(ERROR, outcome.getSeverity()); assertEquals(getValueSetError(), outcome.getMessage()); - assertFalse(outcome.getCodeValidationIssues().isEmpty()); + assertFalse(outcome.getIssues().isEmpty()); verifyIssues(invalidCodeOutcome, outcome); - - assertEquals(CODE, getValueSetProvider().getCode()); - assertEquals(DISPLAY, getValueSetProvider().getDisplay()); - assertEquals(VALUE_SET_URL, getValueSetProvider().getValueSet()); } default void verifyIssues(IBaseOperationOutcome theOperationOutcome, CodeValidationResult theResult) { List issues = getCodeValidationIssues(theOperationOutcome); - assertEquals(issues.size(), theResult.getCodeValidationIssues().size()); + assertEquals(issues.size(), theResult.getIssues().size()); for (int i = 0; i < issues.size(); i++) { IValidationSupport.CodeValidationIssue expectedIssue = issues.get(i); - IValidationSupport.CodeValidationIssue actualIssue = theResult.getCodeValidationIssues().get(i); - assertEquals(expectedIssue.getCode(), actualIssue.getCode()); + IValidationSupport.CodeValidationIssue actualIssue = theResult.getIssues().get(i); + assertEquals(expectedIssue.getType().getCode(), actualIssue.getType().getCode()); assertEquals(expectedIssue.getSeverity(), actualIssue.getSeverity()); - assertEquals(expectedIssue.getCoding(), actualIssue.getCoding()); - assertEquals(expectedIssue.getMessage(), actualIssue.getMessage()); + assertEquals(expectedIssue.getDetails().getText(), actualIssue.getDetails().getText()); + assertEquals(expectedIssue.getDetails().getCodings().size(), actualIssue.getDetails().getCodings().size()); + for (int index = 0; index < expectedIssue.getDetails().getCodings().size(); index++) { + IValidationSupport.CodeValidationIssueCoding expectedCoding = expectedIssue.getDetails().getCodings().get(index); + IValidationSupport.CodeValidationIssueCoding actualCoding = actualIssue.getDetails().getCodings().get(index); + assertEquals(expectedCoding.getSystem(), actualCoding.getSystem()); + assertEquals(expectedCoding.getCode(), actualCoding.getCode()); + } + assertEquals(expectedIssue.getDetails().getText(), actualIssue.getDetails().getText()); + assertEquals(expectedIssue.getDiagnostics(), actualIssue.getDiagnostics()); } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java deleted file mode 100644 index 1537f8e5c00..00000000000 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/IValidationProviders.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.hl7.fhir.common.hapi.validation; - -import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.rest.server.IResourceProvider; -import org.hl7.fhir.instance.model.api.IBaseParameters; - -public interface IValidationProviders { - String CODE_SYSTEM = "http://code.system/url"; - String CODE_SYSTEM_VERSION = "1.0.0"; - String CODE_SYSTEM_NAME = "Test Code System"; - String CODE = "CODE"; - String VALUE_SET_URL = "http://value.set/url"; - String DISPLAY = "Explanation for code TestCode."; - String LANGUAGE = "en"; - String ERROR_MESSAGE = "This is an error message"; - - interface IMyCodeSystemProvider extends IResourceProvider { - String getCode(); - String getSystem(); - String getDisplay(); - void setException(Exception theException); - void setReturnParams(IBaseParameters theParameters); - } - - interface IMyLookupCodeProvider extends IResourceProvider { - String getCode(); - String getSystem(); - void setLookupCodeResult(IValidationSupport.LookupCodeResult theLookupCodeResult); - } - - interface IMyValueSetProvider extends IResourceProvider { - String getCode(); - String getSystem(); - String getDisplay(); - String getValueSet(); - void setException(Exception theException); - void setReturnParams(IBaseParameters theParameters); - } -} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java index 57aae8d96f9..cbc79fadc98 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java @@ -7,7 +7,6 @@ import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; - import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; @@ -16,17 +15,18 @@ import org.hl7.fhir.utilities.validation.ValidationOptions; import org.junit.jupiter.api.Test; import org.mockito.quality.Strictness; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; -import java.util.List; - public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestWithInlineMocks { final byte[] EXPECTED_BINARY_CONTENT_1 = "dummyBinaryContent1".getBytes(); @@ -80,7 +80,7 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW } @Test - public void validateCode_normally_resolvesCodeSystemFromValueSet() { + public void validateCode_codeInValueSet_resolvesCodeSystemFromValueSet() { // setup IValidationSupport validationSupport = mockValidationSupport(); ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport); @@ -90,8 +90,7 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW ValueSet valueSet = new ValueSet(); valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(validationSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(valueSet); - when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(new IValidationSupport.CodeValidationResult()); + when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(mock(IValidationSupport.CodeValidationResult.class)); // execute wrapper.validateCode(new ValidationOptions(), "code0", valueSet); @@ -101,6 +100,26 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW verify(validationSupport, times(1)).validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), any()); } + @Test + public void validateCode_codeNotInValueSet_doesNotResolveSystem() { + // setup + IValidationSupport validationSupport = mockValidationSupport(); + ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport); + VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(FhirContext.forR5Cached()); + VersionSpecificWorkerContextWrapper wrapper = new VersionSpecificWorkerContextWrapper(mockContext, versionCanonicalizer); + + ValueSet valueSet = new ValueSet(); + valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); + valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); + + // execute + wrapper.validateCode(new ValidationOptions(), "code1", valueSet); + + // verify + verify(validationSupport, times(1)).validateCodeInValueSet(any(), any(), eq(null), eq("code1"), any(), any()); + verify(validationSupport, never()).validateCode(any(), any(), any(), any(), any(), any()); + } + @Test public void isPrimitive_primitive() { // setup diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java index da73c0be800..5a53ba0ac3d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu2/hapi/validation/FhirInstanceValidatorDstu2Test.java @@ -4,7 +4,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; import ca.uhn.fhir.model.dstu2.composite.PeriodDt; import ca.uhn.fhir.model.dstu2.resource.Parameters; @@ -28,10 +27,7 @@ import org.hl7.fhir.dstu2.model.Observation.ObservationStatus; import org.hl7.fhir.dstu2.model.QuestionnaireResponse; import org.hl7.fhir.dstu2.model.QuestionnaireResponse.QuestionnaireResponseStatus; import org.hl7.fhir.dstu2.model.StringType; -import org.hl7.fhir.dstu3.model.CodeSystem; -import org.hl7.fhir.utilities.validation.ValidationMessage; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -41,9 +37,7 @@ import org.mockito.stubbing.Answer; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -100,7 +94,7 @@ public class FhirInstanceValidatorDstu2Test extends BaseValidationTestWithInline if (myValidConcepts.contains(system + "___" + code)) { retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { - return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code"); + return new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage("Unknown code"); } else { retVal = null; } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index ad6276f2761..cfbeca68626 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -58,7 +58,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.hl7.fhir.utilities.validation.ValidationMessage; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -229,10 +228,10 @@ public class FhirInstanceValidatorDstu3Test extends BaseValidationTestWithInline retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message).setCodeValidationIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message).setIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); } else if (myValidSystemsNotReturningIssues.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message); } else if (myCodeSystems.containsKey(system)) { CodeSystem cs = myCodeSystems.get(system); Optional found = cs.getConcept().stream().filter(t -> t.getCode().equals(code)).findFirst(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java deleted file mode 100644 index 0c639e310ee..00000000000 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/IValidateCodeProvidersDstu3.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.hl7.fhir.dstu3.hapi.validation; - -import ca.uhn.fhir.jpa.model.util.JpaConstants; -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 jakarta.servlet.http.HttpServletRequest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; -import org.hl7.fhir.dstu3.model.BooleanType; -import org.hl7.fhir.dstu3.model.CodeSystem; -import org.hl7.fhir.dstu3.model.CodeType; -import org.hl7.fhir.dstu3.model.Coding; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.Parameters; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.UriType; -import org.hl7.fhir.dstu3.model.ValueSet; -import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; - -import java.util.List; - -public interface IValidateCodeProvidersDstu3 { - @SuppressWarnings("unused") - class MyCodeSystemProviderDstu3 implements IValidationProviders.IMyCodeSystemProvider { - private UriType mySystemUrl; - private CodeType myCode; - private StringType myDisplay; - private Exception myException; - private Parameters myReturnParams; - - @Operation(name = "validate-code", idempotent = true, returnParameters = { - @OperationParam(name = "result", type = org.hl7.fhir.dstu3.model.BooleanType.class, min = 1), - @OperationParam(name = "message", type = org.hl7.fhir.dstu3.model.StringType.class), - @OperationParam(name = "display", type = org.hl7.fhir.dstu3.model.StringType.class) - }) - public org.hl7.fhir.dstu3.model.Parameters validateCode( - HttpServletRequest theServletRequest, - @IdParam(optional = true) org.hl7.fhir.dstu3.model.IdType theId, - @OperationParam(name = "url", min = 0, max = 1) org.hl7.fhir.dstu3.model.UriType theCodeSystemUrl, - @OperationParam(name = "code", min = 0, max = 1) org.hl7.fhir.dstu3.model.CodeType theCode, - @OperationParam(name = "display", min = 0, max = 1) org.hl7.fhir.dstu3.model.StringType theDisplay - ) throws Exception { - mySystemUrl = theCodeSystemUrl; - myCode = theCode; - myDisplay = theDisplay; - if (myException != null) { - throw myException; - } - return myReturnParams; - } - - @Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= { - @OperationParam(name = "name", type = org.hl7.fhir.dstu3.model.StringType.class, min = 1), - @OperationParam(name = "version", type = org.hl7.fhir.dstu3.model.StringType.class), - @OperationParam(name = "display", type = org.hl7.fhir.dstu3.model.StringType.class, min = 1), - @OperationParam(name = "abstract", type = org.hl7.fhir.dstu3.model.BooleanType.class, min = 1), - @OperationParam(name = "property", type = org.hl7.fhir.dstu3.model.StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED) - }) - public IBaseParameters lookup( - HttpServletRequest theServletRequest, - @OperationParam(name = "code", max = 1) org.hl7.fhir.dstu3.model.CodeType theCode, - @OperationParam(name = "system",max = 1) org.hl7.fhir.dstu3.model.UriType theSystem, - @OperationParam(name = "coding", max = 1) Coding theCoding, - @OperationParam(name = "version", max = 1) org.hl7.fhir.dstu3.model.StringType theVersion, - @OperationParam(name = "displayLanguage", max = 1) org.hl7.fhir.dstu3.model.CodeType theDisplayLanguage, - @OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, - RequestDetails theRequestDetails - ) { - myCode = theCode; - return myReturnParams; - } - - @Override - public Class getResourceType() { - return CodeSystem.class; - } - - public void setException(Exception theException) { - myException = theException; - } - @Override - public void setReturnParams(IBaseParameters theParameters) { - myReturnParams = (Parameters) theParameters; - } - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } - public String getDisplay() { - return myDisplay != null ? myDisplay.getValue() : null; - } - } - - @SuppressWarnings("unused") - class MyValueSetProviderDstu3 implements IValidationProviders.IMyValueSetProvider { - private Exception myException; - private Parameters myReturnParams; - private UriType mySystemUrl; - private UriType myValueSetUrl; - private CodeType myCode; - private StringType myDisplay; - - @Operation(name = "validate-code", idempotent = true, returnParameters = { - @OperationParam(name = "result", type = BooleanType.class, min = 1), - @OperationParam(name = "message", type = org.hl7.fhir.dstu3.model.StringType.class), - @OperationParam(name = "display", type = org.hl7.fhir.dstu3.model.StringType.class) - }) - public Parameters validateCode( - HttpServletRequest theServletRequest, - @IdParam(optional = true) IdType theId, - @OperationParam(name = "url", min = 0, max = 1) org.hl7.fhir.dstu3.model.UriType theValueSetUrl, - @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, - @OperationParam(name = "system", min = 0, max = 1) UriType theSystem, - @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay, - @OperationParam(name = "valueSet") org.hl7.fhir.dstu3.model.ValueSet theValueSet - ) throws Exception { - mySystemUrl = theSystem; - myValueSetUrl = theValueSetUrl; - myCode = theCode; - myDisplay = theDisplay; - if (myException != null) { - throw myException; - } - return myReturnParams; - } - @Override - public Class getResourceType() { - return ValueSet.class; - } - public void setException(Exception theException) { - myException = theException; - } - @Override - public void setReturnParams(IBaseParameters theParameters) { - myReturnParams = (Parameters) theParameters; - } - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } - @Override - public String getValueSet() { - return myValueSetUrl != null ? myValueSetUrl.getValueAsString() : null; - } - public String getDisplay() { - return myDisplay != null ? myDisplay.getValue() : null; - } - } -} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 7804b9df10f..57591b31e76 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -41,7 +41,6 @@ import org.hl7.fhir.dstu3.model.Type; import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.utilities.validation.ValidationMessage; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -56,6 +55,8 @@ import java.util.Date; import java.util.List; import java.util.stream.Collectors; +import static ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity.ERROR; +import static ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity.WARNING; import static org.assertj.core.api.Assertions.assertThat; import static org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType.BOOLEAN; import static org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType.CHOICE; @@ -224,7 +225,7 @@ public class QuestionnaireResponseValidatorDstu3Test { when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(ValueSet.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(ValueSet.class))) - .thenReturn(new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code")); + .thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(ERROR).setMessage("Unknown code")); CodeSystem codeSystem = new CodeSystem(); codeSystem.setContent(CodeSystemContentMode.COMPLETE); @@ -246,7 +247,7 @@ public class QuestionnaireResponseValidatorDstu3Test { when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class))) - .thenReturn(new IValidationSupport.CodeValidationResult().setSeverityCode("warning").setMessage("Unknown code: http://codesystems.com/system / code1")); + .thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(WARNING).setMessage("Unknown code: http://codesystems.com/system / code1")); QuestionnaireResponse qa; @@ -1034,7 +1035,7 @@ public class QuestionnaireResponseValidatorDstu3Test { when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class))) - .thenReturn(new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage("Unknown code")); + .thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(ERROR).setMessage("Unknown code")); CodeSystem codeSystem = new CodeSystem(); codeSystem.setContent(CodeSystemContentMode.COMPLETE); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java index 6e98c4b31a9..56bc892c3d4 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeDstu3Test.java @@ -12,9 +12,9 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyLookupCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.dstu3.model.BooleanType; import org.hl7.fhir.dstu3.model.CodeSystem; @@ -164,8 +164,6 @@ public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyL @SuppressWarnings("unused") static class MyLookupCodeProviderDstu3 implements IValidationProviders.IMyLookupCodeProvider { - private UriType mySystemUrl; - private CodeType myCode; private LookupCodeResult myLookupCodeResult; @Override @@ -190,8 +188,6 @@ public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyL @OperationParam(name= " property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, RequestDetails theRequestDetails ) { - myCode = theCode; - mySystemUrl = theSystem; if (theSystem == null) { throw new InvalidRequestException(MessageFormat.format(MESSAGE_RESPONSE_INVALID, theCode)); } @@ -205,15 +201,5 @@ public class RemoteTerminologyLookupCodeDstu3Test implements IRemoteTerminologyL public Class getResourceType() { return CodeSystem.class; } - - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java index 48a99f260d0..4817542ef4d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyLookupCodeWithResponseFileDstu3Test.java @@ -5,13 +5,10 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersDstu3; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; -import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,13 +16,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_LOOKUP; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class RemoteTerminologyLookupCodeWithResponseFileDstu3Test { private static final FhirContext ourCtx = FhirContext.forDstu3Cached(); - private IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3 myCodeSystemProvider; + private IValidationProvidersDstu3.MyCodeSystemProviderDstu3 myCodeSystemProvider; @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); @@ -36,7 +35,7 @@ public class RemoteTerminologyLookupCodeWithResponseFileDstu3Test { String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3(); + myCodeSystemProvider = new IValidationProvidersDstu3.MyCodeSystemProviderDstu3(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider); } @@ -47,13 +46,10 @@ public class RemoteTerminologyLookupCodeWithResponseFileDstu3Test { } @Test void lookupCode_withParametersOutput_convertsCorrectly() { - String paramsAsString = ClasspathUtil.loadResource("/terminology/CodeSystem-lookup-output-with-subproperties.json"); - IBaseResource baseResource = ourCtx.newJsonParser().parseResource(paramsAsString); - assertTrue(baseResource instanceof Parameters); - Parameters resultParameters = (Parameters) baseResource; - myCodeSystemProvider.setReturnParams(resultParameters); + String outputFile ="/terminology/CodeSystem-lookup-output-with-subproperties.json"; + IBaseParameters resultParameters = myCodeSystemProvider.addTerminologyResponse(OPERATION_LOOKUP, CODE_SYSTEM, CODE, ourCtx, outputFile); - LookupCodeRequest request = new LookupCodeRequest(IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, null, List.of("interfaces")); + LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, null, List.of("interfaces")); // test IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, request); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java index af4f39f0926..2f573ad3e5f 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/RemoteTerminologyValidateCodeDstu3Test.java @@ -4,16 +4,18 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersDstu3; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyValidateCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeSystem; import org.hl7.fhir.dstu3.model.OperationOutcome; import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.UriType; +import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.jupiter.api.AfterEach; @@ -22,6 +24,11 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.ERROR_MESSAGE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.VALUE_SET_URL; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET; @@ -38,8 +45,8 @@ public class RemoteTerminologyValidateCodeDstu3Test implements IRemoteTerminolog private static final FhirContext ourCtx = FhirContext.forDstu3Cached(); @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); - private IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3 myCodeSystemProvider; - private IValidateCodeProvidersDstu3.MyValueSetProviderDstu3 myValueSetProvider; + private IValidationProviders.MyValidationProvider myCodeSystemProvider; + private IValidationProviders.MyValidationProvider myValueSetProvider; private RemoteTerminologyServiceValidationSupport mySvc; private String myCodeSystemError, myValueSetError; @@ -48,14 +55,14 @@ public class RemoteTerminologyValidateCodeDstu3Test implements IRemoteTerminolog String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); myCodeSystemError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, CODE_SYSTEM, CODE, baseUrl, ERROR_MESSAGE); myValueSetError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, IValidationProviders.VALUE_SET_URL, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, CODE_SYSTEM, CODE, VALUE_SET_URL, baseUrl, ERROR_MESSAGE); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3(); - myValueSetProvider = new IValidateCodeProvidersDstu3.MyValueSetProviderDstu3(); + myCodeSystemProvider = new IValidationProvidersDstu3.MyCodeSystemProviderDstu3(); + myValueSetProvider = new IValidationProvidersDstu3.MyValueSetProviderDstu3(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider, myValueSetProvider); } @@ -82,45 +89,40 @@ public class RemoteTerminologyValidateCodeDstu3Test implements IRemoteTerminolog } @Override - public IValidateCodeProvidersDstu3.MyCodeSystemProviderDstu3 getCodeSystemProvider() { + public IValidationProviders.IMyValidationProvider getCodeSystemProvider() { return myCodeSystemProvider; } @Override - public IValidateCodeProvidersDstu3.MyValueSetProviderDstu3 getValueSetProvider() { + public IValidationProviders.IMyValidationProvider getValueSetProvider() { return myValueSetProvider; } @Override public IBaseOperationOutcome getCodeSystemInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-CodeSystem-invalid-code.json"); + return getCodeSystemInvalidCodeOutcome(OperationOutcome.class); } @Override public IBaseOperationOutcome getValueSetInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-ValueSet-invalid-code.json"); + return getValueSetInvalidCodeOutcome(OperationOutcome.class); + } + + @Override + public IBaseOperationOutcome getValueSetCustomDetailCodeOutcome() { + return getValueSetCustomDetailCodeOutcome(OperationOutcome.class); } @Override public Parameters createParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { Parameters parameters = new Parameters(); parameters.addParameter().setName("result").setValue(new BooleanType(theResult)); - parameters.addParameter().setName("code").setValue(new StringType(IValidationProviders.CODE)); - parameters.addParameter().setName("system").setValue(new UriType(IValidationProviders.CODE_SYSTEM)); - parameters.addParameter().setName("version").setValue(new StringType(IValidationProviders.CODE_SYSTEM_VERSION)); + parameters.addParameter().setName("code").setValue(new StringType(CODE)); + parameters.addParameter().setName("system").setValue(new UriType(CODE_SYSTEM)); + parameters.addParameter().setName("version").setValue(new StringType(CODE_SYSTEM_VERSION)); parameters.addParameter().setName("display").setValue(new StringType(theDisplay)); parameters.addParameter().setName("message").setValue(new StringType(theMessage)); parameters.addParameter().setName("issues").setResource((Resource) theIssuesResource); return parameters; } - - @Override - public void createCodeSystemReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { - myCodeSystemProvider.setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource)); - } - - @Override - public void createValueSetReturnParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { - myValueSetProvider.setReturnParams(createParameters(theResult, theDisplay, theMessage, theIssuesResource)); - } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java index 15bbc3a8bf3..1c2382b6aac 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/utils/FhirPathEngineR4Test.java @@ -54,6 +54,7 @@ public class FhirPathEngineR4Test extends BaseValidationTestWithInlineMocks { List value; + value = ourCtx.newFhirPath().evaluate(o, "Observation.specimen", Base.class); assertThat(value).hasSize(1); value = ourCtx.newFhirPath().evaluate(o, "Observation.specimen.resolve()", Base.class); @@ -65,6 +66,9 @@ public class FhirPathEngineR4Test extends BaseValidationTestWithInlineMocks { assertEquals("2011-01-01", ((DateTimeType) value.get(0)).getValueAsString()); } + + + @Test public void testComponentCode() { String path = "(Observation.component.value.ofType(FHIR.Quantity)) "; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index d03c3aa974d..fb46f7f8008 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -307,10 +307,10 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message).setCodeValidationIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message).setIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); } else if (myValidSystemsNotReturningIssues.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message); } else { retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java index d67966df6d4..382f621f547 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeR4Test.java @@ -11,9 +11,10 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; import jakarta.servlet.http.HttpServletRequest; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyLookupCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -52,7 +53,7 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); private final RemoteTerminologyServiceValidationSupport mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx); - private IValidateCodeProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; + private IValidationProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; private MyLookupCodeProviderR4 myLookupCodeProviderR4; @BeforeEach @@ -60,7 +61,7 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc.setBaseUrl(baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(true)); - myCodeSystemProvider = new IValidateCodeProvidersR4.MyCodeSystemProviderR4(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); myLookupCodeProviderR4 = new MyLookupCodeProviderR4(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider, myLookupCodeProviderR4); } @@ -166,8 +167,6 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook @SuppressWarnings("unused") static class MyLookupCodeProviderR4 implements IValidationProviders.IMyLookupCodeProvider { - private UriType mySystemUrl; - private CodeType myCode; private LookupCodeResult myLookupCodeResult; @Override @@ -192,8 +191,6 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook @OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List thePropertyNames, RequestDetails theRequestDetails ) { - myCode = theCode; - mySystemUrl = theSystem; if (theSystem == null) { throw new InvalidRequestException(MessageFormat.format(MESSAGE_RESPONSE_INVALID, theCode)); } @@ -206,15 +203,5 @@ public class RemoteTerminologyLookupCodeR4Test implements IRemoteTerminologyLook public Class getResourceType() { return CodeSystem.class; } - - @Override - public String getCode() { - return myCode != null ? myCode.getValueAsString() : null; - } - - @Override - public String getSystem() { - return mySystemUrl != null ? mySystemUrl.getValueAsString() : null; - } } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java index 37eba91d0ca..a0896bd8c4e 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyLookupCodeWithResponseFileR4Test.java @@ -5,12 +5,9 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -19,13 +16,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.util.List; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_LOOKUP; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class RemoteTerminologyLookupCodeWithResponseFileR4Test { private static final FhirContext ourCtx = FhirContext.forR4Cached(); - private IValidateCodeProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; + private IValidationProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); @@ -36,7 +35,7 @@ public class RemoteTerminologyLookupCodeWithResponseFileR4Test { String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersR4.MyCodeSystemProviderR4(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider); } @@ -48,13 +47,10 @@ public class RemoteTerminologyLookupCodeWithResponseFileR4Test { @Test void lookupCode_withParametersOutput_convertsCorrectly() { - String paramsAsString = ClasspathUtil.loadResource("/terminology/CodeSystem-lookup-output-with-subproperties.json"); - IBaseResource baseResource = ourCtx.newJsonParser().parseResource(paramsAsString); - assertTrue(baseResource instanceof Parameters); - Parameters resultParameters = (Parameters) baseResource; - myCodeSystemProvider.setReturnParams(resultParameters); + String outputFile ="/terminology/CodeSystem-lookup-output-with-subproperties.json"; + IBaseParameters resultParameters = myCodeSystemProvider.addTerminologyResponse(OPERATION_LOOKUP, CODE_SYSTEM, CODE, ourCtx, outputFile); - LookupCodeRequest request = new LookupCodeRequest(IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, null, List.of("interfaces")); + LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, null, List.of("interfaces")); // test IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, request); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java index 08f6c251869..ffd8045a8a5 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/RemoteTerminologyValidateCodeR4Test.java @@ -2,8 +2,8 @@ package org.hl7.fhir.r4.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; -import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.parser.IJsonLikeParser; import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IHttpRequest; @@ -13,11 +13,11 @@ import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; -import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.test.utilities.validation.IValidationProviders; +import ca.uhn.fhir.test.utilities.validation.IValidationProvidersR4; import ca.uhn.fhir.util.ParametersUtil; import com.google.common.collect.Lists; import org.hl7.fhir.common.hapi.validation.IRemoteTerminologyValidateCodeTest; -import org.hl7.fhir.common.hapi.validation.IValidationProviders; import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -39,6 +39,12 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.CODE_SYSTEM_VERSION; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.DISPLAY; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.ERROR_MESSAGE; +import static ca.uhn.fhir.test.utilities.validation.IValidationProviders.VALUE_SET_URL; import static org.assertj.core.api.Assertions.assertThat; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM; import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET; @@ -61,8 +67,8 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa private static final FhirContext ourCtx = FhirContext.forR4Cached(); @RegisterExtension public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx); - private IValidateCodeProvidersR4.MyCodeSystemProviderR4 myCodeSystemProvider; - private IValidateCodeProvidersR4.MyValueSetProviderR4 myValueSetProvider; + private IValidationProviders.IMyValidationProvider myCodeSystemProvider; + private IValidationProviders.IMyValidationProvider myValueSetProvider; private RemoteTerminologyServiceValidationSupport mySvc; private String myCodeSystemError, myValueSetError; @@ -71,14 +77,14 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort(); myCodeSystemError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, CODE_SYSTEM, CODE, baseUrl, ERROR_MESSAGE); myValueSetError = ourCtx.getLocalizer().getMessage( RemoteTerminologyServiceValidationSupport.class, - ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, IValidationProviders.VALUE_SET_URL, baseUrl, IValidationProviders.ERROR_MESSAGE); + ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET, CODE_SYSTEM, CODE, VALUE_SET_URL, baseUrl, ERROR_MESSAGE); mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, baseUrl); mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true)); - myCodeSystemProvider = new IValidateCodeProvidersR4.MyCodeSystemProviderR4(); - myValueSetProvider = new IValidateCodeProvidersR4.MyValueSetProviderR4(); + myCodeSystemProvider = new IValidationProvidersR4.MyCodeSystemProviderR4(); + myValueSetProvider = new IValidationProvidersR4.MyValueSetProviderR4(); ourRestfulServerExtension.getRestfulServer().registerProviders(myCodeSystemProvider, myValueSetProvider); } @@ -95,12 +101,12 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa } @Override - public IValidationProviders.IMyCodeSystemProvider getCodeSystemProvider() { + public IValidationProviders.IMyValidationProvider getCodeSystemProvider() { return myCodeSystemProvider; } @Override - public IValidationProviders.IMyValueSetProvider getValueSetProvider() { + public IValidationProviders.IMyValidationProvider getValueSetProvider() { return myValueSetProvider; } @@ -116,51 +122,40 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Override public IBaseOperationOutcome getCodeSystemInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-CodeSystem-invalid-code.json"); + return getCodeSystemInvalidCodeOutcome(OperationOutcome.class); } @Override public IBaseOperationOutcome getValueSetInvalidCodeOutcome() { - return ClasspathUtil.loadResource(getService().getFhirContext(), OperationOutcome.class, "/terminology/OperationOutcome-ValueSet-invalid-code.json"); + return getValueSetInvalidCodeOutcome(OperationOutcome.class); } @Override - public List getCodeValidationIssues(IBaseOperationOutcome theOperationOutcome) { - return ((OperationOutcome)theOperationOutcome).getIssue().stream() - .map(issueComponent -> new IValidationSupport.CodeValidationIssue( - issueComponent.getDetails().getText(), - IValidationSupport.IssueSeverity.ERROR, - /* assume issue type is OperationOutcome.IssueType#CODEINVALID as it is the only match */ - IValidationSupport.CodeValidationIssueCode.INVALID, - IValidationSupport.CodeValidationIssueCoding.INVALID_CODE)) - .toList(); + public IBaseOperationOutcome getValueSetCustomDetailCodeOutcome() { + return getValueSetCustomDetailCodeOutcome(OperationOutcome.class); } @Test void validateCodeInValueSet_success() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); - CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, new ConceptValidationOptions(), IValidationProviders.CODE_SYSTEM, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, new ConceptValidationOptions(), CODE_SYSTEM, CODE, DISPLAY, valueSet); assertNotNull(outcome); - assertEquals(IValidationProviders.CODE, outcome.getCode()); - assertEquals(IValidationProviders.DISPLAY, outcome.getDisplay()); + assertEquals(CODE, outcome.getCode()); + assertEquals(DISPLAY, outcome.getDisplay()); assertNull(outcome.getSeverity()); assertNull(outcome.getMessage()); - - assertEquals(IValidationProviders.CODE, myValueSetProvider.getCode()); - assertEquals(IValidationProviders.DISPLAY, myValueSetProvider.getDisplay()); - assertEquals(IValidationProviders.VALUE_SET_URL, myValueSetProvider.getValueSet()); } @Override public Parameters createParameters(Boolean theResult, String theDisplay, String theMessage, IBaseResource theIssuesResource) { Parameters parameters = new Parameters() - .addParameter("code", IValidationProviders.CODE) - .addParameter("system", IValidationProviders.CODE_SYSTEM) - .addParameter("version", IValidationProviders.CODE_SYSTEM_VERSION) + .addParameter("code", CODE) + .addParameter("system", CODE_SYSTEM) + .addParameter("version", CODE_SYSTEM_VERSION) .addParameter("display", theDisplay) .addParameter("message", theMessage); if (theResult != null) { @@ -181,16 +176,16 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Test void validateCodeInValueSet_uniqueComposeInclude() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( Collections.singletonList(new ValueSet.ConceptSetComponent().setSystem(systemUrl)) )); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); // validate service doesn't return error message (as when no code system is present) assertNotNull(outcome); @@ -211,16 +206,16 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @ParameterizedTest @MethodSource(value = "getRemoteTerminologyServerExceptions") void validateCodeInValueSet_systemNotPresent_returnsValidationResultWithError(Exception theException, String theServerMessage) { - myValueSetProvider.setException(theException); - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + getValueSetProvider().addException("$validate-code", VALUE_SET_URL, CODE, theException); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( Lists.newArrayList(new ValueSet.ConceptSetComponent(), new ValueSet.ConceptSetComponent()))); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); String unknownCodeForValueSetError = "Unknown code \"null#CODE\" for ValueSet with URL \"http://value.set/url\". The Remote Terminology server http://"; verifyErrorResultFromException(outcome, unknownCodeForValueSetError, theServerMessage); @@ -230,11 +225,11 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @ParameterizedTest @MethodSource(value = "getRemoteTerminologyServerExceptions") void validateCodeInValueSet_systemPresentCodeNotPresent_returnsValidationResultWithError(Exception theException, String theServerMessage) { - myValueSetProvider.setException(theException); - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + getValueSetProvider().addException(JpaConstants.OPERATION_VALIDATE_CODE, VALUE_SET_URL, CODE, theException); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; String systemUrl2 = "http://hl7.org/fhir/ValueSet/other-valueset"; valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( @@ -243,7 +238,7 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa new ValueSet.ConceptSetComponent().setSystem(systemUrl2)))); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); String unknownCodeForValueSetError = "Unknown code \"null#CODE\" for ValueSet with URL \"http://value.set/url\". The Remote Terminology server http://"; verifyErrorResultFromException(outcome, unknownCodeForValueSetError, theServerMessage); @@ -252,10 +247,10 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Test void validateCodeInValueSet_systemPresentCodePresentValidatesOKNoVersioned() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; String systemUrl2 = "http://hl7.org/fhir/ValueSet/other-valueset"; valueSet.setCompose(new ValueSet.ValueSetComposeComponent().setInclude( @@ -264,14 +259,14 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa new ValueSet.ConceptSetComponent().setSystem(systemUrl2).setConcept( Lists.newArrayList( new ValueSet.ConceptReferenceComponent().setCode("not-the-code"), - new ValueSet.ConceptReferenceComponent().setCode(IValidationProviders.CODE) ) + new ValueSet.ConceptReferenceComponent().setCode(CODE) ) )) )); TestClientInterceptor requestInterceptor = new TestClientInterceptor(); mySvc.addClientInterceptor(requestInterceptor); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); assertNotNull(outcome); assertEquals(systemUrl2, requestInterceptor.getCapturedSystemParameter()); @@ -280,10 +275,10 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa @Test void validateCodeInValueSet_systemPresentCodePresentValidatesOKVersioned() { - createValueSetReturnParameters(true, IValidationProviders.DISPLAY, null, null); + createValueSetReturnParameters(true, DISPLAY, null, null); ValueSet valueSet = new ValueSet(); - valueSet.setUrl(IValidationProviders.VALUE_SET_URL); + valueSet.setUrl(VALUE_SET_URL); String systemUrl = "http://hl7.org/fhir/ValueSet/administrative-gender"; String systemVersion = "3.0.2"; String systemUrl2 = "http://hl7.org/fhir/ValueSet/other-valueset"; @@ -294,14 +289,14 @@ public class RemoteTerminologyValidateCodeR4Test implements IRemoteTerminologyVa new ValueSet.ConceptSetComponent().setSystem(systemUrl2).setVersion(system2Version).setConcept( Lists.newArrayList( new ValueSet.ConceptReferenceComponent().setCode("not-the-code"), - new ValueSet.ConceptReferenceComponent().setCode(IValidationProviders.CODE) ) + new ValueSet.ConceptReferenceComponent().setCode(CODE) ) )) )); TestClientInterceptor requestInterceptor = new TestClientInterceptor(); mySvc.addClientInterceptor(requestInterceptor); CodeValidationResult outcome = mySvc.validateCodeInValueSet(null, - new ConceptValidationOptions().setInferSystem(true), null, IValidationProviders.CODE, IValidationProviders.DISPLAY, valueSet); + new ConceptValidationOptions().setInferSystem(true), null, CODE, DISPLAY, valueSet); assertNotNull(outcome); assertEquals(systemUrl2 + "|" + system2Version, requestInterceptor.getCapturedSystemParameter()); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java index ca55a41bf4c..d5035a8048e 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java @@ -31,6 +31,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4b.conformance.ProfileUtilities; import org.hl7.fhir.r4b.context.IWorkerContext; +import org.hl7.fhir.r4b.fhirpath.FHIRPathEngine; import org.hl7.fhir.r4b.hapi.ctx.HapiWorkerContext; import org.hl7.fhir.r4b.model.AllergyIntolerance; import org.hl7.fhir.r4b.model.Base; @@ -61,7 +62,6 @@ import org.hl7.fhir.r4b.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r4b.model.ValueSet; import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r4b.terminologies.ValueSetExpander; -import org.hl7.fhir.r4b.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; @@ -203,7 +203,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - return new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message).setCodeValidationIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); + return new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message).setIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(message, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); } else { retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java index f0ea48686e1..abcb0f94704 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java @@ -48,7 +48,6 @@ import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; -import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; @@ -200,10 +199,10 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc retVal = new IValidationSupport.CodeValidationResult().setCode(code); } else if (myValidSystems.contains(system)) { String theMessage = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(theMessage).setCodeValidationIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(theMessage, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(theMessage).setIssues(Collections.singletonList(new IValidationSupport.CodeValidationIssue(theMessage, IValidationSupport.IssueSeverity.ERROR, IValidationSupport.CodeValidationIssueCode.CODE_INVALID, IValidationSupport.CodeValidationIssueCoding.INVALID_CODE))); } else if (myValidSystemsNotReturningIssues.contains(system)) { final String message = "Unknown code (for '" + system + "#" + code + "')"; - retVal = new IValidationSupport.CodeValidationResult().setSeverityCode(ValidationMessage.IssueSeverity.ERROR.toCode()).setMessage(message); + retVal = new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage(message); } else { retVal = myDefaultValidationSupport.validateCode(new ValidationSupportContext(myDefaultValidationSupport), options, system, code, display, valueSetUrl); } diff --git a/hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json b/hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json new file mode 100644 index 00000000000..0823a430cf8 --- /dev/null +++ b/hapi-fhir-validation/src/test/resources/terminology/OperationOutcome-ValueSet-custom-issue-detail.json @@ -0,0 +1,22 @@ +{ + "resourceType": "OperationOutcome", + "issue": [ + { + "severity": "information", + "code": "processing", + "details": { + "coding": [ + { + "system": "http://example.com/custom-issue-type", + "code": "valueset-is-draft" + } + ] + }, + "diagnostics": "The ValueSet status is marked as draft.", + "location": [ + "Bundle", + "Line[1] Col[2]" + ] + } + ] +} \ No newline at end of file diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java index 3fd0a49aa5e..640bde8c994 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/Configuration.java @@ -4,7 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.tinder.parser.BaseStructureSpreadsheetParser; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.text.WordUtils; import java.io.File; import java.io.IOException; diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderGenericSingleFileMojo.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderGenericSingleFileMojo.java index e5a2816340f..08fc8f2d7bb 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderGenericSingleFileMojo.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderGenericSingleFileMojo.java @@ -7,7 +7,7 @@ import ca.uhn.fhir.tinder.TinderStructuresMojo.ValueSetFileDefinition; import ca.uhn.fhir.tinder.parser.BaseStructureParser; import ca.uhn.fhir.tinder.parser.DatatypeGeneratorUsingSpreadsheet; import ca.uhn.fhir.tinder.parser.TargetType; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.text.WordUtils; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderJpaRestServerMojo.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderJpaRestServerMojo.java index 4503b47061f..4f2fe43f8f6 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderJpaRestServerMojo.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/TinderJpaRestServerMojo.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.tinder.parser.BaseStructureSpreadsheetParser; import ca.uhn.fhir.tinder.parser.ResourceGeneratorUsingModel; import ca.uhn.fhir.util.ClasspathUtil; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.text.WordUtils; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ant/TinderGeneratorTask.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ant/TinderGeneratorTask.java index 9819edcc795..c8304187c85 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ant/TinderGeneratorTask.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/ant/TinderGeneratorTask.java @@ -33,7 +33,7 @@ import ca.uhn.fhir.tinder.parser.BaseStructureParser; import ca.uhn.fhir.tinder.parser.BaseStructureSpreadsheetParser; import ca.uhn.fhir.tinder.parser.DatatypeGeneratorUsingSpreadsheet; import ca.uhn.fhir.tinder.parser.TargetType; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.text.WordUtils; import org.apache.maven.plugin.MojoFailureException; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Task; diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/BaseElement.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/BaseElement.java index 8de25b93376..69c30538f57 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/BaseElement.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/BaseElement.java @@ -8,8 +8,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.apache.commons.lang.StringUtils.defaultString; -import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseElement { diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/SearchParameter.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/SearchParameter.java index 7ac33c81693..fe8233c02b9 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/SearchParameter.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/model/SearchParameter.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.tinder.model; -import org.apache.commons.lang.WordUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.WordUtils; import java.util.ArrayList; import java.util.Collections; diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java index 68b28b0b96b..9e465ad18cf 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/BaseStructureParser.java @@ -25,9 +25,9 @@ import ca.uhn.fhir.tinder.model.SimpleSetter.Parameter; import com.google.common.base.Charsets; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.WordUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.WordUtils; import org.apache.maven.plugin.MojoFailureException; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; @@ -56,8 +56,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; -import static org.apache.commons.lang.StringUtils.defaultString; -import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseStructureParser { diff --git a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/ResourceGeneratorUsingModel.java b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/ResourceGeneratorUsingModel.java index 734a35822ab..0f90505d77b 100644 --- a/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/ResourceGeneratorUsingModel.java +++ b/hapi-tinder-plugin/src/main/java/ca/uhn/fhir/tinder/parser/ResourceGeneratorUsingModel.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.tinder.model.Resource; import ca.uhn.fhir.tinder.model.SearchParameter; -import org.apache.commons.lang.WordUtils; +import org.apache.commons.text.WordUtils; import org.apache.maven.plugin.MojoFailureException; import java.io.File; diff --git a/pom.xml b/pom.xml index 31b2d0c455f..9b19056b43d 100644 --- a/pom.xml +++ b/pom.xml @@ -999,8 +999,8 @@ 2.12.0 1.10.0 - 2.11.0 - 3.14.0 + 2.17.0 + 3.17.0 1.2 2.23.0 5.8.0 @@ -1013,15 +1013,15 @@ 4.0.4 4.9.0 3.0.3 - 12.0.9 + 12.0.14 3.0.2 5.10.1 0.64.8 9.4.0 - 6.4.1.Final + 6.4.10.Final 1.4.14 - 7.0.0.Final + 7.0.1.Final 9.8.0 2.2 @@ -1046,8 +1046,8 @@ 2.2.22 2.0.13 2.19.0 - 6.1.8 - 2023.1.6 + 6.1.14 + 2024.0.5 4.3.10 3.2.6 2.0.6 @@ -1063,7 +1063,7 @@ 1.0.8 - 3.13.0 + 3.13.1 5.4.1 @@ -1673,7 +1673,7 @@ org.apache.velocity velocity-engine-core - 2.3 + 2.4.1 org.awaitility From 77fa7f78191aa83b48a12bd29a7ff26fc574bb84 Mon Sep 17 00:00:00 2001 From: JasonRoberts-smile <85363818+JasonRoberts-smile@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:14:18 -0500 Subject: [PATCH 2/6] adapt template for reuse in CDA (#6500) * adapt template for reuse in CDA * use coerced onset date time value * fix broken test --- .../ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html | 6 +++--- .../uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html index 728afbbd7a0..55f3d14dfc9 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html @@ -23,8 +23,8 @@ Comments: AllergyIntolerance.note[x].text (separated by
    ) - - + + Allergen Status Category @@ -33,7 +33,7 @@ Comments: AllergyIntolerance.note[x].text (separated by
    ) Comments - Onset + Onset Onset diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java index 30791aadc8d..96464a82b4d 100644 --- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java @@ -220,7 +220,7 @@ public class IpsGeneratorSvcImplTest { HtmlTable table = (HtmlTable) tables.get(0); int onsetIndex = 6; assertEquals("Onset", table.getHeader().getRows().get(0).getCell(onsetIndex).asNormalizedText()); - assertEquals(new DateTimeType("2020-02-03T11:22:33Z").getValue().toString(), table.getBodies().get(0).getRows().get(0).getCell(onsetIndex).asNormalizedText()); + assertEquals(new DateTimeType("2020-02-03T11:22:33Z").getValueAsString(), table.getBodies().get(0).getRows().get(0).getCell(onsetIndex).asNormalizedText()); assertEquals("Some Onset", table.getBodies().get(0).getRows().get(1).getCell(onsetIndex).asNormalizedText()); assertEquals("", table.getBodies().get(0).getRows().get(2).getCell(onsetIndex).asNormalizedText()); } From 3b8569127e6ccb2a18a1ec769c6df45a216b8209 Mon Sep 17 00:00:00 2001 From: Michael Buckley Date: Tue, 26 Nov 2024 13:46:05 -0500 Subject: [PATCH 3/6] Start removing dependency from FhirVersionEnum to FhirContext (#6512) Deprecate path from FhirVersionEnum to FhirContext and replace usages. --- .../src/main/java/ca/uhn/fhir/context/FhirContext.java | 10 +++++++++- .../main/java/ca/uhn/fhir/context/FhirVersionEnum.java | 6 +++++- .../narrative2/NarrativeGeneratorTemplateUtils.java | 4 +++- .../src/main/java/ca/uhn/fhir/cli/BaseCommand.java | 2 +- .../converters/canonical/VersionCanonicalizer.java | 2 +- .../fhir/changelog/7_8_0/6512-snip-fhir-context.yaml | 4 ++++ .../ca/uhn/fhir/rest/server/RestfulServerUtils.java | 2 +- .../src/main/java/ca/uhn/fhir/to/BaseController.java | 2 +- 8 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6512-snip-fhir-context.yaml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index acc1f898574..796c9f7392e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -1293,7 +1293,15 @@ public class FhirContext { * @since 5.1.0 */ public static FhirContext forCached(FhirVersionEnum theFhirVersionEnum) { - return ourStaticContexts.computeIfAbsent(theFhirVersionEnum, v -> new FhirContext(v)); + return ourStaticContexts.computeIfAbsent(theFhirVersionEnum, FhirContext::forVersion); + } + + /** + * An uncached version of forCached() + * @return a new FhirContext for theFhirVersionEnum + */ + public static FhirContext forVersion(FhirVersionEnum theFhirVersionEnum) { + return new FhirContext(theFhirVersionEnum); } private static Collection> toCollection( diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java index 8666fd77ce0..2c3e0082bcf 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java @@ -135,15 +135,19 @@ public enum FhirVersionEnum { /** * Creates a new FhirContext for this FHIR version + * @deprecated since 7.7. Use {@link FhirContext#forVersion(FhirVersionEnum)} instead */ + @Deprecated(forRemoval = true, since = "7.7") public FhirContext newContext() { - return new FhirContext(this); + return FhirContext.forVersion(this); } /** * Creates a new FhirContext for this FHIR version, or returns a previously created one if one exists. This * method uses {@link FhirContext#forCached(FhirVersionEnum)} to return a cached instance. + * @deprecated since 7.7. Use {@link FhirContext#forCached(FhirVersionEnum)} instead */ + @Deprecated(forRemoval = true, since = "7.7") public FhirContext newContextCached() { return FhirContext.forCached(this); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java index 21cdab2dcde..a6a9406d72b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeGeneratorTemplateUtils.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.narrative2; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.util.BundleUtil; import org.apache.commons.lang3.tuple.Pair; import org.hl7.fhir.instance.model.api.IBaseBundle; @@ -42,7 +43,8 @@ public class NarrativeGeneratorTemplateUtils { * Given a Bundle as input, are any entries present with a given resource type */ public boolean bundleHasEntriesWithResourceType(IBaseBundle theBaseBundle, String theResourceType) { - FhirContext ctx = theBaseBundle.getStructureFhirVersionEnum().newContextCached(); + FhirVersionEnum fhirVersionEnum = theBaseBundle.getStructureFhirVersionEnum(); + FhirContext ctx = FhirContext.forCached(fhirVersionEnum); List> entryResources = BundleUtil.getBundleEntryUrlsAndResources(ctx, theBaseBundle); return entryResources.stream() diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java index be49c375f4e..0077accc776 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java @@ -668,7 +668,7 @@ public abstract class BaseCommand implements Comparable { protected void parseFhirContext(CommandLine theCommandLine) throws ParseException { FhirVersionEnum versionEnum = parseFhirVersion(theCommandLine); - myFhirCtx = versionEnum.newContext(); + myFhirCtx = FhirContext.forVersion(versionEnum); } public abstract void run(CommandLine theCommandLine) throws ParseException, ExecutionException; diff --git a/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/canonical/VersionCanonicalizer.java b/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/canonical/VersionCanonicalizer.java index 0a01892f056..25c8ab333c1 100644 --- a/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/canonical/VersionCanonicalizer.java +++ b/hapi-fhir-converter/src/main/java/ca/uhn/hapi/converters/canonical/VersionCanonicalizer.java @@ -98,7 +98,7 @@ public class VersionCanonicalizer { private final FhirContext myContext; public VersionCanonicalizer(FhirVersionEnum theTargetVersion) { - this(theTargetVersion.newContextCached()); + this(FhirContext.forCached(theTargetVersion)); } public VersionCanonicalizer(FhirContext theTargetContext) { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6512-snip-fhir-context.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6512-snip-fhir-context.yaml new file mode 100644 index 00000000000..71d35b1adf4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6512-snip-fhir-context.yaml @@ -0,0 +1,4 @@ +--- +type: remove +issue: 6512 +title: "The methods on FhirVersionEnum which produces a FhirContext (newContext() ,and newContextCached()) have been deprecated, and will be removed." diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index a88e50327b6..8477293b90b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -686,7 +686,7 @@ public class RestfulServerUtils { if (context.getVersion().getVersion() != theForVersion) { context = myFhirContextMap.get(theForVersion); if (context == null) { - context = theForVersion.newContext(); + context = FhirContext.forVersion(theForVersion); myFhirContextMap.put(theForVersion, context); } } diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java index f51dc5a4500..ba607ace1d9 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java @@ -297,7 +297,7 @@ public class BaseController { FhirVersionEnum version = theRequest.getFhirVersion(myConfig); VersionCanonicalizer retVal = myCanonicalizers.get(version); if (retVal == null) { - retVal = new VersionCanonicalizer(version.newContext()); + retVal = new VersionCanonicalizer(FhirContext.forVersion(version)); myCanonicalizers.put(version, retVal); } return retVal; From 061390d76b3e6097dca8decb97a3c8fab81eb637 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 27 Nov 2024 07:14:48 -0500 Subject: [PATCH 4/6] Add composite interceptor registry (#6511) * Composite interceptor improvements * Add composite interceptor registry * Add changelog * Composite Interceptor Broadcaster Improvements * Fix compile error * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6511-rework-composite-interceptor-broadcaster.yaml Co-authored-by: Tadgh * Address review comments * Test fixes * Test fix * Test fix --------- Co-authored-by: Tadgh --- .../ca/uhn/fhir/interceptor/api/Hook.java | 6 +- .../api/IBaseInterceptorBroadcaster.java | 12 + .../uhn/fhir/interceptor/api/IPointcut.java | 2 + .../ca/uhn/fhir/interceptor/api/Pointcut.java | 10 + .../executor/BaseInterceptorService.java | 283 ++++++++++-------- .../executor/InterceptorService.java | 6 +- ...ork-composite-interceptor-broadcaster.yaml | 8 + .../export/svc/JpaBulkExportProcessor.java | 4 +- .../ca/uhn/fhir/jpa/config/SearchConfig.java | 5 +- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 17 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 33 +- .../fhir/jpa/dao/FulltextSearchSvcImpl.java | 8 +- .../dao/expunge/ExpungeEverythingService.java | 15 +- .../expunge/JpaResourceExpungeService.java | 8 +- .../jpa/delete/DeleteConflictService.java | 6 +- .../delete/ThreadSafeResourceDeleterSvc.java | 8 +- .../search/PersistedJpaBundleProvider.java | 17 +- .../jpa/search/SearchCoordinatorSvcImpl.java | 20 +- .../jpa/search/SynchronousSearchSvcImpl.java | 22 +- .../jpa/search/builder/SearchBuilder.java | 114 +++---- .../StorageInterceptorHooksFacade.java | 19 +- .../ResourceLinkPredicateBuilder.java | 16 +- .../predicate/UriPredicateBuilder.java | 21 +- .../jpa/search/builder/tasks/SearchTask.java | 24 +- .../svc/JpaBulkExportProcessorTest.java | 19 +- .../SearchParamExtractorService.java | 21 +- .../SearchParamExtractorServiceTest.java | 17 +- .../SubscriptionMatcherInterceptor.java | 13 +- .../SubscriptionTriggeringSvcImpl.java | 4 +- .../jpa/provider/SubscriptionsDstu2Test.java | 2 +- .../ca/uhn/fhir/jpa/search/BaseSearchSvc.java | 2 +- .../search/SearchCoordinatorSvcImplTest.java | 18 +- .../search/SynchronousSearchSvcImplTest.java | 6 +- .../jpa/term/TerminologySvcImplDstu2Test.java | 5 + .../bulk/BulkDataExportProviderR4Test.java | 13 +- .../bulk/BulkDataExportProviderR5Test.java | 13 +- .../jpa/dao/BaseHapiFhirResourceDaoTest.java | 2 +- .../jpa/dao/r4/BaseComboParamsR4Test.java | 28 +- .../MdmReadVirtualizationInterceptor.java | 2 +- .../fhir/mdm/svc/MdmSearchExpansionSvc.java | 24 +- .../uhn/fhir/mdm/svc/MdmSearchParamSvc.java | 2 +- .../rest/api/server/SystemRequestDetails.java | 6 + .../interceptor/ServerInterceptorUtil.java | 23 +- .../util/CompositeInterceptorBroadcaster.java | 150 +++++----- .../CompositeInterceptorBroadcasterTest.java | 251 +++++++++++++--- .../jobs/export/BulkDataExportProvider.java | 36 +-- .../DeleteExpungeJobSubmitterImpl.java | 17 +- .../interceptor/BinaryStorageInterceptor.java | 16 +- .../binary/svc/BaseBinaryStorageSvcImpl.java | 14 +- .../ca/uhn/fhir/jpa/dao/BaseStorageDao.java | 87 +++--- .../jpa/dao/BaseTransactionProcessor.java | 58 ++-- .../fhir/jpa/dao/MatchResourceUrlService.java | 16 +- .../fhir/jpa/dao/SearchBuilderFactory.java | 8 +- .../jpa/dao/tx/HapiTransactionService.java | 28 +- .../BaseRequestPartitionHelperSvc.java | 100 ++++--- .../dao/tx/HapiTransactionServiceTest.java | 9 +- .../uhn/fhir/test/utilities/MockInvoker.java | 51 ++++ 57 files changed, 1036 insertions(+), 709 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6511-rework-composite-interceptor-broadcaster.yaml create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/MockInvoker.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java index 009a4a9b134..db3e34b0c5f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Hook.java @@ -46,7 +46,11 @@ public @interface Hook { * and allowable values can be positive or negative or 0. *

    * If no order is specified, or the order is set to 0 (the default order), - * the order specified at the interceptor type level will take precedence. + * the order specified at the {@link Interceptor#order() interceptor type level} will be used. + *

    + *

    + * Note that if two hook methods have the same order, then the order of execution is undefined. If + * order is important, then an order must always be explicitly stated. *

    */ int order() default Interceptor.DEFAULT_ORDER; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java index ecbedb55b58..8400caa0a4f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.interceptor.api; +import java.util.List; import java.util.function.Supplier; public interface IBaseInterceptorBroadcaster { @@ -73,4 +74,15 @@ public interface IBaseInterceptorBroadcaster { * @since 4.0.0 */ boolean hasHooks(POINTCUT thePointcut); + + List getInvokersForPointcut(POINTCUT thePointcut); + + interface IInvoker extends Comparable { + + Object invoke(HookParams theParams); + + int getOrder(); + + Object getInterceptor(); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java index ab5d718091f..382890eb603 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java @@ -27,6 +27,8 @@ public interface IPointcut { @Nonnull Class getReturnType(); + Class getBooleanReturnTypeForEnum(); + @Nonnull List getParameterTypes(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index a3baa55eb45..65fe637fe9c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.validation.ValidationResult; import jakarta.annotation.Nonnull; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseConformance; import java.io.Writer; @@ -3107,6 +3108,10 @@ public enum Pointcut implements IPointcut { @Nonnull Class theReturnType, @Nonnull ExceptionHandlingSpec theExceptionHandlingSpec, String... theParameterTypes) { + + // This enum uses the lowercase-b boolean type to indicate boolean return pointcuts + Validate.isTrue(!theReturnType.equals(Boolean.class), "Return type Boolean not allowed here, must be boolean"); + myReturnType = theReturnType; myExceptionHandlingSpec = theExceptionHandlingSpec; myParameterTypes = Collections.unmodifiableList(Arrays.asList(theParameterTypes)); @@ -3132,6 +3137,11 @@ public enum Pointcut implements IPointcut { return myReturnType; } + @Override + public Class getBooleanReturnTypeForEnum() { + return boolean.class; + } + @Override @Nonnull public List getParameterTypes() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java index 911164745d4..8698269ec97 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.interceptor.executor; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IBaseInterceptorService; @@ -57,12 +58,13 @@ import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + public abstract class BaseInterceptorService & IPointcut> implements IBaseInterceptorService, IBaseInterceptorBroadcaster { private static final Logger ourLog = LoggerFactory.getLogger(BaseInterceptorService.class); @@ -74,12 +76,11 @@ public abstract class BaseInterceptorService & I AttributeKey.stringKey("hapifhir.interceptor.method_name"); private final List myInterceptors = new ArrayList<>(); - private final ListMultimap myGlobalInvokers = ArrayListMultimap.create(); - private final ListMultimap myAnonymousInvokers = ArrayListMultimap.create(); + private final ListMultimap myGlobalInvokers = ArrayListMultimap.create(); + private final ListMultimap myAnonymousInvokers = ArrayListMultimap.create(); private final Object myRegistryMutex = new Object(); private final Class myPointcutType; private volatile EnumSet myRegisteredPointcuts; - private String myName; private boolean myWarnOnInterceptorWithNoHooks = true; /** @@ -93,10 +94,11 @@ public abstract class BaseInterceptorService & I * Constructor * * @param theName The name for this registry (useful for troubleshooting) + * @deprecated The name parameter is not used for anything */ + @Deprecated(since = "8.0.0", forRemoval = true) public BaseInterceptorService(Class thePointcutType, String theName) { super(); - myName = theName; myPointcutType = thePointcutType; rebuildRegisteredPointcutSet(); } @@ -113,13 +115,17 @@ public abstract class BaseInterceptorService & I return myInterceptors; } - public void setName(String theName) { - myName = theName; + /** + * @deprecated This value is not used anywhere + */ + @Deprecated(since = "8.0.0", forRemoval = true) + public void setName(@SuppressWarnings("unused") String theName) { + // nothing } protected void registerAnonymousInterceptor(POINTCUT thePointcut, Object theInterceptor, BaseInvoker theInvoker) { - Validate.notNull(thePointcut); - Validate.notNull(theInterceptor); + Validate.notNull(thePointcut, "thePointcut must not be null"); + Validate.notNull(theInterceptor, "theInterceptor must not be null"); synchronized (myRegistryMutex) { myAnonymousInvokers.put(thePointcut, theInvoker); if (!isInterceptorAlreadyRegistered(theInterceptor)) { @@ -179,9 +185,9 @@ public abstract class BaseInterceptorService & I } private void unregisterInterceptorsIf( - Predicate theShouldUnregisterFunction, ListMultimap theGlobalInvokers) { + Predicate theShouldUnregisterFunction, ListMultimap theGlobalInvokers) { synchronized (myRegistryMutex) { - for (Map.Entry nextInvoker : new ArrayList<>(theGlobalInvokers.entries())) { + for (Map.Entry nextInvoker : new ArrayList<>(theGlobalInvokers.entries())) { if (theShouldUnregisterFunction.test(nextInvoker.getValue().getInterceptor())) { unregisterInterceptor(nextInvoker.getValue().getInterceptor()); } @@ -265,7 +271,7 @@ public abstract class BaseInterceptorService & I assert haveAppropriateParams(thePointcut, theParams); assert thePointcut.getReturnType() != void.class; - return doCallHooks(thePointcut, theParams, null); + return doCallHooks(thePointcut, theParams); } @Override @@ -282,116 +288,47 @@ public abstract class BaseInterceptorService & I assert haveAppropriateParams(thePointcut, theParams); assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == getBooleanReturnType(); - Object retValObj = doCallHooks(thePointcut, theParams, true); + Object retValObj = doCallHooks(thePointcut, theParams); + retValObj = defaultIfNull(retValObj, true); return (Boolean) retValObj; } - private Object doCallHooks(POINTCUT thePointcut, HookParams theParams, Object theRetVal) { - // use new list for loop to avoid ConcurrentModificationException in case invoker gets added while looping - List invokers = new ArrayList<>(getInvokersForPointcut(thePointcut)); - - /* - * Call each hook in order - */ - for (BaseInvoker nextInvoker : invokers) { - Object nextOutcome = nextInvoker.invoke(theParams); - Class pointcutReturnType = thePointcut.getReturnType(); - if (pointcutReturnType.equals(getBooleanReturnType())) { - Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome; - if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) { - ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker); - theRetVal = false; - break; - } else { - theRetVal = true; - } - } else if (!pointcutReturnType.equals(void.class)) { - if (nextOutcome != null) { - theRetVal = nextOutcome; - break; - } - } - } - - return theRetVal; + private Object doCallHooks(POINTCUT thePointcut, HookParams theParams) { + List invokers = getInvokersForPointcut(thePointcut); + return callInvokers(thePointcut, theParams, invokers); } @VisibleForTesting List getInterceptorsWithInvokersForPointcut(POINTCUT thePointcut) { return getInvokersForPointcut(thePointcut).stream() - .map(BaseInvoker::getInterceptor) + .map(IInvoker::getInterceptor) .collect(Collectors.toList()); } /** - * Returns an ordered list of invokers for the given pointcut. Note that - * a new and stable list is returned to.. do whatever you want with it. + * Returns a list of all invokers registered for the given pointcut. The list + * is ordered by the invoker order (specified on the {@link Interceptor#order()} + * and {@link Hook#order()} values. + * + * @return The list returned by this method will always be a newly created list, so it will be stable and can be modified. */ - private List getInvokersForPointcut(POINTCUT thePointcut) { - List invokers; + @Override + public List getInvokersForPointcut(POINTCUT thePointcut) { + List invokers; synchronized (myRegistryMutex) { - List globalInvokers = myGlobalInvokers.get(thePointcut); - List anonymousInvokers = myAnonymousInvokers.get(thePointcut); - List threadLocalInvokers = null; - invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers); + List globalInvokers = myGlobalInvokers.get(thePointcut); + List anonymousInvokers = myAnonymousInvokers.get(thePointcut); + invokers = union(Arrays.asList(globalInvokers, anonymousInvokers)); } return invokers; } - /** - * First argument must be the global invoker list!! - */ - @SafeVarargs - private List union(List... theInvokersLists) { - List haveOne = null; - boolean haveMultiple = false; - for (List nextInvokerList : theInvokersLists) { - if (nextInvokerList == null || nextInvokerList.isEmpty()) { - continue; - } - - if (haveOne == null) { - haveOne = nextInvokerList; - } else { - haveMultiple = true; - } - } - - if (haveOne == null) { - return Collections.emptyList(); - } - - List retVal; - - if (!haveMultiple) { - - // The global list doesn't need to be sorted every time since it's sorted on - // insertion each time. Doing so is a waste of cycles.. - if (haveOne == theInvokersLists[0]) { - retVal = haveOne; - } else { - retVal = new ArrayList<>(haveOne); - retVal.sort(Comparator.naturalOrder()); - } - - } else { - - retVal = Arrays.stream(theInvokersLists) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .sorted() - .collect(Collectors.toList()); - } - - return retVal; - } - /** * Only call this when assertions are enabled, it's expensive */ - final boolean haveAppropriateParams(POINTCUT thePointcut, HookParams theParams) { + public static boolean haveAppropriateParams(IPointcut thePointcut, HookParams theParams) { if (theParams.getParamsForType().values().size() != thePointcut.getParameterTypes().size()) { throw new IllegalArgumentException(Msg.code(1909) @@ -430,7 +367,7 @@ public abstract class BaseInterceptorService & I } private List scanInterceptorAndAddToInvokerMultimap( - Object theInterceptor, ListMultimap theInvokers) { + Object theInterceptor, ListMultimap theInvokers) { Class interceptorClass = theInterceptor.getClass(); int typeOrder = determineOrder(interceptorClass); @@ -452,7 +389,7 @@ public abstract class BaseInterceptorService & I // Make sure we're always sorted according to the order declared in @Order for (POINTCUT nextPointcut : theInvokers.keys()) { - List nextInvokerList = theInvokers.get(nextPointcut); + List nextInvokerList = theInvokers.get(nextPointcut); nextInvokerList.sort(Comparator.naturalOrder()); } @@ -483,6 +420,108 @@ public abstract class BaseInterceptorService & I protected abstract Optional scanForHook(Method nextMethod); + public static Object callInvokers(IPointcut thePointcut, HookParams theParams, List invokers) { + + Object retVal = null; + + /* + * Call each hook in order + */ + for (IInvoker nextInvoker : invokers) { + Object nextOutcome = nextInvoker.invoke(theParams); + Class pointcutReturnType = thePointcut.getReturnType(); + if (pointcutReturnType.equals(thePointcut.getBooleanReturnTypeForEnum())) { + Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome; + if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) { + ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker); + retVal = false; + break; + } else { + retVal = true; + } + } else if (!pointcutReturnType.equals(void.class)) { + if (nextOutcome != null) { + retVal = nextOutcome; + break; + } + } + } + + return retVal; + } + + /** + * First argument must be the global invoker list!! + */ + public static List union(List> theInvokersLists) { + List haveOne = null; + boolean haveMultiple = false; + for (List nextInvokerList : theInvokersLists) { + if (nextInvokerList == null || nextInvokerList.isEmpty()) { + continue; + } + + if (haveOne == null) { + haveOne = nextInvokerList; + } else { + haveMultiple = true; + } + } + + if (haveOne == null) { + return Collections.emptyList(); + } + + List retVal; + + if (!haveMultiple) { + + // The global list doesn't need to be sorted every time since it's sorted on + // insertion each time. Doing so is a waste of cycles.. + if (haveOne == theInvokersLists.get(0)) { + retVal = haveOne; + } else { + retVal = new ArrayList<>(haveOne); + retVal.sort(Comparator.naturalOrder()); + } + + } else { + + int totalSize = 0; + for (List list : theInvokersLists) { + totalSize += list.size(); + } + retVal = new ArrayList<>(totalSize); + for (List list : theInvokersLists) { + retVal.addAll(list); + } + retVal.sort(Comparator.naturalOrder()); + } + + return retVal; + } + + protected static Optional findAnnotation( + AnnotatedElement theObject, Class theHookClass) { + T annotation; + if (theObject instanceof Method) { + annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true); + } else { + annotation = theObject.getAnnotation(theHookClass); + } + return Optional.ofNullable(annotation); + } + + private static int determineOrder(Class theInterceptorClass) { + return findAnnotation(theInterceptorClass, Interceptor.class) + .map(Interceptor::order) + .orElse(Interceptor.DEFAULT_ORDER); + } + + private static String toErrorString(List theParameterTypes) { + return theParameterTypes.stream().sorted().collect(Collectors.joining(",")); + } + private class HookInvoker extends BaseInvoker { private final Method myMethod; @@ -501,10 +540,11 @@ public abstract class BaseInterceptorService & I myMethod = theHookMethod; Class returnType = theHookMethod.getReturnType(); - if (myPointcut.getReturnType().equals(getBooleanReturnType())) { + if (myPointcut.getReturnType().equals(myPointcut.getBooleanReturnTypeForEnum())) { Validate.isTrue( - getBooleanReturnType().equals(returnType) || void.class.equals(returnType), - "Method does not return boolean or void: %s", + myPointcut.getBooleanReturnTypeForEnum().equals(returnType) || void.class.equals(returnType), + "Method does not return %s or void: %s", + myPointcut.getBooleanReturnTypeForEnum().getSimpleName(), theHookMethod); } else if (myPointcut.getReturnType().equals(void.class)) { Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod); @@ -541,7 +581,7 @@ public abstract class BaseInterceptorService & I * @return Returns true/false if the hook method returns a boolean, returns true otherwise */ @Override - Object invoke(HookParams theParams) { + public Object invoke(HookParams theParams) { Object[] args = new Object[myParameterTypes.length]; for (int i = 0; i < myParameterTypes.length; i++) { @@ -610,7 +650,7 @@ public abstract class BaseInterceptorService & I } } - protected abstract static class BaseInvoker implements Comparable { + public abstract static class BaseInvoker implements IInvoker { private final int myOrder; private final Object myInterceptor; @@ -620,36 +660,19 @@ public abstract class BaseInterceptorService & I myOrder = theOrder; } + @Override public Object getInterceptor() { return myInterceptor; } - abstract Object invoke(HookParams theParams); + @Override + public int getOrder() { + return myOrder; + } @Override - public int compareTo(BaseInvoker theInvoker) { - return myOrder - theInvoker.myOrder; + public int compareTo(IInvoker theInvoker) { + return myOrder - theInvoker.getOrder(); } } - - protected static Optional findAnnotation( - AnnotatedElement theObject, Class theHookClass) { - T annotation; - if (theObject instanceof Method) { - annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true); - } else { - annotation = theObject.getAnnotation(theHookClass); - } - return Optional.ofNullable(annotation); - } - - private static int determineOrder(Class theInterceptorClass) { - return findAnnotation(theInterceptorClass, Interceptor.class) - .map(Interceptor::order) - .orElse(Interceptor.DEFAULT_ORDER); - } - - private static String toErrorString(List theParameterTypes) { - return theParameterTypes.stream().sorted().collect(Collectors.joining(",")); - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java index 659dfb27b8b..28a3bb6f96c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java @@ -64,8 +64,8 @@ public class InterceptorService extends BaseInterceptorService @Override public void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor) { - Validate.notNull(thePointcut); - Validate.notNull(theInterceptor); + Validate.notNull(thePointcut, "thePointcut must not be null"); + Validate.notNull(theInterceptor, "theInterceptor must not be null"); BaseInvoker invoker = new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder); registerAnonymousInterceptor(thePointcut, theInterceptor, invoker); } @@ -81,7 +81,7 @@ public class InterceptorService extends BaseInterceptorService } @Override - Object invoke(HookParams theParams) { + public Object invoke(HookParams theParams) { myHook.invoke(myPointcut, theParams); return true; } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6511-rework-composite-interceptor-broadcaster.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6511-rework-composite-interceptor-broadcaster.yaml new file mode 100644 index 00000000000..e62ac0fabde --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6511-rework-composite-interceptor-broadcaster.yaml @@ -0,0 +1,8 @@ +--- +type: add +issue: 6511 +title: "Interceptors can be defined against the registry on the RestfulServer, or on + the registry in the JPA repository. Because these are separate registries, the order() + attribute on the Hook annotation isn't correctly processed today across the two + registries. The CompositeInterceptorRegistry has been reworked so ordering will be + respected across both registries." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java index 17a6833bd12..d2df02a8bce 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessor.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.PersistentIdToForcedIdMap; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; import ca.uhn.fhir.jpa.bulk.export.api.IBulkExportProcessor; @@ -349,10 +348,9 @@ public class JpaBulkExportProcessor implements IBulkExportProcessor { * Get a ISearchBuilder for the given resource type. */ protected ISearchBuilder getSearchBuilderForResourceType(String theResourceType) { - IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceType); RuntimeResourceDefinition def = myContext.getResourceDefinition(theResourceType); Class typeClass = def.getImplementingClass(); - return mySearchBuilderFactory.newSearchBuilder(dao, theResourceType, typeClass); + return mySearchBuilderFactory.newSearchBuilder(theResourceType, typeClass); } protected RuntimeSearchParam getPatientSearchParamForCurrentResourceType(String theResourceType) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java index e1070f1de6a..80d8665362a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/SearchConfig.java @@ -25,7 +25,6 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.dao.ISearchBuilder; @@ -158,10 +157,8 @@ public class SearchConfig { @Bean(name = ISearchBuilder.SEARCH_BUILDER_BEAN_NAME) @Scope("prototype") - public ISearchBuilder newSearchBuilder( - IDao theDao, String theResourceName, Class theResourceType) { + public ISearchBuilder newSearchBuilder(String theResourceName, Class theResourceType) { return new SearchBuilder( - theDao, theResourceName, myStorageSettings, myEntityManagerFactory, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index f86ad9310be..6335adde1b3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1057,8 +1057,9 @@ public abstract class BaseHapiFhirDao extends BaseStora // Interceptor broadcast: JPA_PERFTRACE_INFO if (!presenceCount.isEmpty()) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { StorageProcessingMessage message = new StorageProcessingMessage(); message.setMessage( "For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " @@ -1068,8 +1069,7 @@ public abstract class BaseHapiFhirDao extends BaseStora .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest) .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); } } } @@ -1092,8 +1092,10 @@ public abstract class BaseHapiFhirDao extends BaseStora // Interceptor broadcast: JPA_PERFTRACE_INFO if (!searchParamAddRemoveCount.isEmpty()) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { StorageProcessingMessage message = new StorageProcessingMessage(); message.setMessage("For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " @@ -1104,8 +1106,7 @@ public abstract class BaseHapiFhirDao extends BaseStora .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest) .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 95f8cdbe480..ece727ab61b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -227,15 +227,15 @@ public abstract class BaseHapiFhirResourceDao extends B @Nullable public static T invokeStoragePreShowResources( IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequest, T retVal) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_PRESHOW_RESOURCES, theInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal); HookParams params = new HookParams() .add(IPreResourceShowDetails.class, showDetails) .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - theInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); //noinspection unchecked retVal = (T) showDetails.getResource( 0); // TODO GGG/JA : getting resource 0 is interesting. We apparently allow null values in the list. @@ -251,15 +251,15 @@ public abstract class BaseHapiFhirResourceDao extends B RequestDetails theRequest, IIdType theId, IBaseResource theResource) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_PREACCESS_RESOURCES, theInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource); HookParams params = new HookParams() .add(IPreResourceAccessDetails.class, accessDetails) .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - theInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); if (accessDetails.isDontReturnResourceAtIndex(0)) { throw new ResourceNotFoundException(Msg.code(1995) + "Resource " + theId + " is not known"); } @@ -1585,15 +1585,15 @@ public abstract class BaseHapiFhirResourceDao extends B } private Optional invokeStoragePreAccessResources(RequestDetails theRequest, T theResource) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_PREACCESS_RESOURCES, myInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource); HookParams params = new HookParams() .add(IPreResourceAccessDetails.class, accessDetails) .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); if (accessDetails.isDontReturnResourceAtIndex(0)) { return Optional.empty(); } @@ -2103,7 +2103,7 @@ public abstract class BaseHapiFhirResourceDao extends B } ISearchBuilder builder = - mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); + mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); List ids = new ArrayList<>(); @@ -2136,8 +2136,7 @@ public abstract class BaseHapiFhirResourceDao extends B myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull); - ISearchBuilder builder = - mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); + ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); String uuid = UUID.randomUUID().toString(); @@ -2175,7 +2174,7 @@ public abstract class BaseHapiFhirResourceDao extends B .withPropagation(Propagation.REQUIRED) .searchList(() -> { ISearchBuilder builder = - mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); + mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); Stream pidStream = builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, requestPartitionId); @@ -2191,7 +2190,7 @@ public abstract class BaseHapiFhirResourceDao extends B @Nonnull private Stream pidsToResource(RequestDetails theRequest, Stream pidStream) { ISearchBuilder searchBuilder = - mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); + mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); @SuppressWarnings("unchecked") Stream resourceStream = (Stream) new QueryChunker<>() .chunk(pidStream, SearchBuilder.getMaximumPageSize()) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java index 0bccd1b6c67..c313854c355 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java @@ -516,8 +516,9 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { */ @SuppressWarnings("rawtypes") private void logQuery(SearchQueryOptionsStep theQuery, RequestDetails theRequestDetails) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { StorageProcessingMessage storageProcessingMessage = new StorageProcessingMessage(); String queryString = theQuery.toQuery().queryString(); storageProcessingMessage.setMessage(queryString); @@ -525,8 +526,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .add(StorageProcessingMessage.class, storageProcessingMessage); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_INFO, params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index b6427e80f75..f8d63002793 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -124,12 +124,15 @@ public class ExpungeEverythingService implements IExpungeEverythingService { final AtomicInteger counter = new AtomicInteger(); // Notify Interceptors about pre-action call - HookParams hooks = new HookParams() - .add(AtomicInteger.class, counter) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING)) { + HookParams hooks = new HookParams() + .add(AtomicInteger.class, counter) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks); + } ourLog.info("BEGINNING GLOBAL $expunge"); Propagation propagation = Propagation.REQUIRES_NEW; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/JpaResourceExpungeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/JpaResourceExpungeService.java index da5652af20e..860113cec06 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/JpaResourceExpungeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/JpaResourceExpungeService.java @@ -256,8 +256,9 @@ public class JpaResourceExpungeService implements IResourceExpungeService dao) { - // Interceptor call: STORAGE_CASCADE_DELETE // Remove the version so we grab the latest version to delete IBaseResource resource = dao.read(nextSource.toVersionless(), theRequest); + + // Interceptor call: STORAGE_CASCADE_DELETE + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); HookParams params = new HookParams() .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest) .add(DeleteConflictList.class, theConflictList) .add(IBaseResource.class, resource); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_CASCADE_DELETE, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_CASCADE_DELETE, params); return dao.delete(resource.getIdElement(), theConflictList, theRequest, theTransactionDetails); } 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 0c59001032e..fccdb3ea4f1 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 @@ -27,7 +27,6 @@ import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.dao.HistoryBuilder; import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory; @@ -189,15 +188,17 @@ public class PersistedJpaBundleProvider implements IBundleProvider { retVal.add(myJpaStorageResourceParser.toResource(resource, true)); } + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, myRequest); + // Interceptor call: STORAGE_PREACCESS_RESOURCES - { + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(retVal); HookParams params = new HookParams() .add(IPreResourceAccessDetails.class, accessDetails) .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); for (int i = retVal.size() - 1; i >= 0; i--) { if (accessDetails.isDontReturnResourceAtIndex(i)) { @@ -207,14 +208,13 @@ public class PersistedJpaBundleProvider implements IBundleProvider { } // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES - { + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal); HookParams params = new HookParams() .add(IPreResourceShowDetails.class, showDetails) .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); retVal = showDetails.toList(); } @@ -254,9 +254,8 @@ public class PersistedJpaBundleProvider implements IBundleProvider { String resourceName = mySearchEntity.getResourceType(); Class resourceType = myContext.getResourceDefinition(resourceName).getImplementingClass(); - IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceName); - final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(dao, resourceName, resourceType); + final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(resourceName, resourceType); RequestPartitionId requestPartitionId = getRequestPartitionId(); // we request 1 more resource than we need diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index f169fba81d8..0a40be8c438 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -374,8 +374,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { Class resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass(); - final ISearchBuilder sb = - mySearchBuilderFactory.newSearchBuilder(theCallingDao, theResourceType, resourceTypeClass); + final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theResourceType, resourceTypeClass); sb.setFetchSize(mySyncSize); final Integer loadSynchronousUpTo = getLoadSynchronousUpToOrNull(theCacheControlDirective); @@ -599,17 +598,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { .withRequest(theRequestDetails) .withRequestPartitionId(theRequestPartitionId) .execute(() -> { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequestDetails); // Interceptor call: STORAGE_PRECHECK_FOR_CACHED_SEARCH + HookParams params = new HookParams() .add(SearchParameterMap.class, theParams) .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - boolean canUseCache = CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, - theRequestDetails, - Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH, - params); + boolean canUseCache = + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH, params); if (!canUseCache) { return null; } @@ -626,11 +626,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { .add(SearchParameterMap.class, theParams) .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, - theRequestDetails, - Pointcut.JPA_PERFTRACE_SEARCH_REUSING_CACHED, - params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_REUSING_CACHED, params); return myPersistedJpaBundleProviderFactory.newInstance(theRequestDetails, searchToUse.getUuid()); }); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImpl.java index 317b8fa2105..a7a0aab6239 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImpl.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IResultIterator; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; @@ -181,12 +180,16 @@ public class SynchronousSearchSvcImpl implements ISynchronousSearchSvc { } JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(pids, () -> theSb); - HookParams params = new HookParams() - .add(IPreResourceAccessDetails.class, accessDetails) - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PREACCESS_RESOURCES, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { + HookParams params = new HookParams() + .add(IPreResourceAccessDetails.class, accessDetails) + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); + } for (int i = pids.size() - 1; i >= 0; i--) { if (accessDetails.isDontReturnResourceAtIndex(i)) { @@ -279,12 +282,9 @@ public class SynchronousSearchSvcImpl implements ISynchronousSearchSvc { RequestPartitionId theRequestPartitionId) { final String searchUuid = UUID.randomUUID().toString(); - IFhirResourceDao callingDao = myDaoRegistry.getResourceDao(theResourceType); - Class resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass(); - final ISearchBuilder sb = - mySearchBuilderFactory.newSearchBuilder(callingDao, theResourceType, resourceTypeClass); + final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theResourceType, resourceTypeClass); sb.setFetchSize(mySyncSize); return executeQuery( theSearchParameterMap, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index acd8fe0e5a3..088703dbb82 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -33,7 +33,6 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; @@ -192,7 +191,6 @@ public class SearchBuilder implements ISearchBuilder { private final FhirContext myContext; private final IIdHelperService myIdHelperService; private final JpaStorageSettings myStorageSettings; - private final IDao myCallingDao; @PersistenceContext(type = PersistenceContextType.TRANSACTION) protected EntityManager myEntityManager; @@ -220,7 +218,6 @@ public class SearchBuilder implements ISearchBuilder { */ @SuppressWarnings({"rawtypes", "unchecked"}) public SearchBuilder( - IDao theDao, String theResourceName, JpaStorageSettings theStorageSettings, HapiFhirLocalContainerEntityManagerFactoryBean theEntityManagerFactory, @@ -235,7 +232,6 @@ public class SearchBuilder implements ISearchBuilder { FhirContext theContext, IIdHelperService theIdHelperService, Class theResourceType) { - myCallingDao = theDao; myResourceName = theResourceName; myResourceType = theResourceType; myStorageSettings = theStorageSettings; @@ -426,15 +422,15 @@ public class SearchBuilder implements ISearchBuilder { if (theSearchRuntimeDetails != null) { theSearchRuntimeDetails.setFoundIndexMatchesCount(resultCount); - HookParams params = new HookParams() - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(SearchRuntimeDetails.class, theSearchRuntimeDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, - theRequest, - Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, - params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE)) { + HookParams params = new HookParams() + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(SearchRuntimeDetails.class, theSearchRuntimeDetails); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, params); + } } // can we skip the database entirely and return the pid list from here? @@ -1393,8 +1389,10 @@ public class SearchBuilder implements ISearchBuilder { String searchIdOrDescription = theParameters.getSearchIdOrDescription(); List desiredResourceTypes = theParameters.getDesiredResourceTypes(); boolean hasDesiredResourceTypes = desiredResourceTypes != null && !desiredResourceTypes.isEmpty(); - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, theParameters.getRequestDetails())) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, request); + + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL)) { CurrentThreadCaptureQueriesListener.startCapturing(); } if (matches.isEmpty()) { @@ -1498,17 +1496,16 @@ public class SearchBuilder implements ISearchBuilder { w.getMillisAndRestart(), searchIdOrDescription); - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, request)) { - callRawSqlHookWithCurrentThreadQueries(request); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL)) { + callRawSqlHookWithCurrentThreadQueries(request, compositeBroadcaster); } + // Interceptor call: STORAGE_PREACCESS_RESOURCES // This can be used to remove results from the search result details before // the user has a chance to know that they were in the results if (!allAdded.isEmpty()) { - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_PREACCESS_RESOURCES, myInterceptorBroadcaster, request)) { + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { List includedPidList = new ArrayList<>(allAdded); JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(includedPidList, () -> this); @@ -1516,8 +1513,7 @@ public class SearchBuilder implements ISearchBuilder { .add(IPreResourceAccessDetails.class, accessDetails) .add(RequestDetails.class, request) .addIfMatchesType(ServletRequestDetails.class, request); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, request, Pointcut.STORAGE_PREACCESS_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); for (int i = includedPidList.size() - 1; i >= 0; i--) { if (accessDetails.isDontReturnResourceAtIndex(i)) { @@ -1813,17 +1809,18 @@ public class SearchBuilder implements ISearchBuilder { /** * Calls Performance Trace Hook - * @param request the request deatils - * Sends a raw SQL query to the Pointcut for raw SQL queries. + * + * @param request the request deatils + * Sends a raw SQL query to the Pointcut for raw SQL queries. */ - private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) { + private void callRawSqlHookWithCurrentThreadQueries( + RequestDetails request, IInterceptorBroadcaster theCompositeBroadcaster) { SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing(); HookParams params = new HookParams() .add(RequestDetails.class, request) .addIfMatchesType(ServletRequestDetails.class, request) .add(SqlQueryList.class, capturedQueries); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, request, Pointcut.JPA_PERFTRACE_RAW_SQL, params); + theCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, params); } @Nullable @@ -2095,16 +2092,19 @@ public class SearchBuilder implements ISearchBuilder { indexStrings.sort(Comparator.naturalOrder()); // Interceptor broadcast: JPA_PERFTRACE_INFO - String indexStringForLog = indexStrings.size() > 1 ? indexStrings.toString() : indexStrings.get(0); - StorageProcessingMessage msg = new StorageProcessingMessage() - .setMessage("Using " + theComboParam.getComboSearchParamType() + " index(es) for query for search: " - + indexStringForLog); - HookParams params = new HookParams() - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(StorageProcessingMessage.class, msg); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { + String indexStringForLog = indexStrings.size() > 1 ? indexStrings.toString() : indexStrings.get(0); + StorageProcessingMessage msg = new StorageProcessingMessage() + .setMessage("Using " + theComboParam.getComboSearchParamType() + " index(es) for query for search: " + + indexStringForLog); + HookParams params = new HookParams() + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(StorageProcessingMessage.class, msg); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); + } switch (requireNonNull(theComboParam.getComboSearchParamType())) { case UNIQUE: @@ -2330,6 +2330,7 @@ public class SearchBuilder implements ISearchBuilder { private final boolean myHavePerfTraceFoundIdHook; private final SortSpec mySort; private final Integer myOffset; + private final IInterceptorBroadcaster myCompositeBroadcaster; private boolean myFirst = true; private IncludesIterator myIncludesIterator; /** @@ -2368,16 +2369,16 @@ public class SearchBuilder implements ISearchBuilder { mySort = myParams.getSort(); myOffset = myParams.getOffset(); myRequest = theRequest; + myCompositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); // everything requires fetching recursively all related resources if (myParams.getEverythingMode() != null) { myFetchIncludesForEverythingOperation = true; } - myHavePerfTraceFoundIdHook = CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest); - myHaveRawSqlHooks = CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest); + myHavePerfTraceFoundIdHook = myCompositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID); + myHaveRawSqlHooks = myCompositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL); } private void fetchNext() { @@ -2479,7 +2480,7 @@ public class SearchBuilder implements ISearchBuilder { } finally { // search finished - fire hooks if (myHaveRawSqlHooks) { - callRawSqlHookWithCurrentThreadQueries(myRequest); + callRawSqlHookWithCurrentThreadQueries(myRequest, myCompositeBroadcaster); } } @@ -2488,8 +2489,7 @@ public class SearchBuilder implements ISearchBuilder { .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest) .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params); + myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params); myFirst = false; } @@ -2498,8 +2498,7 @@ public class SearchBuilder implements ISearchBuilder { .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest) .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params); + myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params); } } @@ -2523,8 +2522,7 @@ public class SearchBuilder implements ISearchBuilder { HookParams params = new HookParams() .add(Integer.class, System.identityHashCode(this)) .add(Object.class, theNextLong); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params); + myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params); } private void sendProcessingMsgAndFirePerformanceHook() { @@ -2627,14 +2625,18 @@ public class SearchBuilder implements ISearchBuilder { firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_WARNING); } - private void firePerformanceMessage(RequestDetails theRequest, String theMessage, Pointcut pointcut) { - StorageProcessingMessage message = new StorageProcessingMessage(); - message.setMessage(theMessage); - HookParams params = new HookParams() - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, pointcut, params); + private void firePerformanceMessage(RequestDetails theRequest, String theMessage, Pointcut thePointcut) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(thePointcut)) { + StorageProcessingMessage message = new StorageProcessingMessage(); + message.setMessage(theMessage); + HookParams params = new HookParams() + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(StorageProcessingMessage.class, message); + compositeBroadcaster.callHooks(thePointcut, params); + } } public static int getMaximumPageSize() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/StorageInterceptorHooksFacade.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/StorageInterceptorHooksFacade.java index ed031c53f87..31aecca81d9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/StorageInterceptorHooksFacade.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/StorageInterceptorHooksFacade.java @@ -53,14 +53,17 @@ public class StorageInterceptorHooksFacade { SearchParameterMap theParams, Search search, RequestPartitionId theRequestPartitionId) { - HookParams params = new HookParams() - .add(ICachedSearchDetails.class, search) - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(SearchParameterMap.class, theParams) - .add(RequestPartitionId.class, theRequestPartitionId); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESEARCH_REGISTERED)) { + HookParams params = new HookParams() + .add(ICachedSearchDetails.class, search) + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(SearchParameterMap.class, theParams) + .add(RequestPartitionId.class, theRequestPartitionId); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESEARCH_REGISTERED, params); + } } // private IInterceptorBroadcaster myInterceptorBroadcaster; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java index 5196028423d..a5e7f203874 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java @@ -407,12 +407,16 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder im } String message = builder.toString(); StorageProcessingMessage msg = new StorageProcessingMessage().setMessage(message); - HookParams params = new HookParams() - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest) - .add(StorageProcessingMessage.class, msg); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params); + + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { + HookParams params = new HookParams() + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(StorageProcessingMessage.class, msg); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); + } } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/UriPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/UriPredicateBuilder.java index 70dd30d42b7..96ace48a913 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/UriPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/UriPredicateBuilder.java @@ -109,15 +109,18 @@ public class UriPredicateBuilder extends BaseSearchParamPredicateBuilder { + "] param[" + theParamName + "]"; ourLog.info(msg); - StorageProcessingMessage message = new StorageProcessingMessage(); - ourLog.warn(msg); - message.setMessage(msg); - HookParams params = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_WARNING, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { + StorageProcessingMessage message = new StorageProcessingMessage(); + message.setMessage(msg); + HookParams params = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(StorageProcessingMessage.class, message); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); + } long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity( getPartitionSettings(), getRequestPartitionId(), getResourceType(), theParamName); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/tasks/SearchTask.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/tasks/SearchTask.java index cbec057c33a..395d1c5e374 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/tasks/SearchTask.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/tasks/SearchTask.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; -import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.jpa.dao.IResultIterator; import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; @@ -95,7 +94,6 @@ public class SearchTask implements Callable { protected final FhirContext myContext; protected final ISearchResultCacheSvc mySearchResultCacheSvc; private final SearchParameterMap myParams; - private final IDao myCallingDao; private final String myResourceType; private final ArrayList mySyncedPids = new ArrayList<>(); private final CountDownLatch myInitialCollectionLatch = new CountDownLatch(1); @@ -113,6 +111,7 @@ public class SearchTask implements Callable { private final JpaStorageSettings myStorageSettings; private final ISearchCacheSvc mySearchCacheSvc; private final IPagingProvider myPagingProvider; + private final IInterceptorBroadcaster myCompositeBroadcaster; private Search mySearch; private boolean myAbortRequested; private int myCountSavedTotal = 0; @@ -149,7 +148,6 @@ public class SearchTask implements Callable { // values myOnRemove = theCreationParams.OnRemove; mySearch = theCreationParams.Search; - myCallingDao = theCreationParams.CallingDao; myParams = theCreationParams.Params; myResourceType = theCreationParams.ResourceType; myRequest = theCreationParams.Request; @@ -158,9 +156,11 @@ public class SearchTask implements Callable { myLoadingThrottleForUnitTests = theCreationParams.getLoadingThrottleForUnitTests(); mySearchRuntimeDetails = new SearchRuntimeDetails(myRequest, mySearch.getUuid()); - mySearchRuntimeDetails.setQueryString(myParams.toNormalizedQueryString(myCallingDao.getContext())); + mySearchRuntimeDetails.setQueryString(myParams.toNormalizedQueryString(myContext)); myRequestPartitionId = theCreationParams.RequestPartitionId; myParentTransaction = ElasticApm.currentTransaction(); + myCompositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, myRequest); } protected RequestPartitionId getRequestPartitionId() { @@ -203,7 +203,7 @@ public class SearchTask implements Callable { private ISearchBuilder newSearchBuilder() { Class resourceTypeClass = myContext.getResourceDefinition(myResourceType).getImplementingClass(); - return mySearchBuilderFactory.newSearchBuilder(myCallingDao, myResourceType, resourceTypeClass); + return mySearchBuilderFactory.newSearchBuilder(myResourceType, resourceTypeClass); } @Nonnull @@ -280,7 +280,7 @@ public class SearchTask implements Callable { .withRequest(myRequest) .withRequestPartitionId(myRequestPartitionId) .withPropagation(Propagation.REQUIRES_NEW) - .execute(() -> doSaveSearch()); + .execute(this::doSaveSearch); } @SuppressWarnings("rawtypes") @@ -307,8 +307,7 @@ public class SearchTask implements Callable { .add(RequestDetails.class, mySearchRuntimeDetails.getRequestDetails()) .addIfMatchesType( ServletRequestDetails.class, mySearchRuntimeDetails.getRequestDetails()); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); + myCompositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); for (int i = unsyncedPids.size() - 1; i >= 0; i--) { if (accessDetails.isDontReturnResourceAtIndex(i)) { @@ -454,15 +453,13 @@ public class SearchTask implements Callable { .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest) .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_COMPLETE, params); + myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_COMPLETE, params); } else { HookParams params = new HookParams() .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest) .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_PASS_COMPLETE, params); + myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_PASS_COMPLETE, params); } ourLog.trace( @@ -516,8 +513,7 @@ public class SearchTask implements Callable { .add(RequestDetails.class, myRequest) .addIfMatchesType(ServletRequestDetails.class, myRequest) .add(SearchRuntimeDetails.class, mySearchRuntimeDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FAILED, params); + myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FAILED, params); saveSearch(); span.captureException(t); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessorTest.java index d5900c83e02..b04c8c18900 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/export/svc/JpaBulkExportProcessorTest.java @@ -214,9 +214,7 @@ public class JpaBulkExportProcessorTest { when(myBulkExportHelperService.createSearchParameterMapsForResourceType(any(RuntimeResourceDefinition.class), eq(parameters), any(boolean.class))) .thenReturn(maps); // from getSearchBuilderForLocalResourceType - when(myDaoRegistry.getResourceDao(anyString())) - .thenReturn(mockDao); - when(mySearchBuilderFactory.newSearchBuilder(eq(mockDao), eq(parameters.getResourceType()), any())) + when(mySearchBuilderFactory.newSearchBuilder(eq(parameters.getResourceType()), any())) .thenReturn(searchBuilder); // ret when(searchBuilder.createQuery( @@ -304,9 +302,7 @@ public class JpaBulkExportProcessorTest { when(myBulkExportHelperService.createSearchParameterMapsForResourceType(any(RuntimeResourceDefinition.class), eq(parameters), any(boolean.class))) .thenReturn(Collections.singletonList(new SearchParameterMap())); // from getSearchBuilderForLocalResourceType - when(myDaoRegistry.getResourceDao(not(eq("Group")))) - .thenReturn(mockDao); - when(mySearchBuilderFactory.newSearchBuilder(eq(mockDao), eq(parameters.getResourceType()), any())) + when(mySearchBuilderFactory.newSearchBuilder(eq(parameters.getResourceType()), any())) .thenReturn(searchBuilder); // ret when(searchBuilder.createQuery( @@ -432,9 +428,7 @@ public class JpaBulkExportProcessorTest { when(myIdHelperService.getPidOrNull(eq(getPartitionIdFromParams(thePartitioned)), eq(groupResource))) .thenReturn(groupId); // getMembersFromGroupWithFilter - when(myDaoRegistry.getResourceDao(eq("Patient"))) - .thenReturn(patientDao); - when(mySearchBuilderFactory.newSearchBuilder(eq(patientDao), eq("Patient"), eq(Patient.class))) + when(mySearchBuilderFactory.newSearchBuilder(eq("Patient"), eq(Patient.class))) .thenReturn(patientSearchBuilder); RuntimeResourceDefinition patientDef = myFhirContext.getResourceDefinition("Patient"); SearchParameterMap patientSpMap = new SearchParameterMap(); @@ -447,9 +441,7 @@ public class JpaBulkExportProcessorTest { RuntimeResourceDefinition observationDef = myFhirContext.getResourceDefinition("Observation"); when(myBulkExportHelperService.createSearchParameterMapsForResourceType(eq(observationDef), eq(parameters), any(boolean.class))) .thenReturn(Collections.singletonList(observationSpMap)); - when(myDaoRegistry.getResourceDao((eq("Observation")))) - .thenReturn(observationDao); - when(mySearchBuilderFactory.newSearchBuilder(eq(observationDao), eq("Observation"), eq(Observation.class))) + when(mySearchBuilderFactory.newSearchBuilder(eq("Observation"), eq(Observation.class))) .thenReturn(observationSearchBuilder); when(observationSearchBuilder.loadIncludes( any(SearchBuilderLoadIncludesParameters.class) @@ -520,10 +512,7 @@ public class JpaBulkExportProcessorTest { any(ExportPIDIteratorParameters.class), any(boolean.class) )).thenReturn(Collections.singletonList(new SearchParameterMap())); - when(myDaoRegistry.getResourceDao(eq("Patient"))) - .thenReturn(dao); when(mySearchBuilderFactory.newSearchBuilder( - any(IFhirResourceDao.class), anyString(), any() )).thenReturn(searchBuilder); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java index a7903eaf03a..12990fb07c4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -941,8 +941,9 @@ public class SearchParamExtractorService { if (myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) { // Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, myInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE)) { CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails( theRequestPartitionId, theSourceResourceName, @@ -950,12 +951,8 @@ public class SearchParamExtractorService { theRequest, theTransactionDetails); HookParams params = new HookParams(referenceDetails); - targetResource = - (IResourceLookup) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( - myInterceptorBroadcaster, - theRequest, - Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, - params); + targetResource = (IResourceLookup) compositeBroadcaster.callHooksAndReturnObject( + Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, params); } else { targetResource = myResourceLinkResolver.findTargetResource( RequestPartitionId.allPartitions(), @@ -1089,8 +1086,9 @@ public class SearchParamExtractorService { } // If extraction generated any warnings, broadcast an error - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_WARNING, theInterceptorBroadcaster, theRequestDetails)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { for (String next : theSearchParamSet.getWarnings()) { StorageProcessingMessage messageHolder = new StorageProcessingMessage(); messageHolder.setMessage(next); @@ -1098,8 +1096,7 @@ public class SearchParamExtractorService { .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .add(StorageProcessingMessage.class, messageHolder); - CompositeInterceptorBroadcaster.doCallHooks( - theInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_WARNING, params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); } } } diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorServiceTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorServiceTest.java index 5472ff8d328..bf2ccb56591 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorServiceTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorServiceTest.java @@ -1,14 +1,20 @@ package ca.uhn.fhir.jpa.searchparam.extractor; +import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.MockInvoker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; @@ -36,14 +42,17 @@ public class SearchParamExtractorServiceTest { searchParamSet.addWarning("help i'm a bug"); searchParamSet.addWarning("Spiff"); - when(myJpaInterceptorBroadcaster.hasHooks(any())).thenReturn(true); - when(myJpaInterceptorBroadcaster.callHooks(any(), any())).thenReturn(true); + AtomicInteger counter = new AtomicInteger(); + + when(myJpaInterceptorBroadcaster.hasHooks(eq(Pointcut.JPA_PERFTRACE_WARNING))).thenReturn(true); + when(myJpaInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.JPA_PERFTRACE_WARNING))).thenReturn(MockInvoker.list((Consumer) params->counter.incrementAndGet())); ServletRequestDetails requestDetails = new ServletRequestDetails(myRequestInterceptorBroadcaster); SearchParamExtractorService.handleWarnings(requestDetails, myJpaInterceptorBroadcaster, searchParamSet); - verify(myJpaInterceptorBroadcaster, times(2)).callHooks(eq(Pointcut.JPA_PERFTRACE_WARNING), any()); - verify(myRequestInterceptorBroadcaster, times(2)).callHooks(eq(Pointcut.JPA_PERFTRACE_WARNING), any()); + verify(myJpaInterceptorBroadcaster, times(3)).hasHooks(eq(Pointcut.JPA_PERFTRACE_WARNING)); + verify(myRequestInterceptorBroadcaster, times(2)).hasHooks(eq(Pointcut.JPA_PERFTRACE_WARNING)); + assertEquals(2, counter.get()); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java index f8d8c11b02e..4e72a7fd65d 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java @@ -118,12 +118,15 @@ public class SubscriptionMatcherInterceptor { ResourceModifiedMessage msg = createResourceModifiedMessage(theNewResource, theOperationType, theRequest); // Interceptor call: SUBSCRIPTION_RESOURCE_MODIFIED - HookParams params = new HookParams().add(ResourceModifiedMessage.class, msg); - boolean outcome = CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED)) { + HookParams params = new HookParams().add(ResourceModifiedMessage.class, msg); + boolean outcome = compositeBroadcaster.callHooks(Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED, params); - if (!outcome) { - return; + if (!outcome) { + return; + } } processResourceModifiedMessage(msg); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java index 2a712297a4d..531a0d47e01 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/triggering/SubscriptionTriggeringSvcImpl.java @@ -383,8 +383,8 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc String resourceType = myFhirContext.getResourceType(theJobDetails.getCurrentSearchResourceType()); RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theJobDetails.getCurrentSearchResourceType()); - ISearchBuilder searchBuilder = mySearchBuilderFactory.newSearchBuilder( - resourceDao, resourceType, resourceDef.getImplementingClass()); + ISearchBuilder searchBuilder = + mySearchBuilderFactory.newSearchBuilder(resourceType, resourceDef.getImplementingClass()); List listToPopulate = new ArrayList<>(); myTransactionService.withRequest(null).execute(() -> { diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/SubscriptionsDstu2Test.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/SubscriptionsDstu2Test.java index 30bfbd0e10d..a7fce70959d 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/SubscriptionsDstu2Test.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/provider/SubscriptionsDstu2Test.java @@ -113,7 +113,7 @@ public class SubscriptionsDstu2Test extends BaseResourceProviderDstu2Test { myClient.create().resource(subs).execute(); fail(""); } catch (UnprocessableEntityException e) { - assertThat(e.getMessage()).contains("Unknown SubscriptionStatus code 'aaaaa'"); + assertThat(e.getMessage()).containsAnyOf("invalid value aaaaa", "Unknown SubscriptionStatus code 'aaaaa'"); } } diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java index 46f8fd95af4..5adadc364cf 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java @@ -52,7 +52,7 @@ public class BaseSearchSvc { protected static final FhirContext ourCtx = FhirContext.forDstu3Cached(); public void after() { - verify(mySearchBuilderFactory, atMost(myExpectedNumberOfSearchBuildersCreated)).newSearchBuilder(any(), any(), any()); + verify(mySearchBuilderFactory, atMost(myExpectedNumberOfSearchBuildersCreated)).newSearchBuilder(any(), any()); } protected List createPidSequence(int to) { diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java index d48de17a9d5..5d400d11539 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImplTest.java @@ -76,6 +76,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked"}) @@ -91,7 +92,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { @Mock private ISearchResultCacheSvc mySearchResultCacheSvc; private Search myCurrentSearch; - @Mock + @Mock(strictness = Mock.Strictness.STRICT_STUBS) private IInterceptorBroadcaster myInterceptorBroadcaster; @Mock private SearchBuilderFactory mySearchBuilderFactory; @@ -289,7 +290,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { } private void initSearches() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); } private void initAsyncSearches() { @@ -318,8 +319,8 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { SlowIterator iter = new SlowIterator(pids.iterator(), 500); when(mySearchBuilder.createQuery(same(params), any(), any(), nullable(RequestPartitionId.class))).thenReturn(iter); mockSearchTask(); - when(myInterceptorBroadcaster.callHooks(any(), any())) - .thenReturn(true); + when(myInterceptorBroadcaster.hasHooks(any())).thenReturn(true); + when(myInterceptorBroadcaster.getInvokersForPointcut(any())).thenReturn(List.of()); ourLog.info("Registering the first search"); new Thread(() -> mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null, RequestPartitionId.allPartitions())).start(); @@ -437,7 +438,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { @Test public void testLoadSearchResultsFromDifferentCoordinator() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); final String uuid = UUID.randomUUID().toString(); @@ -517,7 +518,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { @Test public void testSynchronousSearch() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronous(true); @@ -531,7 +532,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { @Test public void testSynchronousSearchWithOffset() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); SearchParameterMap params = new SearchParameterMap(); params.setOffset(10); @@ -544,7 +545,7 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { @Test public void testSynchronousSearchUpTo() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); int loadUpto = 30; SearchParameterMap params = new SearchParameterMap(); @@ -584,7 +585,6 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc { @Test public void testFetchAllResultsReturnsNull() { when(myDaoRegistry.getResourceDao(anyString())).thenReturn(myCallingDao); - when(myCallingDao.getContext()).thenReturn(ourCtx); Search search = new Search(); search.setUuid("0000-1111"); diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImplTest.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImplTest.java index 993e39257ae..3f8e511b823 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImplTest.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/SynchronousSearchSvcImplTest.java @@ -41,7 +41,7 @@ public class SynchronousSearchSvcImplTest extends BaseSearchSvc { @Test public void testSynchronousSearch() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())) + when(mySearchBuilderFactory.newSearchBuilder(any(), any())) .thenReturn(mySearchBuilder); SearchParameterMap params = new SearchParameterMap(); @@ -65,7 +65,7 @@ public class SynchronousSearchSvcImplTest extends BaseSearchSvc { @Test public void testSynchronousSearchWithOffset() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); SearchParameterMap params = new SearchParameterMap(); params.setCount(10); @@ -87,7 +87,7 @@ public class SynchronousSearchSvcImplTest extends BaseSearchSvc { @Test public void testSynchronousSearchUpTo() { - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(mySearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(mySearchBuilder); when(myStorageSettings.getDefaultTotalMode()).thenReturn(null); SearchParameterMap params = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu2Test.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu2Test.java index c5b302b6cb1..d1ff5745201 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu2Test.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplDstu2Test.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; public class TerminologySvcImplDstu2Test extends BaseJpaDstu2Test { @@ -29,6 +30,8 @@ public class TerminologySvcImplDstu2Test extends BaseJpaDstu2Test { List concepts; Set codes; + when(mySrd.getInterceptorBroadcaster()).thenReturn(null); + ValueSet upload = new ValueSet(); upload.setId(new IdDt("testVs")); upload.setUrl("http://myVs"); @@ -61,6 +64,8 @@ public class TerminologySvcImplDstu2Test extends BaseJpaDstu2Test { List concepts; Set codes; + when(mySrd.getInterceptorBroadcaster()).thenReturn(null); + ValueSet upload = new ValueSet(); upload.setId(new IdDt("testVs")); upload.setUrl("http://myVs"); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR4Test.java index f1873c35179..4d7d955f602 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR4Test.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; import ca.uhn.fhir.test.utilities.HttpClientExtension; +import ca.uhn.fhir.test.utilities.MockInvoker; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.JsonUtil; import ca.uhn.fhir.util.SearchParameterUtil; @@ -1054,18 +1055,18 @@ public class BulkDataExportProviderR4Test { AtomicBoolean initiateCalled = new AtomicBoolean(false); // when - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT), any(HookParams.class))) - .thenAnswer((args) -> { + when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT))).thenReturn(true); + when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.STORAGE_INITIATE_BULK_EXPORT))).thenReturn(true); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT))).thenReturn(MockInvoker.list(params->{ assertFalse(initiateCalled.get()); assertFalse(preInitiateCalled.getAndSet(true)); return true; - }); - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_INITIATE_BULK_EXPORT), any(HookParams.class))) - .thenAnswer((args) -> { + })); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.STORAGE_INITIATE_BULK_EXPORT))).thenReturn(MockInvoker.list(params->{ assertTrue(preInitiateCalled.get()); assertFalse(initiateCalled.getAndSet(true)); return true; - }); + })); when(myJobCoordinator.startInstance(isNotNull(), any())) .thenReturn(createJobStartResponse()); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR5Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR5Test.java index 62bbeb742da..61c4a9fd48c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR5Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderR5Test.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy; import ca.uhn.fhir.test.utilities.HttpClientExtension; +import ca.uhn.fhir.test.utilities.MockInvoker; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.JsonUtil; import ca.uhn.fhir.util.SearchParameterUtil; @@ -1057,18 +1058,18 @@ public class BulkDataExportProviderR5Test { AtomicBoolean initiateCalled = new AtomicBoolean(false); // when - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT), any(HookParams.class))) - .thenAnswer((args) -> { + when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT))).thenReturn(true); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT))).thenReturn(MockInvoker.list(params -> { assertFalse(initiateCalled.get()); assertFalse(preInitiateCalled.getAndSet(true)); return true; - }); - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_INITIATE_BULK_EXPORT), any(HookParams.class))) - .thenAnswer((args) -> { + })); + when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.STORAGE_INITIATE_BULK_EXPORT))).thenReturn(true); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.STORAGE_INITIATE_BULK_EXPORT))).thenReturn(MockInvoker.list(params -> { assertTrue(preInitiateCalled.get()); assertFalse(initiateCalled.getAndSet(true)); return true; - }); + })); when(myJobCoordinator.startInstance(isNotNull(), any())) .thenReturn(createJobStartResponse()); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java index c423a70915b..6a6cada087a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java @@ -320,7 +320,7 @@ class BaseHapiFhirResourceDaoTest { mySvc.setTransactionService(myTransactionService); when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(any(), any(), any(), any())).thenReturn(mock(RequestPartitionId.class)); - when(mySearchBuilderFactory.newSearchBuilder(any(), any(), any())).thenReturn(myISearchBuilder); + when(mySearchBuilderFactory.newSearchBuilder(any(), any())).thenReturn(myISearchBuilder); when(myISearchBuilder.createQuery(any(), any(), any(), any())).thenReturn(mock(IResultIterator.class)); lenient().when(myStorageSettings.getInternalSynchronousSearchSize()).thenReturn(5000); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java index 2eb8a60aa2e..5b50aa855fc 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseComboParamsR4Test.java @@ -1,6 +1,5 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; @@ -10,9 +9,9 @@ import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.jpa.util.SpringObjectCaster; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.test.utilities.MockInvoker; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.mockito.ArgumentMatchers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -47,24 +46,22 @@ public abstract class BaseComboParamsR4Test extends BaseJpaR4Test { when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.JPA_PERFTRACE_WARNING))).thenReturn(true); when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.JPA_PERFTRACE_INFO))).thenReturn(true); - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.JPA_PERFTRACE_INFO), ArgumentMatchers.any(HookParams.class))).thenAnswer(t -> { - HookParams params = t.getArgument(1, HookParams.class); + when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.JPA_PERFTRACE_SEARCH_REUSING_CACHED))).thenReturn(true); + when(myInterceptorBroadcaster.hasHooks(eq(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH))).thenReturn(true); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.JPA_PERFTRACE_INFO))).thenReturn(MockInvoker.list(params->{ myMessages.add("INFO " + params.get(StorageProcessingMessage.class).getMessage()); - return null; - }); - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.JPA_PERFTRACE_WARNING), ArgumentMatchers.any(HookParams.class))).thenAnswer(t -> { - HookParams params = t.getArgument(1, HookParams.class); + })); + + + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.JPA_PERFTRACE_WARNING))).thenReturn(MockInvoker.list(params->{ myMessages.add("WARN " + params.get(StorageProcessingMessage.class).getMessage()); - return null; - }); - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.JPA_PERFTRACE_SEARCH_REUSING_CACHED), ArgumentMatchers.any(HookParams.class))).thenAnswer(t -> { - HookParams params = t.getArgument(1, HookParams.class); + })); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.JPA_PERFTRACE_SEARCH_REUSING_CACHED))).thenReturn(MockInvoker.list(params->{ myMessages.add("REUSING CACHED SEARCH"); - return null; - }); + })); // allow searches to use cached results - when(myInterceptorBroadcaster.callHooks(eq(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH), ArgumentMatchers.any(HookParams.class))).thenReturn(true); + when(myInterceptorBroadcaster.getInvokersForPointcut(eq(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH))).thenReturn(MockInvoker.list(params->true)); } @AfterEach @@ -83,4 +80,5 @@ public abstract class BaseComboParamsR4Test extends BaseJpaR4Test { ourLog.info("Messages:\n {}", String.join("\n ", myMessages)); } + } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java index 2b635c81084..fa37a2c4f49 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/interceptor/MdmReadVirtualizationInterceptor.java @@ -90,7 +90,7 @@ public class MdmReadVirtualizationInterceptor

    @Hook( value = Pointcut.STORAGE_PRESEARCH_REGISTERED, order = MdmConstants.ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR) - public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { + public void preSearchRegistered(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { ourMdmTroubleshootingLog .atTrace() .setMessage("MDM virtualization original search: {}{}") diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java index adc993511fc..6b2d141d580 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchExpansionSvc.java @@ -85,14 +85,15 @@ public class MdmSearchExpansionSvc { // Try to detect if the RequestDetails is being reused across multiple different queries, which // can happen during CQL measure evaluation - String resourceName = theRequestDetails.getResourceName(); - String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext); - if (!Objects.equals(resourceName, theRequestDetails.getUserData().get(RESOURCE_NAME)) - || !Objects.equals(queryString, theRequestDetails.getUserData().get(QUERY_STRING))) { - theRequestDetails.getUserData().remove(EXPANSION_RESULTS); + { + String resourceName = theRequestDetails.getResourceName(); + String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext); + if (!Objects.equals(resourceName, theRequestDetails.getUserData().get(RESOURCE_NAME)) + || !Objects.equals( + queryString, theRequestDetails.getUserData().get(QUERY_STRING))) { + theRequestDetails.getUserData().remove(EXPANSION_RESULTS); + } } - theRequestDetails.getUserData().put(RESOURCE_NAME, resourceName); - theRequestDetails.getUserData().put(QUERY_STRING, queryString); MdmSearchExpansionResults expansionResults = getCachedExpansionResults(theRequestDetails); if (expansionResults != null) { @@ -123,6 +124,15 @@ public class MdmSearchExpansionSvc { theRequestDetails.getUserData().put(EXPANSION_RESULTS, expansionResults); + /* + * Note: Do this at the end so that the query string reflects the post-translated + * query string + */ + String resourceName = theRequestDetails.getResourceName(); + String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext); + theRequestDetails.getUserData().put(RESOURCE_NAME, resourceName); + theRequestDetails.getUserData().put(QUERY_STRING, queryString); + return expansionResults; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchParamSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchParamSvc.java index 18043f0687c..483614e4d56 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchParamSvc.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/svc/MdmSearchParamSvc.java @@ -92,7 +92,7 @@ public class MdmSearchParamSvc { public ISearchBuilder generateSearchBuilderForType(String theSourceType) { IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theSourceType); - return mySearchBuilderFactory.newSearchBuilder(resourceDao, theSourceType, resourceDao.getResourceType()); + return mySearchBuilderFactory.newSearchBuilder(theSourceType, resourceDao.getResourceType()); } /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java index 97b2f4f6657..46e14d53476 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java @@ -41,6 +41,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.charset.Charset; +import java.util.Collections; import java.util.List; import static java.util.Objects.nonNull; @@ -241,6 +242,11 @@ public class SystemRequestDetails extends RequestDetails { public boolean hasHooks(Pointcut thePointcut) { return false; } + + @Override + public List getInvokersForPointcut(Pointcut thePointcut) { + return Collections.emptyList(); + } } public static SystemRequestDetails forAllPartitions() { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerInterceptorUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerInterceptorUtil.java index 71f8e74892a..6e049949b16 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerInterceptorUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ServerInterceptorUtil.java @@ -54,17 +54,20 @@ public class ServerInterceptorUtil { // Interceptor call: STORAGE_PRESHOW_RESOURCE // This can be used to remove results from the search result details before // the user has a chance to know that they were in the results - if (retVal.size() > 0) { - SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(retVal); - HookParams params = new HookParams() - .add(IPreResourceShowDetails.class, accessDetails) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - theInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); + if (!retVal.isEmpty()) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { + SimplePreResourceShowDetails accessDetails = new SimplePreResourceShowDetails(retVal); + HookParams params = new HookParams() + .add(IPreResourceShowDetails.class, accessDetails) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); - retVal = accessDetails.toList(); - retVal.removeIf(Objects::isNull); + retVal = accessDetails.toList(); + retVal.removeIf(Objects::isNull); + } } return retVal; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java index 6670ab15a4c..440665d4edc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcaster.java @@ -19,97 +19,105 @@ */ package ca.uhn.fhir.rest.server.util; +import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.interceptor.executor.BaseInterceptorService; import ca.uhn.fhir.rest.api.server.RequestDetails; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -public class CompositeInterceptorBroadcaster { +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + +/** + * This is an {@link IInterceptorBroadcaster} which combines multiple interceptor + * broadcasters. Hook methods are called across all broadcasters, respecting + * the {@link Hook#order()} across all broadcasters. + */ +public class CompositeInterceptorBroadcaster implements IInterceptorBroadcaster { + + private final List myServices; /** - * Non instantiable + * Constructor */ - private CompositeInterceptorBroadcaster() { - // nothing + private CompositeInterceptorBroadcaster(Collection theServices) { + myServices = theServices.stream().filter(t -> t != null).collect(Collectors.toList()); + } + + @Override + public boolean callHooks(Pointcut thePointcut, HookParams theParams) { + assert BaseInterceptorService.haveAppropriateParams(thePointcut, theParams); + assert thePointcut.getReturnType() == void.class + || thePointcut.getReturnType() == thePointcut.getBooleanReturnTypeForEnum(); + + List invokers = getInvokersForPointcut(thePointcut); + Object retVal = BaseInterceptorService.callInvokers(thePointcut, theParams, invokers); + retVal = defaultIfNull(retVal, true); + return (Boolean) retVal; + } + + @Override + public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) { + assert BaseInterceptorService.haveAppropriateParams(thePointcut, theParams); + assert thePointcut.getReturnType() != void.class; + + List invokers = getInvokersForPointcut(thePointcut); + return BaseInterceptorService.callInvokers(thePointcut, theParams, invokers); + } + + @Override + @Nonnull + public List getInvokersForPointcut(Pointcut thePointcut) { + List invokers = new ArrayList<>(); + for (IInterceptorBroadcaster services : myServices) { + if (services.hasHooks(thePointcut)) { + List serviceInvokers = services.getInvokersForPointcut(thePointcut); + assert serviceInvokers != null; + invokers.addAll(serviceInvokers); + } + } + invokers.sort(Comparator.naturalOrder()); + return invokers; + } + + @Override + public boolean hasHooks(Pointcut thePointcut) { + for (IInterceptorBroadcaster service : myServices) { + if (service.hasHooks(thePointcut)) { + return true; + } + } + return false; } /** - * Broadcast hooks to both the interceptor service associated with the request, as well - * as the one associated with the JPA module. + * @since 8.0.0 */ - public static boolean doCallHooks( - IInterceptorBroadcaster theInterceptorBroadcaster, - @Nullable RequestDetails theRequestDetails, - Pointcut thePointcut, - HookParams theParams) { - return newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails) - .callHooks(thePointcut, theParams); - } - - /** - * Broadcast hooks to both the interceptor service associated with the request, as well - * as the one associated with the JPA module. - */ - public static Object doCallHooksAndReturnObject( - IInterceptorBroadcaster theInterceptorBroadcaster, - RequestDetails theRequestDetails, - Pointcut thePointcut, - HookParams theParams) { - return newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails) - .callHooksAndReturnObject(thePointcut, theParams); - } - - // TODO: JA - Refactor to make thePointcut the last argument in order to be consistent with thr other methods here - public static boolean hasHooks( - Pointcut thePointcut, IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequestDetails) { - return newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails) - .hasHooks(thePointcut); + public static IInterceptorBroadcaster newCompositeBroadcaster(IInterceptorBroadcaster... theServices) { + return new CompositeInterceptorBroadcaster(Arrays.asList(theServices)); } /** * @since 5.5.0 */ public static IInterceptorBroadcaster newCompositeBroadcaster( - IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequestDetails) { - return new IInterceptorBroadcaster() { - @Override - public boolean callHooks(Pointcut thePointcut, HookParams theParams) { - boolean retVal = true; - if (theInterceptorBroadcaster != null) { - retVal = theInterceptorBroadcaster.callHooks(thePointcut, theParams); - } - if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null && retVal) { - IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); - retVal = interceptorBroadcaster.callHooks(thePointcut, theParams); - } - return retVal; + @Nonnull IInterceptorBroadcaster theInterceptorBroadcaster, @Nullable RequestDetails theRequestDetails) { + if (theRequestDetails != null) { + IInterceptorBroadcaster requestBroadcaster = theRequestDetails.getInterceptorBroadcaster(); + if (requestBroadcaster != null) { + return newCompositeBroadcaster(theInterceptorBroadcaster, requestBroadcaster); } + } - @Override - public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) { - Object retVal = true; - if (theInterceptorBroadcaster != null) { - retVal = theInterceptorBroadcaster.callHooksAndReturnObject(thePointcut, theParams); - } - if (theRequestDetails != null - && theRequestDetails.getInterceptorBroadcaster() != null - && retVal == null) { - IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); - retVal = interceptorBroadcaster.callHooksAndReturnObject(thePointcut, theParams); - } - return retVal; - } - - @Override - public boolean hasHooks(Pointcut thePointcut) { - if (theInterceptorBroadcaster != null && theInterceptorBroadcaster.hasHooks(thePointcut)) { - return true; - } - return theRequestDetails != null - && theRequestDetails.getInterceptorBroadcaster() != null - && theRequestDetails.getInterceptorBroadcaster().hasHooks(thePointcut); - } - }; + return newCompositeBroadcaster(theInterceptorBroadcaster); } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java index 2a670d15811..ca075e86fa7 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/util/CompositeInterceptorBroadcasterTest.java @@ -1,161 +1,314 @@ package ca.uhn.fhir.rest.server.util; +import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.interceptor.executor.InterceptorService; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import com.google.common.collect.MultimapBuilder; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.ArrayList; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@SuppressWarnings("unchecked") @ExtendWith(MockitoExtension.class) class CompositeInterceptorBroadcasterTest { + @SuppressWarnings("rawtypes") + private static final Class BOOLEAN_CLASS = boolean.class; + private final List myOrders = new ArrayList<>(); @Mock private IInterceptorBroadcaster myModuleBroadcasterMock; @Mock private IInterceptorBroadcaster myReqDetailsBroadcasterMock; @Mock + private IBaseInterceptorBroadcaster.IInvoker myModuleBroadcasterInvokerMock; + @Mock + private IBaseInterceptorBroadcaster.IInvoker myReqDetailsInvokerMock; + @Mock private Pointcut myPointcutMock; @Mock private HookParams myHookParamsMock; @Mock private RequestDetails myRequestDetailsMock; - @Test void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_RequestDetailsBroadcasterReturnsTrue_ThenReturnsTrue() { when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); - when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myReqDetailsBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myReqDetailsBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myReqDetailsInvokerMock)); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); + when(myModuleBroadcasterInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(true); + when(myReqDetailsInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(true); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, - myPointcutMock, myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, myRequestDetailsMock); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isTrue(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); - verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock, times(1)).invoke(eq(myHookParamsMock)); + verify(myReqDetailsInvokerMock, times(1)).invoke(eq(myHookParamsMock)); } + @SuppressWarnings("unchecked") @Test void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_RequestDetailsBroadcasterReturnsFalse_ThenReturnsFalse() { when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); - when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myReqDetailsBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myReqDetailsBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myReqDetailsInvokerMock)); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); + when(myModuleBroadcasterInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(true); + when(myReqDetailsInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(false); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, - myPointcutMock, myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, myRequestDetailsMock); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isFalse(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); - verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock).invoke(eq(myHookParamsMock)); + verify(myReqDetailsInvokerMock).invoke(eq(myHookParamsMock)); } @Test void doCallHooks_WhenModuleBroadcasterReturnsFalse_ThenSkipsBroadcasterInRequestDetails_And_ReturnsFalse() { when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myReqDetailsBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myReqDetailsBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myReqDetailsInvokerMock)); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); + when(myModuleBroadcasterInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(false); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, - myPointcutMock, myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, myRequestDetailsMock); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isFalse(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); - verify(myReqDetailsBroadcasterMock, never()).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock, times(1)).invoke(eq(myHookParamsMock)); + verify(myReqDetailsInvokerMock, never()).invoke(eq(myHookParamsMock)); } @Test void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_NullRequestDetailsBroadcaster_ThenReturnsTrue() { - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); - when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(null); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, myPointcutMock, - myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, myRequestDetailsMock); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isTrue(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock, times(1)).invoke(eq(myHookParamsMock)); + verify(myReqDetailsInvokerMock, never()).invoke(eq(myHookParamsMock)); } + @SuppressWarnings("unchecked") @Test void doCallHooks_WhenModuleBroadcasterReturnsFalse_And_NullRequestDetailsBroadcaster_ThenReturnsFalse() { - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); - when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(null); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); + when(myModuleBroadcasterInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(false); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, myRequestDetailsMock, myPointcutMock, - myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, myRequestDetailsMock); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isFalse(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock, times(1)).invoke(eq(myHookParamsMock)); } + @SuppressWarnings("unchecked") @Test void doCallHooks_WhenModuleBroadcasterReturnsTrue_And_NullRequestDetails_ThenReturnsTrue() { - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, null, myPointcutMock, myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, null); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isTrue(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock, times(1)).invoke(eq(myHookParamsMock)); + verify(myReqDetailsInvokerMock, never()).invoke(eq(myHookParamsMock)); } + @SuppressWarnings("unchecked") @Test void doCallHooks_WhenModuleBroadcasterReturnsFalse_And_NullRequestDetails_ThenReturnsFalse() { - when(myModuleBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + when(myPointcutMock.getReturnType()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getBooleanReturnTypeForEnum()).thenReturn(BOOLEAN_CLASS); + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); + when(myModuleBroadcasterMock.hasHooks(eq(myPointcutMock))).thenReturn(true); + when(myModuleBroadcasterMock.getInvokersForPointcut(eq(myPointcutMock))).thenReturn(List.of(myModuleBroadcasterInvokerMock)); + when(myModuleBroadcasterInvokerMock.invoke(eq(myHookParamsMock))).thenReturn(false); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(myModuleBroadcasterMock, null, myPointcutMock, myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(myModuleBroadcasterMock, null); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isFalse(); - verify(myModuleBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + verify(myModuleBroadcasterInvokerMock).invoke(eq(myHookParamsMock)); } @Test void doCallHooks_WhenNullModuleBroadcaster_And_NullRequestDetails_ThenReturnsTrue() { + when(myPointcutMock.getParameterTypes()).thenReturn(List.of()); + when(myHookParamsMock.getParamsForType()).thenReturn(MultimapBuilder.hashKeys().arrayListValues().build()); - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, null, myPointcutMock, myHookParamsMock); + IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster + .newCompositeBroadcaster(); + boolean retVal = interceptorBroadcaster.callHooks(myPointcutMock, myHookParamsMock); assertThat(retVal).isTrue(); } - @Test - void doCallHooks_WhenNullModuleBroadcaster_And_RequestDetailsBroadcasterReturnsTrue_ThenReturnsTrue() { - when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); - when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(true); + @ParameterizedTest + @CsvSource({ + "0, 1, 2", + "1, 0, 2", + "2, 0, 1" + }) + public void testCompositeBroadcasterBroadcastsInOrder(int theIndex0, int theIndex1, int theIndex2) { + // Setup + InterceptorService svc0 = new InterceptorService(); + svc0.registerInterceptor(new Interceptor0()); + InterceptorService svc1 = new InterceptorService(); + svc1.registerInterceptor(new Interceptor1()); + InterceptorService svc2 = new InterceptorService(); + svc2.registerInterceptor(new Interceptor2()); + InterceptorService[] services = new InterceptorService[]{svc0, svc1, svc2}; - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, myRequestDetailsMock, myPointcutMock, myHookParamsMock); + List serviceList = List.of( + services[theIndex0], services[theIndex1], services[theIndex2] + ); + IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster(serviceList.toArray(new IInterceptorBroadcaster[0])); - assertThat(retVal).isTrue(); - verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); + assertTrue(compositeBroadcaster.hasHooks(Pointcut.TEST_RO)); + assertFalse(compositeBroadcaster.hasHooks(Pointcut.TEST_RB)); + + // Test + HookParams hookParams = new HookParams() + .add(String.class, "PARAM_A") + .add(String.class, "PARAM_B"); + Object outcome = compositeBroadcaster.callHooksAndReturnObject(Pointcut.TEST_RO, hookParams); + + // Verify + assertNull(outcome); + assertThat(myOrders).asList().containsExactly( + -2, -1, 0, 1, 2, 3 + ); } - @Test - void doCallHooks_WhenNullModuleBroadcaster_And_RequestDetailsBroadcasterReturnsFalse_ThenReturnsFalse() { - when(myRequestDetailsMock.getInterceptorBroadcaster()).thenReturn(myReqDetailsBroadcasterMock); - when(myReqDetailsBroadcasterMock.callHooks(myPointcutMock, myHookParamsMock)).thenReturn(false); + @Interceptor + private class Interceptor0 { - boolean retVal = CompositeInterceptorBroadcaster.doCallHooks(null, myRequestDetailsMock, myPointcutMock, myHookParamsMock); + @Hook(value = Pointcut.TEST_RO, order = 0) + public BaseServerResponseException hook0() { + myOrders.add(0); + return null; + } + + @Hook(value = Pointcut.TEST_RO, order = 2) + public BaseServerResponseException hook2() { + myOrders.add(2); + return null; + } - assertThat(retVal).isFalse(); - verify(myReqDetailsBroadcasterMock).callHooks(myPointcutMock, myHookParamsMock); } + + @Interceptor + private class Interceptor1 { + + @Hook(value = Pointcut.TEST_RO, order = 1) + public BaseServerResponseException hook1() { + myOrders.add(1); + return null; + } + + @Hook(value = Pointcut.TEST_RO, order = 3) + public BaseServerResponseException hook3() { + myOrders.add(3); + return null; + } + + } + + @Interceptor + private class Interceptor2 { + + @Hook(value = Pointcut.TEST_RO, order = -1) + public BaseServerResponseException hookMinus1() { + myOrders.add(-1); + return null; + } + + @Hook(value = Pointcut.TEST_RO, order = -2) + public BaseServerResponseException hookMinus2() { + myOrders.add(-2); + return null; + } + + } + } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java index 002e21a0cf9..f9c28053da9 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkDataExportProvider.java @@ -173,15 +173,15 @@ public class BulkDataExportProvider { expandParameters(theRequestDetails, theOptions); // permission check - HookParams initiateBulkExportHookParams = (new HookParams()) - .add(BulkExportJobParameters.class, theOptions) - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - CompositeInterceptorBroadcaster.doCallHooks( - this.myInterceptorBroadcaster, - theRequestDetails, - Pointcut.STORAGE_INITIATE_BULK_EXPORT, - initiateBulkExportHookParams); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_INITIATE_BULK_EXPORT)) { + HookParams initiateBulkExportHookParams = (new HookParams()) + .add(BulkExportJobParameters.class, theOptions) + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); + compositeBroadcaster.callHooks(Pointcut.STORAGE_INITIATE_BULK_EXPORT, initiateBulkExportHookParams); + } // get cache boolean boolean useCache = shouldUseCache(theRequestDetails); @@ -220,15 +220,15 @@ public class BulkDataExportProvider { theOptions.setPartitionId(partitionId); // call hook so any other parameter manipulation can be done - HookParams preInitiateBulkExportHookParams = new HookParams(); - preInitiateBulkExportHookParams.add(BulkExportJobParameters.class, theOptions); - preInitiateBulkExportHookParams.add(RequestDetails.class, theRequestDetails); - preInitiateBulkExportHookParams.addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, - theRequestDetails, - Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT, - preInitiateBulkExportHookParams); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT)) { + HookParams preInitiateBulkExportHookParams = new HookParams(); + preInitiateBulkExportHookParams.add(BulkExportJobParameters.class, theOptions); + preInitiateBulkExportHookParams.add(RequestDetails.class, theRequestDetails); + preInitiateBulkExportHookParams.addIfMatchesType(ServletRequestDetails.class, theRequestDetails); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRE_INITIATE_BULK_EXPORT, preInitiateBulkExportHookParams); + } } private boolean shouldUseCache(ServletRequestDetails theRequestDetails) { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java index 526bacd71d5..93b1475e699 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/expunge/DeleteExpungeJobSubmitterImpl.java @@ -78,13 +78,16 @@ public class DeleteExpungeJobSubmitterImpl implements IDeleteExpungeJobSubmitter Msg.code(820) + "Delete Expunge not allowed: " + myStorageSettings.cannotDeleteExpungeReason()); } - for (String url : theUrlsToDeleteExpunge) { - HookParams params = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(String.class, url); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRE_DELETE_EXPUNGE, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRE_DELETE_EXPUNGE)) { + for (String url : theUrlsToDeleteExpunge) { + HookParams params = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(String.class, url); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRE_DELETE_EXPUNGE, params); + } } DeleteExpungeJobParameters deleteExpungeJobParameters = new DeleteExpungeJobParameters(); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/interceptor/BinaryStorageInterceptor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/interceptor/BinaryStorageInterceptor.java index 9d4ec9c7cf5..b549c71b6a3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/interceptor/BinaryStorageInterceptor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/interceptor/BinaryStorageInterceptor.java @@ -274,12 +274,15 @@ public class BinaryStorageInterceptor> { * @return A string, which will be used to prefix the blob ID. May be null. */ private String invokeAssignBinaryContentPrefix(RequestDetails theRequest, IBaseResource theResource) { - // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period - boolean hasStorageBinaryAssignBlobIdPrefixHooks = CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX, myInterceptorBroadcaster, theRequest); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); - boolean hasStorageBinaryAssignBinaryContentIdPrefixHooks = CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX, myInterceptorBroadcaster, theRequest); + // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period + boolean hasStorageBinaryAssignBlobIdPrefixHooks = + compositeBroadcaster.hasHooks(Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX); + + boolean hasStorageBinaryAssignBinaryContentIdPrefixHooks = + compositeBroadcaster.hasHooks(Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX); if (!(hasStorageBinaryAssignBlobIdPrefixHooks || hasStorageBinaryAssignBinaryContentIdPrefixHooks)) { return null; @@ -297,8 +300,7 @@ public class BinaryStorageInterceptor> { pointcutToInvoke = Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX; } - return (String) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( - myInterceptorBroadcaster, theRequest, pointcutToInvoke, params); + return (String) compositeBroadcaster.callHooksAndReturnObject(pointcutToInvoke, params); } @Nonnull diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/svc/BaseBinaryStorageSvcImpl.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/svc/BaseBinaryStorageSvcImpl.java index 97efb62d066..917f22b5edb 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/svc/BaseBinaryStorageSvcImpl.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/binary/svc/BaseBinaryStorageSvcImpl.java @@ -174,13 +174,16 @@ public abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { @Nullable private String callBinaryContentIdPointcut( byte[] theBytes, RequestDetails theRequestDetails, String theContentType) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + // TODO: to be removed when pointcut STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX has exceeded the grace period. // Deprecated in 7.2.0. - boolean hasStorageBinaryAssignBlobIdPrefixHooks = CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX, myInterceptorBroadcaster, theRequestDetails); + boolean hasStorageBinaryAssignBlobIdPrefixHooks = + compositeBroadcaster.hasHooks(Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX); - boolean hasStorageBinaryAssignBinaryContentIdPrefixHooks = CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX, myInterceptorBroadcaster, theRequestDetails); + boolean hasStorageBinaryAssignBinaryContentIdPrefixHooks = + compositeBroadcaster.hasHooks(Pointcut.STORAGE_BINARY_ASSIGN_BINARY_CONTENT_ID_PREFIX); if (!(hasStorageBinaryAssignBlobIdPrefixHooks || hasStorageBinaryAssignBinaryContentIdPrefixHooks)) { return null; @@ -201,8 +204,7 @@ public abstract class BaseBinaryStorageSvcImpl implements IBinaryStorageSvc { pointcutToInvoke = Pointcut.STORAGE_BINARY_ASSIGN_BLOB_ID_PREFIX; } - return (String) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( - myInterceptorBroadcaster, theRequestDetails, pointcutToInvoke, hookParams); + return (String) compositeBroadcaster.callHooksAndReturnObject(pointcutToInvoke, hookParams); } @Override diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java index 1f1660b16ea..554051f607c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java @@ -335,14 +335,17 @@ public abstract class BaseStorageDao { // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES if (outcome.getResource() != null) { SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource()); - HookParams params = new HookParams() - .add(IPreResourceAccessDetails.class, accessDetails) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); - if (accessDetails.isDontReturnResourceAtIndex(0)) { - outcome.setResource(null); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(getInterceptorBroadcaster(), theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { + HookParams params = new HookParams() + .add(IPreResourceAccessDetails.class, accessDetails) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); + if (accessDetails.isDontReturnResourceAtIndex(0)) { + outcome.setResource(null); + } } } @@ -352,14 +355,17 @@ public abstract class BaseStorageDao { // outcome.fireResourceViewCallback()) outcome.registerResourceViewCallback(() -> { if (outcome.getResource() != null) { - SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); - HookParams params = new HookParams() - .add(IPreResourceShowDetails.class, showDetails) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); - outcome.setResource(showDetails.getResource(0)); + IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( + getInterceptorBroadcaster(), theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { + SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); + HookParams params = new HookParams() + .add(IPreResourceShowDetails.class, showDetails) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); + outcome.setResource(showDetails.getResource(0)); + } } }); @@ -378,16 +384,19 @@ public abstract class BaseStorageDao { outcome.setEntitySupplierUseCallback(() -> { // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES if (outcome.getResource() != null) { - SimplePreResourceAccessDetails accessDetails = - new SimplePreResourceAccessDetails(outcome.getResource()); - HookParams params = new HookParams() - .add(IPreResourceAccessDetails.class, accessDetails) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); - if (accessDetails.isDontReturnResourceAtIndex(0)) { - outcome.setResource(null); + IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( + getInterceptorBroadcaster(), theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { + SimplePreResourceAccessDetails accessDetails = + new SimplePreResourceAccessDetails(outcome.getResource()); + HookParams params = new HookParams() + .add(IPreResourceAccessDetails.class, accessDetails) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); + if (accessDetails.isDontReturnResourceAtIndex(0)) { + outcome.setResource(null); + } } } @@ -397,14 +406,19 @@ public abstract class BaseStorageDao { // outcome.fireResourceViewCallback()) outcome.registerResourceViewCallback(() -> { if (outcome.getResource() != null) { - SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); - HookParams params = new HookParams() - .add(IPreResourceShowDetails.class, showDetails) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); - outcome.setResource(showDetails.getResource(0)); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + getInterceptorBroadcaster(), theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { + SimplePreResourceShowDetails showDetails = + new SimplePreResourceShowDetails(outcome.getResource()); + HookParams params = new HookParams() + .add(IPreResourceShowDetails.class, showDetails) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); + outcome.setResource(showDetails.getResource(0)); + } } }); }); @@ -420,8 +434,9 @@ public abstract class BaseStorageDao { if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) { theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams); } else { - CompositeInterceptorBroadcaster.doCallHooks( - getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams); + IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( + getInterceptorBroadcaster(), theRequestDetails); + compositeBroadcaster.callHooks(thePointcut, theParams); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java index fee8d73dd73..6705175d2e3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java @@ -359,14 +359,14 @@ public abstract class BaseTransactionProcessor { try { // Interceptor call: STORAGE_TRANSACTION_PROCESSING - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_TRANSACTION_PROCESSING, myInterceptorBroadcaster, theRequestDetails)) { + IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_PROCESSING)) { HookParams params = new HookParams() .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequest) .add(IBaseBundle.class, theRequest); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_TRANSACTION_PROCESSING, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_TRANSACTION_PROCESSING, params); } return processTransaction(theRequestDetails, theRequest, theActionName, theNestedMode); @@ -561,8 +561,9 @@ public abstract class BaseTransactionProcessor { theRequestDetails, response, getEntries, originalRequestOrder, transactionStopWatch, theNestedMode); // Interceptor broadcast: JPA_PERFTRACE_INFO - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { String taskDurations = transactionStopWatch.formatTaskDurations(); StorageProcessingMessage message = new StorageProcessingMessage(); message.setMessage("Transaction timing:\n" + taskDurations); @@ -570,8 +571,7 @@ public abstract class BaseTransactionProcessor { .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_INFO, params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); } return response; @@ -821,12 +821,10 @@ public abstract class BaseTransactionProcessor { } private boolean haveWriteOperationsHooks(RequestDetails theRequestDetails) { - return CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, myInterceptorBroadcaster, theRequestDetails) - || CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, - myInterceptorBroadcaster, - theRequestDetails); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + return compositeBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE) + || compositeBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST); } private void callWriteOperationsHook( @@ -834,10 +832,14 @@ public abstract class BaseTransactionProcessor { RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, TransactionWriteOperationsDetails theWriteOperationsDetails) { - HookParams params = new HookParams() - .add(TransactionDetails.class, theTransactionDetails) - .add(TransactionWriteOperationsDetails.class, theWriteOperationsDetails); - CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, thePointcut, params); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(thePointcut)) { + HookParams params = new HookParams() + .add(TransactionDetails.class, theTransactionDetails) + .add(TransactionWriteOperationsDetails.class, theWriteOperationsDetails); + compositeBroadcaster.callHooks(thePointcut, params); + } } @SuppressWarnings("unchecked") @@ -967,18 +969,16 @@ public abstract class BaseTransactionProcessor { + " as it contained a duplicate conditional " + verb; ourLog.info(msg); // Interceptor broadcast: JPA_PERFTRACE_INFO - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_WARNING, myInterceptorBroadcaster, theRequestDetails)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { StorageProcessingMessage message = new StorageProcessingMessage().setMessage(msg); HookParams params = new HookParams() .add(RequestDetails.class, theRequestDetails) .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, - theRequestDetails, - Pointcut.JPA_PERFTRACE_INFO, - params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); } theEntries.remove(index); @@ -1475,13 +1475,14 @@ public abstract class BaseTransactionProcessor { } } + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); ListMultimap deferredBroadcastEvents = theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts(); for (Map.Entry nextEntry : deferredBroadcastEvents.entries()) { Pointcut nextPointcut = nextEntry.getKey(); HookParams nextParams = nextEntry.getValue(); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, nextPointcut, nextParams); + compositeBroadcaster.callHooks(nextPointcut, nextParams); } DeferredInterceptorBroadcasts deferredInterceptorBroadcasts = @@ -1492,8 +1493,7 @@ public abstract class BaseTransactionProcessor { .add(DeferredInterceptorBroadcasts.class, deferredInterceptorBroadcasts) .add(TransactionDetails.class, theTransactionDetails) .add(IBaseBundle.class, theResponse); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_TRANSACTION_PROCESSED, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_TRANSACTION_PROCESSED, params); theTransactionDetails.deferredBroadcastProcessingFinished(); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java index 1ba7ef4c086..28682bb1302 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java @@ -135,8 +135,9 @@ public class MatchResourceUrlService { } // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.STORAGE_PRESHOW_RESOURCES, myInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { Map resourceToPidMap = new HashMap<>(); IFhirResourceDao dao = getResourceDao(theResourceType); @@ -152,8 +153,7 @@ public class MatchResourceUrlService { .addIfMatchesType(ServletRequestDetails.class, theRequest); try { - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); retVal = accessDetails.toList().stream() .map(resourceToPidMap::get) @@ -222,16 +222,16 @@ public class MatchResourceUrlService { List retVal = dao.searchForIds(theParamMap, theRequest, theConditionalOperationTargetOrNull); // Interceptor broadcast: JPA_PERFTRACE_INFO - if (CompositeInterceptorBroadcaster.hasHooks( - Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { StorageProcessingMessage message = new StorageProcessingMessage(); message.setMessage("Processed conditional resource URL with " + retVal.size() + " result(s) in " + sw); HookParams params = new HookParams() .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest) .add(StorageProcessingMessage.class, message); - CompositeInterceptorBroadcaster.doCallHooks( - myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); + compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); } return new HashSet<>(retVal); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java index f20fb5aca51..3e351d92ca4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilderFactory.java @@ -19,7 +19,6 @@ */ package ca.uhn.fhir.jpa.dao; -import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; @@ -32,9 +31,8 @@ public class SearchBuilderFactory> { public SearchBuilderFactory() {} - public ISearchBuilder newSearchBuilder( - IDao theDao, String theResourceName, Class theResourceType) { - return (ISearchBuilder) myApplicationContext.getBean( - ISearchBuilder.SEARCH_BUILDER_BEAN_NAME, theDao, theResourceName, theResourceType); + public ISearchBuilder newSearchBuilder(String theResourceName, Class theResourceType) { + return (ISearchBuilder) + myApplicationContext.getBean(ISearchBuilder.SEARCH_BUILDER_BEAN_NAME, theResourceName, theResourceType); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java index 34767a3849d..4b91d0670a9 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionService.java @@ -364,19 +364,21 @@ public class HapiTransactionService implements IHapiTransactionService { } if (maxRetries == 0) { - HookParams params = new HookParams() - .add(RequestDetails.class, theExecutionBuilder.myRequestDetails) - .addIfMatchesType( - ServletRequestDetails.class, theExecutionBuilder.myRequestDetails); - ResourceVersionConflictResolutionStrategy conflictResolutionStrategy = - (ResourceVersionConflictResolutionStrategy) - CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( - myInterceptorBroadcaster, - theExecutionBuilder.myRequestDetails, - Pointcut.STORAGE_VERSION_CONFLICT, - params); - if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) { - maxRetries = conflictResolutionStrategy.getMaxRetries(); + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theExecutionBuilder.myRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_VERSION_CONFLICT)) { + HookParams params = new HookParams() + .add(RequestDetails.class, theExecutionBuilder.myRequestDetails) + .addIfMatchesType( + ServletRequestDetails.class, theExecutionBuilder.myRequestDetails); + ResourceVersionConflictResolutionStrategy conflictResolutionStrategy = + (ResourceVersionConflictResolutionStrategy) + compositeBroadcaster.callHooksAndReturnObject( + Pointcut.STORAGE_VERSION_CONFLICT, params); + if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) { + maxRetries = conflictResolutionStrategy.getMaxRetries(); + } } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java index a8d7fc36781..4d797952be3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/partition/BaseRequestPartitionHelperSvc.java @@ -34,12 +34,11 @@ import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.util.Arrays; @@ -48,12 +47,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.doCallHooks; -import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.doCallHooksAndReturnObject; -import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.hasHooks; - public abstract class BaseRequestPartitionHelperSvc implements IRequestPartitionHelperSvc { - private static final Logger ourLog = LoggerFactory.getLogger(BaseRequestPartitionHelperSvc.class); private final HashSet myNonPartitionableResourceNames; @@ -124,21 +118,25 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition requestPartitionId = getSystemRequestPartitionId((SystemRequestDetails) requestDetails, false); } else if ((requestDetails instanceof SystemRequestDetails) && nonPartitionableResource) { requestPartitionId = RequestPartitionId.fromPartitionId(myPartitionSettings.getDefaultPartitionId()); - } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, requestDetails)) { - // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY - HookParams params = new HookParams() - .add(RequestDetails.class, requestDetails) - .addIfMatchesType(ServletRequestDetails.class, requestDetails); - requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( - myInterceptorBroadcaster, requestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); - } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, requestDetails)) { - // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ - HookParams params = new HookParams() - .add(RequestDetails.class, requestDetails) - .addIfMatchesType(ServletRequestDetails.class, requestDetails) - .add(ReadPartitionIdRequestDetails.class, theDetails); - requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( - myInterceptorBroadcaster, requestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params); + } else { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, requestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY)) { + // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY + HookParams params = new HookParams() + .add(RequestDetails.class, requestDetails) + .addIfMatchesType(ServletRequestDetails.class, requestDetails); + requestPartitionId = (RequestPartitionId) + compositeBroadcaster.callHooksAndReturnObject(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); + } else if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)) { + // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ + HookParams params = new HookParams() + .add(RequestDetails.class, requestDetails) + .addIfMatchesType(ServletRequestDetails.class, requestDetails) + .add(ReadPartitionIdRequestDetails.class, theDetails); + requestPartitionId = (RequestPartitionId) + compositeBroadcaster.callHooksAndReturnObject(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params); + } } validateRequestPartitionNotNull( @@ -158,13 +156,17 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition if (theRequestDetails instanceof SystemRequestDetails && systemRequestHasExplicitPartition((SystemRequestDetails) theRequestDetails)) { requestPartitionId = getSystemRequestPartitionId((SystemRequestDetails) theRequestDetails); - } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, theRequestDetails)) { - // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY - HookParams params = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); - requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( - myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); + } else { + IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( + myInterceptorBroadcaster, theRequestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY)) { + // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY + HookParams params = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); + requestPartitionId = (RequestPartitionId) + compositeBroadcaster.callHooksAndReturnObject(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); + } } // TODO MM: at the moment it is ok for this method to return null @@ -244,21 +246,25 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition && systemRequestHasExplicitPartition((SystemRequestDetails) theRequest)) { requestPartitionId = getSystemRequestPartitionId((SystemRequestDetails) theRequest, nonPartitionableResource); - } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, requestDetails)) { - // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY - HookParams params = new HookParams() - .add(RequestDetails.class, requestDetails) - .addIfMatchesType(ServletRequestDetails.class, requestDetails); - requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( - myInterceptorBroadcaster, requestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); - } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, myInterceptorBroadcaster, requestDetails)) { - // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE - HookParams params = new HookParams() - .add(IBaseResource.class, theResource) - .add(RequestDetails.class, requestDetails) - .addIfMatchesType(ServletRequestDetails.class, requestDetails); - requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( - myInterceptorBroadcaster, requestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); + } else { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, requestDetails); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY)) { + // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY + HookParams params = new HookParams() + .add(RequestDetails.class, requestDetails) + .addIfMatchesType(ServletRequestDetails.class, requestDetails); + requestPartitionId = (RequestPartitionId) + compositeBroadcaster.callHooksAndReturnObject(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); + } else if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)) { + // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE + HookParams params = new HookParams() + .add(IBaseResource.class, theResource) + .add(RequestDetails.class, requestDetails) + .addIfMatchesType(ServletRequestDetails.class, requestDetails); + requestPartitionId = (RequestPartitionId) compositeBroadcaster.callHooksAndReturnObject( + Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); + } } // If the interceptors haven't selected a partition, and its a non-partitionable resource anyhow, send @@ -322,7 +328,9 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition @Override public void validateHasPartitionPermissions( @Nonnull RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { - if (myInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_SELECTED)) { + IInterceptorBroadcaster compositeBroadcaster = + CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); + if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_SELECTED)) { RuntimeResourceDefinition runtimeResourceDefinition = null; if (theResourceType != null) { runtimeResourceDefinition = myFhirContext.getResourceDefinition(theResourceType); @@ -332,7 +340,7 @@ public abstract class BaseRequestPartitionHelperSvc implements IRequestPartition .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest) .add(RuntimeResourceDefinition.class, runtimeResourceDefinition); - doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_SELECTED, params); + compositeBroadcaster.callHooks(Pointcut.STORAGE_PARTITION_SELECTED, params); } } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionServiceTest.java index a6d2e6b368a..947d25b7262 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/tx/HapiTransactionServiceTest.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.test.utilities.MockInvoker; import ca.uhn.fhir.util.SleepUtil; import org.hibernate.exception.ConstraintViolationException; import org.junit.jupiter.api.BeforeEach; @@ -80,17 +81,15 @@ class HapiTransactionServiceTest { } private void mockInterceptorBroadcaster() { - lenient().when(myInterceptorBroadcasterMock.callHooksAndReturnObject(eq(Pointcut.STORAGE_VERSION_CONFLICT), - isA(HookParams.class))) - .thenAnswer(invocationOnMock -> { - HookParams hookParams = (HookParams) invocationOnMock.getArguments()[1]; + lenient().when(myInterceptorBroadcasterMock.hasHooks(eq(Pointcut.STORAGE_VERSION_CONFLICT))).thenReturn(true); + lenient().when(myInterceptorBroadcasterMock.getInvokersForPointcut(eq(Pointcut.STORAGE_VERSION_CONFLICT))).thenReturn(MockInvoker.list(hookParams->{ //answer with whatever retry settings passed in as HookParam RequestDetails requestDetails = hookParams.get(RequestDetails.class); ResourceVersionConflictResolutionStrategy answer = new ResourceVersionConflictResolutionStrategy(); answer.setRetry(requestDetails.isRetry()); answer.setMaxRetries(requestDetails.getMaxRetries()); return answer; - }); + })); } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/MockInvoker.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/MockInvoker.java new file mode 100644 index 00000000000..7c84733f4c1 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/MockInvoker.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.test.utilities; + +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster; +import jakarta.annotation.Nonnull; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +public class MockInvoker implements IBaseInterceptorBroadcaster.IInvoker { + + private final Function myFunction; + + private MockInvoker(Consumer theRunnable) { + this(param -> { theRunnable.accept(param); return null; }); + } + + private MockInvoker(Function theFunction) { + myFunction = theFunction; + } + + @Override + public Object invoke(HookParams theParams) { + return myFunction.apply(theParams); + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public Object getInterceptor() { + return new Object(); + } + + @Override + public int compareTo(@Nonnull IBaseInterceptorBroadcaster.IInvoker o) { + return 0; + } + + public static List list(Consumer theRunnable) { + return List.of(new MockInvoker(theRunnable)); + } + + public static List list(Function theRunnable) { + return List.of(new MockInvoker(theRunnable)); + } + +} From 362dc095ac71f57e9ccac0cfbf6a9d6cbd6d584a Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 27 Nov 2024 17:45:17 -0500 Subject: [PATCH 5/6] Streamline ValidationSupportChain (#6508) * Tests passing * Cleanup * Cleanupo * Test fixes * Test fix * Cleanup * Update hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java Co-authored-by: Ken Stevens * Update hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java Co-authored-by: Ken Stevens * Update hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java Co-authored-by: Ken Stevens * Account for review comments * Spotless * Compile fix * Test fixes * Test cleanup * Test cleanup * Test fixes * Resolve fixme * Test fix * Test fixes * Test fixes * Test fixes * Fix * Test fix * Test fixes * Test fixes * HAPI version bump * Try to address intermittent --------- Co-authored-by: Ken Stevens --- hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- .../java/ca/uhn/fhir/context/FhirContext.java | 16 +- .../support/ConceptValidationOptions.java | 15 + .../DefaultProfileValidationSupport.java | 75 +- .../context/support/IValidationSupport.java | 19 +- .../context/support/LookupCodeRequest.java | 17 + .../support/ValidationSupportContext.java | 4 +- .../support/ValueSetExpansionOptions.java | 21 + .../java/ca/uhn/fhir/rest/api/Constants.java | 2 + .../main/java/ca/uhn/fhir/util/SleepUtil.java | 2 +- hapi-fhir-bom/pom.xml | 4 +- 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/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 | 2 +- .../uhn/hapi/fhir/docs/ValidatorExamples.java | 17 +- ...ation-support-chain-perf-improvements.yaml | 13 + .../validation/validation_support_modules.md | 27 +- hapi-fhir-jacoco/pom.xml | 2 +- hapi-fhir-jaxrsserver-base/pom.xml | 2 +- hapi-fhir-jpa/pom.xml | 2 +- hapi-fhir-jpaserver-base/pom.xml | 2 +- .../ca/uhn/fhir/jpa/config/HapiJpaConfig.java | 10 - .../ca/uhn/fhir/jpa/config/JpaConfig.java | 17 + .../jpa/config/ValidationSupportConfig.java | 48 +- .../util/ValidationSupportConfigUtil.java | 43 - .../ca/uhn/fhir/jpa/dao/HistoryBuilder.java | 20 +- ...JpaPersistedResourceValidationSupport.java | 1 + .../ca/uhn/fhir/jpa/term/TermReadSvcImpl.java | 8 +- ...quireManualActivationInterceptorDstu2.java | 12 +- ...quireManualActivationInterceptorDstu3.java | 12 +- ...sRequireManualActivationInterceptorR4.java | 12 +- .../validation/JpaValidationSupportChain.java | 11 +- .../pom.xml | 2 +- hapi-fhir-jpaserver-hfql/pom.xml | 2 +- hapi-fhir-jpaserver-ips/pom.xml | 2 +- hapi-fhir-jpaserver-mdm/pom.xml | 2 +- hapi-fhir-jpaserver-model/pom.xml | 2 +- hapi-fhir-jpaserver-searchparam/pom.xml | 2 +- hapi-fhir-jpaserver-subscription/pom.xml | 2 +- .../SubscriptionValidatingInterceptor.java | 5 +- hapi-fhir-jpaserver-test-dstu2/pom.xml | 2 +- hapi-fhir-jpaserver-test-dstu3/pom.xml | 2 +- .../FhirResourceDaoDstu3TerminologyTest.java | 8 +- .../FhirResourceDaoDstu3ValidateTest.java | 3 - hapi-fhir-jpaserver-test-r4/pom.xml | 2 +- ...idationSupportFromValidationChainTest.java | 4 +- .../r4/FhirResourceDaoR4TerminologyTest.java | 2 +- .../dao/r4/FhirResourceDaoR4ValidateTest.java | 2 + .../dao/r4/FhirResourceDaoR4ValueSetTest.java | 4 +- .../RemoteTerminologyServiceJpaR4Test.java | 496 +++++++ .../provider/r4/ResourceProviderR4Test.java | 7 +- hapi-fhir-jpaserver-test-r4b/pom.xml | 2 +- hapi-fhir-jpaserver-test-r5/pom.xml | 2 +- .../dao/r5/FhirResourceDaoR5ValueSetTest.java | 22 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- .../ca/uhn/fhir/jpa/test/BaseJpaR4Test.java | 3 - .../ca/uhn/fhir/jpa/test/BaseJpaTest.java | 6 + hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 2 +- hapi-fhir-server-cds-hooks/pom.xml | 2 +- hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server-openapi/pom.xml | 2 +- hapi-fhir-server/pom.xml | 2 +- .../subscription/SubscriptionConstants.java | 3 + .../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 +- .../pom.xml | 2 +- hapi-fhir-storage-batch2/pom.xml | 2 +- hapi-fhir-storage-cr/pom.xml | 2 +- hapi-fhir-storage-mdm/pom.xml | 2 +- hapi-fhir-storage-test-utilities/pom.xml | 2 +- hapi-fhir-storage/pom.xml | 2 +- .../jpa/util/BaseCaptureQueriesListener.java | 23 +- .../CircularQueueCaptureQueriesListener.java | 18 + .../CurrentThreadCaptureQueriesListener.java | 10 + .../validation/ValidatorResourceFetcher.java | 17 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- hapi-fhir-structures-r4/pom.xml | 2 +- hapi-fhir-structures-r4b/pom.xml | 2 +- hapi-fhir-structures-r5/pom.xml | 2 +- hapi-fhir-test-utilities/pom.xml | 2 +- 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-r4b/pom.xml | 2 +- hapi-fhir-validation-resources-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 12 +- .../support/CachingValidationSupport.java | 324 +---- .../SnapshotGeneratingValidationSupport.java | 32 +- .../support/ValidationSupportChain.java | 1207 ++++++++++++++--- .../ValidationSupportChainMetrics.java | 93 ++ .../VersionSpecificWorkerContextWrapper.java | 256 +++- .../support/CachingValidationSupportTest.java | 100 +- .../support/ValidationSupportChainTest.java | 741 +++++++++- ...nSpecificWorkerContextWrapperCoreTest.java | 22 +- ...rsionSpecificWorkerContextWrapperTest.java | 97 +- .../FhirInstanceValidatorDstu3Test.java | 7 +- ...estionnaireResponseValidatorDstu3Test.java | 16 +- .../FhirInstanceValidatorR4Test.java | 7 +- .../QuestionnaireResponseValidatorR4Test.java | 12 +- .../FhirInstanceValidatorR4BTest.java | 7 +- .../FhirInstanceValidatorR5Test.java | 8 +- .../QuestionnaireResponseValidatorR5Test.java | 13 +- hapi-tinder-plugin/pom.xml | 2 +- hapi-tinder-test/pom.xml | 2 +- pom.xml | 20 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- 133 files changed, 3193 insertions(+), 990 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml delete mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java create mode 100644 hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 2146bb70fd8..26d51925270 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 6c1b44706b1..9e0e3e40cdc 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 9a173eed3eb..0f624a5a381 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 796c9f7392e..a66d8cc2db6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -713,6 +713,8 @@ public class FhirContext { "org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport"; String commonCodeSystemsSupportType = "org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService"; + String snapshotGeneratingType = + "org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport"; if (ReflectionUtil.typeExists(inMemoryTermSvcType)) { IValidationSupport inMemoryTermSvc = ReflectionUtil.newInstanceOrReturnNull( inMemoryTermSvcType, @@ -724,11 +726,23 @@ public class FhirContext { IValidationSupport.class, new Class[] {FhirContext.class}, new Object[] {this}); + IValidationSupport snapshotGeneratingSupport = null; + if (getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { + snapshotGeneratingSupport = ReflectionUtil.newInstanceOrReturnNull( + snapshotGeneratingType, + IValidationSupport.class, + new Class[] {FhirContext.class}, + new Object[] {this}); + } retVal = ReflectionUtil.newInstanceOrReturnNull( "org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain", IValidationSupport.class, new Class[] {IValidationSupport[].class}, - new Object[] {new IValidationSupport[] {retVal, inMemoryTermSvc, commonCodeSystemsSupport}}); + new Object[] { + new IValidationSupport[] { + retVal, inMemoryTermSvc, commonCodeSystemsSupport, snapshotGeneratingSupport + } + }); assert retVal != null : "Failed to instantiate " + "org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java index 6e2e19e2d22..d1693a4652b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ConceptValidationOptions.java @@ -22,11 +22,26 @@ package ca.uhn.fhir.context.support; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import java.util.Objects; + public class ConceptValidationOptions { private boolean myValidateDisplay; private boolean myInferSystem; + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ConceptValidationOptions)) return false; + ConceptValidationOptions that = (ConceptValidationOptions) theO; + return myValidateDisplay == that.myValidateDisplay && myInferSystem == that.myInferSystem; + } + + @Override + public int hashCode() { + return Objects.hash(myValidateDisplay, myInferSystem); + } + public boolean isInferSystem() { return myInferSystem; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java index 40f3baa355d..e7e95ea22fd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java @@ -23,7 +23,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.util.ILockable; import ca.uhn.fhir.util.ReflectionUtil; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -48,6 +50,13 @@ public class DefaultProfileValidationSupport implements IValidationSupport { private static final Map ourImplementations = Collections.synchronizedMap(new HashMap<>()); + + /** + * Userdata key indicating the source package ID for this package + */ + public static final String SOURCE_PACKAGE_ID = + DefaultProfileValidationSupport.class.getName() + "_SOURCE_PACKAGE_ID"; + private final FhirContext myCtx; /** * This module just delegates all calls to a concrete implementation which will @@ -62,7 +71,8 @@ public class DefaultProfileValidationSupport implements IValidationSupport { * * @param theFhirContext The context to use */ - public DefaultProfileValidationSupport(FhirContext theFhirContext) { + public DefaultProfileValidationSupport(@Nonnull FhirContext theFhirContext) { + Validate.notNull(theFhirContext, "FhirContext must not be null"); myCtx = theFhirContext; IValidationSupport strategy; @@ -106,33 +116,45 @@ public class DefaultProfileValidationSupport implements IValidationSupport { @Override public List fetchAllConformanceResources() { - return myDelegate.fetchAllConformanceResources(); + List retVal = myDelegate.fetchAllConformanceResources(); + addPackageInformation(retVal); + return retVal; } @Override public List fetchAllStructureDefinitions() { - return myDelegate.fetchAllStructureDefinitions(); + List retVal = myDelegate.fetchAllStructureDefinitions(); + addPackageInformation(retVal); + return retVal; } @Nullable @Override public List fetchAllNonBaseStructureDefinitions() { - return myDelegate.fetchAllNonBaseStructureDefinitions(); + List retVal = myDelegate.fetchAllNonBaseStructureDefinitions(); + addPackageInformation(retVal); + return retVal; } @Override public IBaseResource fetchCodeSystem(String theSystem) { - return myDelegate.fetchCodeSystem(theSystem); + IBaseResource retVal = myDelegate.fetchCodeSystem(theSystem); + addPackageInformation(retVal); + return retVal; } @Override public IBaseResource fetchStructureDefinition(String theUrl) { - return myDelegate.fetchStructureDefinition(theUrl); + IBaseResource retVal = myDelegate.fetchStructureDefinition(theUrl); + addPackageInformation(retVal); + return retVal; } @Override public IBaseResource fetchValueSet(String theUrl) { - return myDelegate.fetchValueSet(theUrl); + IBaseResource retVal = myDelegate.fetchValueSet(theUrl); + addPackageInformation(retVal); + return retVal; } public void flush() { @@ -158,4 +180,43 @@ public class DefaultProfileValidationSupport implements IValidationSupport { } return urlValueString; } + + private void addPackageInformation(List theResources) { + if (theResources != null) { + theResources.forEach(this::addPackageInformation); + } + } + + private void addPackageInformation(IBaseResource theResource) { + if (theResource != null) { + String sourcePackageId = null; + switch (myCtx.getVersion().getVersion()) { + case DSTU2: + case DSTU2_HL7ORG: + sourcePackageId = "hl7.fhir.r2.core"; + break; + case DSTU2_1: + return; + case DSTU3: + sourcePackageId = "hl7.fhir.r3.core"; + break; + case R4: + sourcePackageId = "hl7.fhir.r4.core"; + break; + case R4B: + sourcePackageId = "hl7.fhir.r4b.core"; + break; + case R5: + sourcePackageId = "hl7.fhir.r5.core"; + break; + } + + Validate.notNull( + sourcePackageId, + "Don't know how to handle package ID: %s", + myCtx.getVersion().getVersion()); + + theResource.setUserData(SOURCE_PACKAGE_ID, sourcePackageId); + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index fdbc4ca31d7..41091c28ae9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -116,7 +117,8 @@ public interface IValidationSupport { @Nonnull String theValueSetUrlToExpand) throws ResourceNotFoundException { Validate.notBlank(theValueSetUrlToExpand, "theValueSetUrlToExpand must not be null or blank"); - IBaseResource valueSet = fetchValueSet(theValueSetUrlToExpand); + IBaseResource valueSet = + theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrlToExpand); if (valueSet == null) { throw new ResourceNotFoundException( Msg.code(2024) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(theValueSetUrlToExpand)); @@ -212,8 +214,8 @@ public interface IValidationSupport { () -> fetchStructureDefinition(theUri), () -> fetchValueSet(theUri), () -> fetchCodeSystem(theUri) }; return (T) Arrays.stream(sources) - .map(t -> t.get()) - .filter(t -> t != null) + .map(Supplier::get) + .filter(Objects::nonNull) .findFirst() .orElse(null); } @@ -792,6 +794,7 @@ public interface IValidationSupport { return myValue; } + @Override public String getType() { return TYPE_STRING; } @@ -826,6 +829,7 @@ public interface IValidationSupport { return myDisplay; } + @Override public String getType() { return TYPE_CODING; } @@ -1431,10 +1435,9 @@ public interface IValidationSupport { } /** - *

    * See VersionSpecificWorkerContextWrapper#validateCode in hapi-fhir-validation, and the refer to the values below * for the behaviour associated with each value. @@ -1449,7 +1452,7 @@ public interface IValidationSupport { *

    * @return true or false depending on the desired coding validation behaviour. */ - default boolean isEnabledValidationForCodingsLogicalAnd() { + default boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { return false; } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java index 55adec92a7f..46891633f51 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/LookupCodeRequest.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.context.support; import java.util.Collection; import java.util.Collections; +import java.util.Objects; /** * Represents parameters which can be passed to the $lookup operation for codes. @@ -72,4 +73,20 @@ public class LookupCodeRequest { } return myPropertyNames; } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof LookupCodeRequest)) return false; + LookupCodeRequest that = (LookupCodeRequest) theO; + return Objects.equals(mySystem, that.mySystem) + && Objects.equals(myCode, that.myCode) + && Objects.equals(myDisplayLanguage, that.myDisplayLanguage) + && Objects.equals(myPropertyNames, that.myPropertyNames); + } + + @Override + public int hashCode() { + return Objects.hash(mySystem, myCode, myDisplayLanguage, myPropertyNames); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java index 8f4b8a2a52d..e1e8381230c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValidationSupportContext.java @@ -42,7 +42,7 @@ public class ValidationSupportContext { return myCurrentlyGeneratingSnapshots; } - public boolean isEnabledValidationForCodingsLogicalAnd() { - return myRootValidationSupport.isEnabledValidationForCodingsLogicalAnd(); + public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { + return myRootValidationSupport.isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java index 34f2122d705..6dcd46db56b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/ValueSetExpansionOptions.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.context.support; import org.apache.commons.lang3.Validate; +import java.util.Objects; + /** * Options for ValueSet expansion * @@ -126,4 +128,23 @@ public class ValueSetExpansionOptions { myDisplayLanguage = theDisplayLanguage; return this; } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ValueSetExpansionOptions)) return false; + ValueSetExpansionOptions that = (ValueSetExpansionOptions) theO; + return myFailOnMissingCodeSystem == that.myFailOnMissingCodeSystem + && myCount == that.myCount + && myOffset == that.myOffset + && myIncludeHierarchy == that.myIncludeHierarchy + && Objects.equals(myFilter, that.myFilter) + && Objects.equals(myDisplayLanguage, that.myDisplayLanguage); + } + + @Override + public int hashCode() { + return Objects.hash( + myFailOnMissingCodeSystem, myCount, myOffset, myIncludeHierarchy, myFilter, myDisplayLanguage); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index db4dc9cb027..b99ed890a21 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -342,6 +342,8 @@ public class Constants { */ public static final String HIBERNATE_INTEGRATION_ENVERS_ENABLED = "hibernate.integration.envers.enabled"; + public static final String OPENTELEMETRY_BASE_NAME = "io.hapifhir"; + static { CHARSET_UTF8 = StandardCharsets.UTF_8; CHARSET_US_ASCII = StandardCharsets.ISO_8859_1; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java index e0037782d55..439ff6e7fba 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/SleepUtil.java @@ -33,7 +33,7 @@ public class SleepUtil { @SuppressWarnings("BusyWait") public void sleepAtLeast(long theMillis, boolean theLogProgress) { long start = System.currentTimeMillis(); - while (System.currentTimeMillis() <= start + theMillis) { + while (System.currentTimeMillis() < start + theMillis) { try { long timeSinceStarted = System.currentTimeMillis() - start; long timeToSleep = Math.max(0, theMillis - timeSinceStarted); diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index db25bc4f2ba..615928fa422 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,7 +4,7 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT pom HAPI FHIR BOM @@ -12,7 +12,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 6edc883df32..db4657026c1 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-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 d4a3f519428..33782915ddc 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 - 7.7.7-SNAPSHOT + 7.7.8-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 e402e75a1e3..301353ac697 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 1c93cfa5375..746014589d2 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index eb1663c660b..f6dcd6b81a2 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index c743cad6eb5..f9b12517d25 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 16105895f4c..28d3078c087 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index bc99a2f88c7..0749e3faaff 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 27a94cebe28..a8d553276c6 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java index 9b11ad82d50..b2b3a3e935e 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ValidatorExamples.java @@ -40,7 +40,6 @@ import jakarta.annotation.Nonnull; import jakarta.servlet.ServletException; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.NpmPackageValidationSupport; @@ -343,7 +342,8 @@ public class ValidatorExamples { // START SNIPPET: validateSupplyProfiles FhirContext ctx = FhirContext.forR4(); - // Create a chain that will hold our modules + // Create a chain that will hold our modules and caches the + // values they supply ValidationSupportChain supportChain = new ValidationSupportChain(); // DefaultProfileValidationSupport supplies base FHIR definitions. This is generally required @@ -368,12 +368,9 @@ public class ValidatorExamples { // Add the custom definitions to the chain supportChain.addValidationSupport(prePopulatedSupport); - // Wrap the chain in a cache to improve performance - CachingValidationSupport cache = new CachingValidationSupport(supportChain); - // Create a validator using the FhirInstanceValidator module. We can use this // validator to perform validation - FhirInstanceValidator validatorModule = new FhirInstanceValidator(cache); + FhirInstanceValidator validatorModule = new FhirInstanceValidator(supportChain); FhirValidator validator = ctx.newValidator().registerValidatorModule(validatorModule); ValidationResult result = validator.validateWithResult(input); // END SNIPPET: validateSupplyProfiles @@ -403,12 +400,9 @@ public class ValidatorExamples { remoteTermSvc.setBaseUrl("http://hapi.fhir.org/baseR4"); supportChain.addValidationSupport(remoteTermSvc); - // Wrap the chain in a cache to improve performance - CachingValidationSupport cache = new CachingValidationSupport(supportChain); - // Create a validator using the FhirInstanceValidator module. We can use this // validator to perform validation - FhirInstanceValidator validatorModule = new FhirInstanceValidator(cache); + FhirInstanceValidator validatorModule = new FhirInstanceValidator(supportChain); FhirValidator validator = ctx.newValidator().registerValidatorModule(validatorModule); ValidationResult result = validator.validateWithResult(input); // END SNIPPET: validateUsingRemoteTermSvr @@ -462,12 +456,11 @@ public class ValidatorExamples { new CommonCodeSystemsTerminologyService(ctx), new InMemoryTerminologyServerValidationSupport(ctx), new SnapshotGeneratingValidationSupport(ctx)); - CachingValidationSupport validationSupport = new CachingValidationSupport(validationSupportChain); // Create a validator. Note that for good performance you can create as many validator objects // as you like, but you should reuse the same validation support object in all of the,. FhirValidator validator = ctx.newValidator(); - FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupport); + FhirInstanceValidator instanceValidator = new FhirInstanceValidator(validationSupportChain); validator.registerValidatorModule(instanceValidator); // Create a test patient to validate diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml new file mode 100644 index 00000000000..701bbfd0d60 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6508-validation-support-chain-perf-improvements.yaml @@ -0,0 +1,13 @@ +--- +type: perf +issue: 6508 +title: "The ValidationSupportChain module has been rewritten to improve validator performance. This change: +* Adds new caching capabilities to ValidationSupportChain. This is an improvement over the previous separate caching module because the chain can now remember which entries in the cache responded affirmative to `isValueSetSupported()` and will therefore be more efficient about trying entries in the chain. It also makes debugging much less confusing as there is less recursion and the caches don't use loadingCache. +* Importantly, the caching in ValidationSupportChain caches negative lookups (i.e. items that could not be found by URL) as well as positive lookups. This is a change from the historical caching behaviour. +* Changes ValidationSupportChain to never expire StructureDefinition entries in the cache, which is needed because the validator makes assumptions about structuredefinitions never changing. Fixes #6424. +* Modifies `VersionSpecificWorkerContextWrapper` so that it doesn't use a separate cache and instead relies on the caching provided by ValidationSupportChain. This class previously used a cache because it converts arbitrary versions of FHIR StructureDefinitions into the canonical version required by the validator (R5), but these converted versions are now stored in the userdata map of objects returned by and cached by ValidationSupportChain. This makes the caching more predictable since there is only one cache to track. +* Adds OpenTelemetry support to ValidationSupportChain, with metrics for tracking the cache size. +* Deprecates CachingValidationSupport since caching is now provided by ValidationSupportChain. CachingValidationSupport is now just a passthrough and should be removed from applications. It will be removed from the library in a future release. +* Removes ConceptMap caching from TermReachSvcImpl, as this caching is both redundant and inefficient as it operates within a database transaction. + +These changes result in very significant performance improvements when performing validation in the JPA server. Throughput improvements of 1000% have been recorded in benchmarking use cases involving large profiles and remote terminology services enabled. Many other validation use cases should see significant improvements as well." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md index 05da89a6993..33b4e5fe109 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/validation_support_modules.md @@ -16,6 +16,21 @@ There are a several implementations of the [IValidationSupport](/hapi-fhir/apido This module can be used to combine multiple implementations together so that for every request, each support class instance in the chain is tried in sequence. Note that nearly all methods in the [IValidationSupport](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/support/IValidationSupport.html) interface are permitted to return `null` if they are not able to service a particular method call. So for example, if a call to the [`validateCode`](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/support/IValidationSupport.html#validateCode(ca.uhn.fhir.context.support.ValidationSupportContext,ca.uhn.fhir.context.support.ConceptValidationOptions,java.lang.String,java.lang.String,java.lang.String,java.lang.String)) method is made, the validator will try each module in the chain until one of them returns a non-null response. +The following chaining logic is used: + +* Calls to `fetchAll...` methods such as `fetchAllConformanceResources()` and `fetchAllStructureDefinitions()` will call every method in the chain in order, and aggregate the results into a single list to return. +* Calls to fetch or validate codes, such as `validateCode(...)` and `lookupCode(...)` will first test each module in the chain using the`isCodeSystemSupported(...)` or `isValueSetSupported(...)` methods (depending on whether a ValueSet URL is present in the method parameters) and will invoke any methods in the chain which return that they can handle the given CodeSystem/ValueSet URL. The first non-null value returned by a method in the chain that can support the URL will be returned to the caller. +* All other methods will invoke the method in the chain in order, and will return immediately as soon as a non-null value is returned. + +The following caching logic is used if caching is enabled using `CacheConfiguration`. You can use `CacheConfiguration.disabled()` if you want to disable caching. + +* Calls to fetch StructureDefinitions including `fetchAllStructureDefinitions()` and `fetchStructureDefinition(...)` are cached in a non-expiring cache. This is because the `FhirInstanceValidator` module makes assumptions that these objects will not change for the lifetime of the validator for performance reasons. +* Calls to all other `fetchAll...` methods including `fetchAllConformanceResources()` and `fetchAllSearchParameters()` cache their results in an expiring cache, but will refresh that cache asynchronously. +* Results of `generateSnapshot(...)` are not cached, as this method is generally called in contexts where the results are cached. +* Results of all other methods are stored in an expiring cache. + +Note that caching functionality used to be provided by a separate provider called {@literal CachingValidationSupport} but that functionality has been moved into this class as of HAPI FHIR 8.0.0, because it is possible to provide a more efficient chain when these functions are combined. + # DefaultProfileValidationSupport [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/undefined/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.html) / [Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/DefaultProfileValidationSupport.java) @@ -44,12 +59,6 @@ This module contains a series of HashMaps that store loaded conformance resource This module can be used to load FHIR NPM Packages and supply the conformance resources within them to the validator. See [Validating Using Packages](./instance_validator.html#packages) for am example of how to use this module. -# CachingValidationSupport - -[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java) - -This module caches results of calls to a wrapped service implementation for a period of time. This class can be a significant help in terms of performance if you are loading conformance resources or performing terminology operations from a database or disk, but it also has value even for purely in-memory validation since validating codes against a ValueSet can require the expansion of that ValueSet. - # SnapshotGeneratingValidationSupport [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java) @@ -161,6 +170,12 @@ This validation support module may be placed at the end of a ValidationSupportCh Note that this module must also be activated by calling [setAllowNonExistentCodeSystem(true)](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/UnknownCodeSystemWarningValidationSupport.html#setAllowNonExistentCodeSystem(boolean)) in order to specify that unknown code systems should be allowed. +# CachingValidationSupport + +[JavaDoc](/hapi-fhir/apidocs/hapi-fhir-validation/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.html) / [Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java) + +This module is deprecated and no longer provides any functionality. Caching is provided by [ValidationSupportChain](#validationsupportchain). + # Recipes diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 1e106aaa69f..e6a3f64d67d 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index fe17d290684..f1ce7ec0ce8 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index 3d91a9e4086..e894f987778 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index b7db9a6a5a5..02f29b40e6e 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java index 08095145225..e13b75e4c67 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiJpaConfig.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.jpa.api.IDaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.util.ResourceCountCacheUtil; -import ca.uhn.fhir.jpa.config.util.ValidationSupportConfigUtil; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.search.HSearchSortHelperImpl; @@ -32,15 +31,12 @@ import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import ca.uhn.fhir.jpa.util.ResourceCountCache; -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.rest.api.IResourceSupportedSvc; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; @Configuration @Import({JpaConfig.class}) @@ -64,12 +60,6 @@ public class HapiJpaConfig { return new StaleSearchDeletingSvcImpl(); } - @Primary - @Bean - public CachingValidationSupport validationSupportChain(JpaValidationSupportChain theJpaValidationSupportChain) { - return ValidationSupportConfigUtil.newCachingValidationSupport(theJpaValidationSupportChain); - } - @Bean public DatabaseBackedPagingProvider databaseBackedPagingProvider() { return new DatabaseBackedPagingProvider(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 9a066b9dddb..a17e3425f46 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -168,6 +168,7 @@ import ca.uhn.fhir.jpa.term.config.TermCodeSystemConfig; import ca.uhn.fhir.jpa.util.JpaHapiTransactionService; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.jpa.util.PersistenceContextProvider; +import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.jpa.validation.ResourceLoaderImpl; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.model.api.IPrimitiveDatatype; @@ -185,6 +186,7 @@ import ca.uhn.fhir.util.MetaTagSorterAlphabetical; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import jakarta.annotation.Nullable; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -230,11 +232,26 @@ public class JpaConfig { public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = "PersistedJpaSearchFirstPageBundleProvider"; public static final String HISTORY_BUILDER = "HistoryBuilder"; + public static final String DEFAULT_PROFILE_VALIDATION_SUPPORT = "myDefaultProfileValidationSupport"; private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; @Autowired public JpaStorageSettings myStorageSettings; + @Autowired + private FhirContext myFhirContext; + + @Bean + public ValidationSupportChain.CacheConfiguration validationSupportChainCacheConfiguration() { + return ValidationSupportChain.CacheConfiguration.defaultValues(); + } + + @Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) + @Primary + public IValidationSupport jpaValidationSupportChain() { + return new JpaValidationSupportChain(myFhirContext, validationSupportChainCacheConfiguration()); + } + @Bean("myDaoRegistry") public DaoRegistry daoRegistry() { return new DaoRegistry(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java index 8400b65232b..2c4c97640b5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java @@ -20,31 +20,31 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport; -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.jpa.validation.ValidatorPolicyAdvisor; import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher; import ca.uhn.fhir.validation.IInstanceValidatorModule; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; -import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; -import org.hl7.fhir.common.hapi.validation.validator.HapiToHl7OrgDstu2ValidatingSupportWrapper; import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @Configuration public class ValidationSupportConfig { - @Bean(name = "myDefaultProfileValidationSupport") - public DefaultProfileValidationSupport defaultProfileValidationSupport(FhirContext theFhirContext) { - return new DefaultProfileValidationSupport(theFhirContext); + + @Autowired + private FhirContext myFhirContext; + + @Bean(name = JpaConfig.DEFAULT_PROFILE_VALIDATION_SUPPORT) + public DefaultProfileValidationSupport defaultProfileValidationSupport() { + return new DefaultProfileValidationSupport(myFhirContext); } @Bean @@ -56,11 +56,6 @@ public class ValidationSupportConfig { return retVal; } - @Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN) - public JpaValidationSupportChain jpaValidationSupportChain(FhirContext theFhirContext) { - return new JpaValidationSupportChain(theFhirContext); - } - @Bean(name = JpaConfig.JPA_VALIDATION_SUPPORT) public IValidationSupport jpaValidationSupport(FhirContext theFhirContext) { return new JpaPersistedResourceValidationSupport(theFhirContext); @@ -68,26 +63,13 @@ public class ValidationSupportConfig { @Bean(name = "myInstanceValidator") public IInstanceValidatorModule instanceValidator( - FhirContext theFhirContext, - CachingValidationSupport theCachingValidationSupport, - ValidationSupportChain theValidationSupportChain, - IValidationSupport theValidationSupport, - DaoRegistry theDaoRegistry) { - if (theFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { - FhirInstanceValidator val = new FhirInstanceValidator(theCachingValidationSupport); - val.setValidatorResourceFetcher( - jpaValidatorResourceFetcher(theFhirContext, theValidationSupport, theDaoRegistry)); - val.setValidatorPolicyAdvisor(jpaValidatorPolicyAdvisor()); - val.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning); - val.setValidationSupport(theCachingValidationSupport); - return val; - } else { - CachingValidationSupport cachingValidationSupport = new CachingValidationSupport( - new HapiToHl7OrgDstu2ValidatingSupportWrapper(theValidationSupportChain)); - FhirInstanceValidator retVal = new FhirInstanceValidator(cachingValidationSupport); - retVal.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning); - return retVal; - } + FhirContext theFhirContext, IValidationSupport theValidationSupportChain, DaoRegistry theDaoRegistry) { + FhirInstanceValidator val = new FhirInstanceValidator(theValidationSupportChain); + val.setValidatorResourceFetcher( + jpaValidatorResourceFetcher(theFhirContext, theValidationSupportChain, theDaoRegistry)); + val.setValidatorPolicyAdvisor(jpaValidatorPolicyAdvisor()); + val.setBestPracticeWarningLevel(BestPracticeWarningLevel.Warning); + return val; } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java deleted file mode 100644 index c05f9b7edd6..00000000000 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/util/ValidationSupportConfigUtil.java +++ /dev/null @@ -1,43 +0,0 @@ -/*- - * #%L - * HAPI FHIR JPA Server - * %% - * Copyright (C) 2014 - 2024 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package ca.uhn.fhir.jpa.config.util; - -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; - -public final class ValidationSupportConfigUtil { - private ValidationSupportConfigUtil() {} - - public static CachingValidationSupport newCachingValidationSupport( - JpaValidationSupportChain theJpaValidationSupportChain) { - return newCachingValidationSupport(theJpaValidationSupportChain, false); - } - - public static CachingValidationSupport newCachingValidationSupport( - JpaValidationSupportChain theJpaValidationSupportChain, - boolean theIsEnabledValidationForCodingsLogicalAnd) { - // Short timeout for code translation because TermConceptMappingSvcImpl has its own caching - CachingValidationSupport.CacheTimeouts cacheTimeouts = - CachingValidationSupport.CacheTimeouts.defaultValues().setTranslateCodeMillis(1000); - - return new CachingValidationSupport( - theJpaValidationSupportChain, cacheTimeouts, theIsEnabledValidationForCodingsLogicalAnd); - } -} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java index 8255daf5486..dcf578ee5c9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/HistoryBuilder.java @@ -48,6 +48,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -243,8 +246,23 @@ public class HistoryBuilder { Subquery pastDateSubQuery = theQuery.subquery(Date.class); Root subQueryResourceHistory = pastDateSubQuery.from(ResourceHistoryTable.class); Expression myUpdatedMostRecent = theCriteriaBuilder.max(subQueryResourceHistory.get("myUpdated")); + + /* + * This conversion from the Date in myRangeEndInclusive into a ZonedDateTime is an experiment - + * There is an intermittent test failure in testSearchHistoryWithAtAndGtParameters() that I can't + * figure out. But I've added a ton of logging to the error it fails with and I noticed that + * we emit SQL along the lines of + * select coalesce(max(rht2_0.RES_UPDATED), timestamp with time zone '2024-10-05 18:24:48.172000000Z') + * for this date, and all other dates are in GMT so this is an experiment. If nothing changes, + * we can roll this back to + * theCriteriaBuilder.literal(myRangeStartInclusive) + * JA 20241005 + */ + ZonedDateTime rangeStart = + ZonedDateTime.ofInstant(Instant.ofEpochMilli(myRangeStartInclusive.getTime()), ZoneId.of("GMT")); + Expression myUpdatedMostRecentOrDefault = - theCriteriaBuilder.coalesce(myUpdatedMostRecent, theCriteriaBuilder.literal(myRangeStartInclusive)); + theCriteriaBuilder.coalesce(myUpdatedMostRecent, theCriteriaBuilder.literal(rangeStart)); pastDateSubQuery .select(myUpdatedMostRecentOrDefault) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index f9b62ad95e9..48a04cf38fe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -164,6 +164,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport @Override public IBaseResource fetchStructureDefinition(String theUrl) { + assert myStructureDefinitionType != null; return fetchResource(myStructureDefinitionType, theUrl); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java index 972255d9376..d52f493632b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermReadSvcImpl.java @@ -117,7 +117,6 @@ import org.hibernate.search.mapper.orm.Search; import org.hibernate.search.mapper.orm.common.EntityReference; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hibernate.search.mapper.pojo.massindexing.impl.PojoMassIndexingLoggingMonitor; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50; import org.hl7.fhir.convertors.context.ConversionContext40_50; @@ -284,9 +283,6 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { @Autowired private HibernatePropertiesProvider myHibernatePropertiesProvider; - @Autowired - private CachingValidationSupport myCachingValidationSupport; - @Autowired private VersionCanonicalizer myVersionCanonicalizer; @@ -2509,9 +2505,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs { * results while they test changes, which is probably a worthwhile sacrifice */ private void afterValueSetExpansionStatusChange() { - // TODO: JA2 - Move this caching into the memorycacheservice, and only purge the - // relevant individual cache - myCachingValidationSupport.invalidateCaches(); + provideValidationSupport().invalidateCaches(); } private synchronized boolean isPreExpandingValueSets() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java index 096af297509..892776c9d9d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu2.java @@ -20,6 +20,9 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.resource.Subscription; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; @@ -29,31 +32,32 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_ACTIVATING; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Interceptor which requires newly created {@link Subscription subscriptions} to be in * {@link SubscriptionStatusEnum#REQUESTED} state and prevents clients from changing the status. */ -public class SubscriptionsRequireManualActivationInterceptorDstu2 extends ServerOperationInterceptorAdapter { +@Interceptor +public class SubscriptionsRequireManualActivationInterceptorDstu2 { @Autowired @Qualifier("mySubscriptionDaoDstu2") private IFhirResourceDao myDao; - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { if (myDao.getContext().getResourceType(theResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { if (myDao.getContext().getResourceType(theNewResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java index 9c93bd5dcf7..df670af2e93 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorDstu3.java @@ -20,13 +20,15 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; @@ -34,26 +36,28 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_ACTIVATING; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Interceptor which requires newly created {@link Subscription subscriptions} to be in * {@link SubscriptionStatus#REQUESTED} state and prevents clients from changing the status. */ -public class SubscriptionsRequireManualActivationInterceptorDstu3 extends ServerOperationInterceptorAdapter { +@Interceptor +public class SubscriptionsRequireManualActivationInterceptorDstu3 { @Autowired @Qualifier("mySubscriptionDaoDstu3") private IFhirResourceDao myDao; - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { if (myDao.getContext().getResourceType(theResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { if (myDao.getContext().getResourceType(theNewResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java index 2cb14efdd18..3d6cf30cfb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/SubscriptionsRequireManualActivationInterceptorR4.java @@ -20,13 +20,15 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; @@ -34,26 +36,28 @@ import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_ACTIVATING; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Interceptor which requires newly created {@link Subscription subscriptions} to be in * {@link SubscriptionStatus#REQUESTED} state and prevents clients from changing the status. */ -public class SubscriptionsRequireManualActivationInterceptorR4 extends ServerOperationInterceptorAdapter { +@Interceptor +public class SubscriptionsRequireManualActivationInterceptorR4 { @Autowired @Qualifier("mySubscriptionDaoR4") private IFhirResourceDao myDao; - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { if (myDao.getContext().getResourceType(theResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.CREATE, null, theResource); } } - @Override + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_ACTIVATING) public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { if (myDao.getContext().getResourceType(theNewResource).equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { verifyStatusOk(RestOperationTypeEnum.UPDATE, theOldResource, theNewResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java index 25fda432e36..51791e0a051 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.config.JpaConfig; import ca.uhn.fhir.jpa.packages.NpmJpaValidationSupport; import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; @@ -39,7 +40,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain { private final FhirContext myFhirContext; @Autowired - @Qualifier("myJpaValidationSupport") + @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT) public IValidationSupport myJpaValidationSupport; @Qualifier("myDefaultProfileValidationSupport") @@ -64,7 +65,13 @@ public class JpaValidationSupportChain extends ValidationSupportChain { /** * Constructor */ - public JpaValidationSupportChain(FhirContext theFhirContext) { + public JpaValidationSupportChain( + FhirContext theFhirContext, ValidationSupportChain.CacheConfiguration theCacheConfiguration) { + super(theCacheConfiguration); + + assert theFhirContext != null; + assert theCacheConfiguration != null; + myFhirContext = theFhirContext; } diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 02632b55cbd..e945e1951d9 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-hfql/pom.xml b/hapi-fhir-jpaserver-hfql/pom.xml index 7325bd0e32f..6ebfca9d291 100644 --- a/hapi-fhir-jpaserver-hfql/pom.xml +++ b/hapi-fhir-jpaserver-hfql/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml index 8b8d77344cc..bccea89af6f 100644 --- a/hapi-fhir-jpaserver-ips/pom.xml +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index f07832756c1..6d73b51dc28 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 4c77735cfce..71d5378a536 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 3b5fa244225..17961eca9d5 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 4f1146e7b01..01b2efc2643 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java index c57dc761665..de49da049b2 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java @@ -64,6 +64,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import static ca.uhn.fhir.subscription.SubscriptionConstants.ORDER_SUBSCRIPTION_VALIDATING; import static org.apache.commons.lang3.StringUtils.isBlank; @Interceptor @@ -92,14 +93,14 @@ public class SubscriptionValidatingInterceptor { @Autowired private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory; - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, order = ORDER_SUBSCRIPTION_VALIDATING) public void resourcePreCreate( IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { validateSubmittedSubscription( theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); } - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) + @Hook(value = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, order = ORDER_SUBSCRIPTION_VALIDATING) public void resourceUpdated( IBaseResource theOldResource, IBaseResource theResource, diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index c9701f08e4b..9b3c42420e3 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 - 7.7.7-SNAPSHOT + 7.7.8-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 99000fbb047..575740e0752 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index e591027c62d..bccc40e1fd3 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -58,8 +58,6 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { public static final String URL_MY_VALUE_SET = "http://example.com/my_value_set"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3TerminologyTest.class); @Autowired - private CachingValidationSupport myCachingValidationSupport; - @Autowired private ITermDeferredStorageSvc myTermDeferredStorageSvc; @AfterEach @@ -69,10 +67,12 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { TermReindexingSvcImpl.setForceSaveDeferredAlwaysForUnitTest(false); } + @Override @BeforeEach - public void before() { + public void before() throws Exception { + super.before(); myStorageSettings.setMaximumExpansionSize(5000); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); } private CodeSystem createExternalCs() { diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index d8c0a4f99e0..47ac1ba2f36 100644 --- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -14,7 +14,6 @@ import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.validation.IValidatorModule; import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; @@ -55,8 +54,6 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { @Autowired private IValidatorModule myValidatorModule; @Autowired - private CachingValidationSupport myValidationSupport; - @Autowired private FhirInstanceValidator myFhirInstanceValidator; @Autowired @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT) diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 8a9353386f4..fc415fbd3c1 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java index ef9c28688e1..6163dea83f8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupportFromValidationChainTest.java @@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -38,7 +39,7 @@ import static org.mockito.Mockito.when; public class JpaPersistedResourceValidationSupportFromValidationChainTest { private static final FhirContext ourCtx = FhirContext.forR4(); - private IValidationSupport jpaValidator; + private JpaPersistedResourceValidationSupport jpaValidator; @Mock private DaoRegistry myDaoRegistry; @@ -58,6 +59,7 @@ public class JpaPersistedResourceValidationSupportFromValidationChainTest { @BeforeEach public void setUp() { jpaValidator = new JpaPersistedResourceValidationSupport(ourCtx); + jpaValidator.start(); ReflectionTestUtils.setField(jpaValidator, "myDaoRegistry", myDaoRegistry); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java index fac5f68ef0c..ab8bd3a14b5 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java @@ -75,7 +75,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { public void before() throws Exception { super.before(); myStorageSettings.setMaximumExpansionSize(5000); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); } private CodeSystem createExternalCs() { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 7cd70ab49e6..f007aa2fcf9 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -38,6 +38,7 @@ import ch.qos.logback.classic.Level; import org.apache.commons.io.IOUtils; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -2237,6 +2238,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { createStructureDefinitionInDao(); // execute + ((ValidationSupportChain)myValidationSupport).invalidateExpiringCaches(); final String outcomePatientValidateAfterStructDef = validate(PATIENT_WITH_REAL_URL); // verify diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java index 7b5e063681c..ac590617758 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetTest.java @@ -169,7 +169,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); logAllValueSets(); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "child10", null, "http://vs"); assertNotNull(outcome); @@ -272,7 +272,7 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); logAllValueSets(); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "child10", null, "http://vs"); assertNotNull(outcome); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java new file mode 100644 index 00000000000..8ed166482e0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/RemoteTerminologyServiceJpaR4Test.java @@ -0,0 +1,496 @@ +package ca.uhn.fhir.jpa.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +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.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import jakarta.annotation.Nonnull; +import jakarta.servlet.http.HttpServletRequest; +import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.CodeSystem; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ElementDefinition; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.UriType; +import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This test verifies how {@link RemoteTerminologyServiceValidationSupport} interacts with + * the rest of the ValidationSupportChain. The aim here is that we should perform as few + * interactions across the network as we can, so any caching that avoids a lookup through + * the remote module is a good thing. We're also testing that we don't open more database + * connections than we need to, since every connection is a delay. + */ +public class RemoteTerminologyServiceJpaR4Test extends BaseJpaR4Test { + + private static final MyCodeSystemProvider ourCodeSystemProvider = new MyCodeSystemProvider(); + private static final MyValueSetProvider ourValueSetProvider = new MyValueSetProvider(); + @RegisterExtension + private static final RestfulServerExtension ourTerminologyServer = new RestfulServerExtension(FhirContext.forR4Cached()) + .registerProvider(ourCodeSystemProvider) + .registerProvider(ourValueSetProvider); + @Autowired + private ValidationSupportChain myValidationSupportChain; + @Autowired + private FhirInstanceValidator myFhirInstanceValidator; + private RemoteTerminologyServiceValidationSupport myRemoteTerminologyService; + private ValidationSupportChain myInternalValidationSupport; + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + + myFhirInstanceValidator.setBestPracticeWarningLevel(BestPracticeWarningLevel.Ignore); + + List original = myValidationSupportChain.getValidationSupports(); + myInternalValidationSupport = new ValidationSupportChain(original); + + myRemoteTerminologyService = new RemoteTerminologyServiceValidationSupport(myFhirContext, ourTerminologyServer.getBaseUrl()); + myValidationSupportChain.addValidationSupport(0, myRemoteTerminologyService); + myValidationSupportChain.invalidateCaches(); + + // Warm this as it's needed once by the FhirPath evaluator on startup + // so this avoids having different connection counts depending on + // which test method is called first. This is a non-expiring cache, so + // pre-warming here isn't affecting anything meaningful. + myValidationSupportChain.fetchAllStructureDefinitions(); + } + + @AfterEach + public void after() throws Exception { + myValidationSupportChain.logCacheSizes(); + myValidationSupportChain.removeValidationSupport(myRemoteTerminologyService); + + ourValueSetProvider.clearAll(); + ourCodeSystemProvider.clearAll(); + } + + @Test + public void testValidateSimpleCode() { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.FEMALE); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // Verify 1 + Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/administrative-gender", + "http://hl7.org/fhir/ValueSet/administrative-gender" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/administrative-gender", + "http://hl7.org/fhir/administrative-gender" + ); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + + } + + @Test + public void testValidateSimpleCode_SupportedByRemoteService() { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.FEMALE); + ourValueSetProvider.add((ValueSet) requireNonNull(myInternalValidationSupport.fetchValueSet("http://hl7.org/fhir/ValueSet/administrative-gender"))); + ourCodeSystemProvider.add((CodeSystem) requireNonNull(myInternalValidationSupport.fetchCodeSystem("http://hl7.org/fhir/administrative-gender"))); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // Verify 1 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/administrative-gender", + "http://hl7.org/fhir/ValueSet/administrative-gender" + ); + assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/administrative-gender#http://hl7.org/fhir/administrative-gender#female" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/administrative-gender" + ); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/administrative-gender#female#null" + ); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourValueSetProvider.myValidatedCodes).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); + + } + + /** + * If the remote terminology service is serving up stub ValueSet and CodeSystem + * resources, make sure we still behave in a sane way. This probably wouldn't + * happen exactly like this, but the idea here is that the server could + * serve up weird contents where our internal services couldn't determine + * the implicit system from the ValueSet. + */ + @Test + public void testValidateSimpleCode_SupportedByRemoteService_EmptyValueSet() { + Patient p = new Patient(); + p.setGender(Enumerations.AdministrativeGender.FEMALE); + ourValueSetProvider.add((ValueSet) new ValueSet().setUrl("http://hl7.org/fhir/ValueSet/administrative-gender").setId("gender")); + ourCodeSystemProvider.add((CodeSystem) new CodeSystem().setUrl("http://hl7.org/fhir/administrative-gender").setId("gender")); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // Verify 1 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/administrative-gender", + "http://hl7.org/fhir/ValueSet/administrative-gender" + ); + assertThat(ourValueSetProvider.myValidatedCodes).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/administrative-gender#null#female" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourValueSetProvider.myValidatedCodes).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.myValidatedCodes).asList().isEmpty(); + + myValidationSupportChain.logCacheSizes(); + } + + @Test + public void testValidateSimpleExtension() { + // Setup + myStructureDefinitionDao.create(createFooExtensionStructureDefinition(), mySrd); + Patient p = new Patient(); + p.addExtension("http://foo", new StringType("BAR")); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertSuccess(outcome); + + // Verify 1 + myCaptureQueriesListener.logSelectQueries(); + Assertions.assertEquals(4, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + outcome = validate(p); + assertSuccess(outcome); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + + } + + @Test + public void testValidateMultipleCodings() { + // Setup + Patient p = new Patient(); + p.addIdentifier() + // Valid type + .setType(new CodeableConcept().addCoding(new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "DL", null))) + .setSystem("http://my-system-1") + .setValue("1"); + p.addIdentifier() + // Valid type + .setType(new CodeableConcept().addCoding(new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "PPN", null))) + .setSystem("http://my-system-2") + .setValue("2"); + p.addIdentifier() + // Invalid type + .setType(new CodeableConcept().addCoding(new Coding("http://terminology.hl7.org/CodeSystem/v2-0203", "FOO", null))) + .setSystem("http://my-system-3") + .setValue("3"); + + // Test 1 + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + IBaseOperationOutcome outcome = validate(p); + assertHasIssuesContainingMessages(outcome, + "Unknown code 'http://terminology.hl7.org/CodeSystem/v2-0203#FOO'", + "None of the codings provided are in the value set 'IdentifierType'"); + + // Verify 1 + Assertions.assertEquals(2, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://hl7.org/fhir/ValueSet/identifier-type", + "http://hl7.org/fhir/ValueSet/identifier-type" + ); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().containsExactlyInAnyOrder( + "http://terminology.hl7.org/CodeSystem/v2-0203", + "http://terminology.hl7.org/CodeSystem/v2-0203" + ); + assertEquals(0, ourValueSetProvider.myValidatedCodes.size()); + assertEquals(0, ourCodeSystemProvider.myValidatedCodes.size()); + + // Test 2 (should rely on caches) + ourCodeSystemProvider.clearCalls(); + ourValueSetProvider.clearCalls(); + myCaptureQueriesListener.clear(); + validate(p); + + // Verify 2 + Assertions.assertEquals(0, myCaptureQueriesListener.countGetConnections()); + assertThat(ourValueSetProvider.mySearchUrls).asList().isEmpty(); + assertThat(ourCodeSystemProvider.mySearchUrls).asList().isEmpty(); + assertEquals(0, ourValueSetProvider.myValidatedCodes.size()); + assertEquals(0, ourCodeSystemProvider.myValidatedCodes.size()); + + } + + private void assertSuccess(IBaseOperationOutcome theOutcome) { + OperationOutcome oo = (OperationOutcome) theOutcome; + assertEquals(1, oo.getIssue().size(), () -> encode(oo)); + assertThat(oo.getIssue().get(0).getDiagnostics()).as(() -> encode(oo)).contains("No issues detected"); + } + + private void assertHasIssuesContainingMessages(IBaseOperationOutcome theOutcome, String... theDiagnosticMessageFragments) { + OperationOutcome oo = (OperationOutcome) theOutcome; + assertEquals(theDiagnosticMessageFragments.length, oo.getIssue().size(), () -> encode(oo)); + for (int i = 0; i < theDiagnosticMessageFragments.length; i++) { + assertThat(oo.getIssue().get(i).getDiagnostics()).as(() -> encode(oo)).contains(theDiagnosticMessageFragments[i]); + } + } + + private IBaseOperationOutcome validate(Patient p) { + return myPatientDao.validate(p, null, myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p), EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd).getOperationOutcome(); + } + + /** + * Create a StructureDefinition for an extension with URL http://foo + */ + @Nonnull + private static StructureDefinition createFooExtensionStructureDefinition() { + StructureDefinition sd = new StructureDefinition(); + sd.setUrl("http://foo"); + sd.setFhirVersion(Enumerations.FHIRVersion._4_0_1); + sd.setKind(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE); + sd.setAbstract(false); + sd.addContext().setType(StructureDefinition.ExtensionContextType.ELEMENT).setExpression("Patient"); + sd.setType("Extension"); + sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension"); + sd.setDerivation(StructureDefinition.TypeDerivationRule.CONSTRAINT); + ElementDefinition e0 = sd.getDifferential().addElement(); + e0.setId("Extension"); + e0.setPath("Extension"); + ElementDefinition e1 = sd.getDifferential().addElement(); + e1.setId("Extension.url"); + e1.setPath("Extension.url"); + e1.setFixed(new UriType("http://foo")); + ElementDefinition e2 = sd.getDifferential().addElement(); + e2.setId("Extension.value[x]"); + e2.setPath("Extension.value[x]"); + e2.addType().setCode("string"); + return sd; + } + + private static String toValue(IPrimitiveType theUrlType) { + return theUrlType != null ? theUrlType.getValue() : null; + } + + private static class MyCodeSystemProvider implements IResourceProvider { + private final ListMultimap myUrlToCodeSystems = MultimapBuilder.hashKeys().arrayListValues().build(); + private final List mySearchUrls = new ArrayList<>(); + private final List myValidatedCodes = new ArrayList<>(); + + public void clearAll() { + myUrlToCodeSystems.clear(); + clearCalls(); + } + + public void clearCalls() { + mySearchUrls.clear(); + myValidatedCodes.clear(); + } + + @Operation(name = "validate-code", idempotent = true, returnParameters = { + @OperationParam(name = "result", type = BooleanType.class, min = 1), + @OperationParam(name = "message", type = StringType.class), + @OperationParam(name = "display", type = StringType.class) + }) + public Parameters validateCode( + HttpServletRequest theServletRequest, + @IdParam(optional = true) IdType theId, + @OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl, + @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, + @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay + ) { + myValidatedCodes.add(toValue(theCodeSystemUrl) + "#" + toValue(theCode) + "#" + toValue(theDisplay)); + + Parameters retVal = new Parameters(); + retVal.addParameter("result", new BooleanType(true)); + return retVal; + } + + @Search + public List find(@RequiredParam(name = "url") UriParam theUrlParam) { + String url = theUrlParam != null ? theUrlParam.getValue() : null; + mySearchUrls.add(url); + return myUrlToCodeSystems.get(defaultString(url)); + } + + @Override + public Class getResourceType() { + return CodeSystem.class; + } + + public void add(CodeSystem theCs) { + assert theCs != null; + assert isNotBlank(theCs.getUrl()); + myUrlToCodeSystems.put(theCs.getUrl(), theCs); + } + } + + @SuppressWarnings("unused") + private static class MyValueSetProvider implements IResourceProvider { + + private final ListMultimap myUrlToValueSets = MultimapBuilder.hashKeys().arrayListValues().build(); + private final List mySearchUrls = new ArrayList<>(); + private final List myValidatedCodes = new ArrayList<>(); + + public void clearAll() { + myUrlToValueSets.clear(); + clearCalls(); + } + + public void clearCalls() { + mySearchUrls.clear(); + myValidatedCodes.clear(); + } + + @Operation(name = "validate-code", idempotent = true, returnParameters = { + @OperationParam(name = "result", type = BooleanType.class, min = 1), + @OperationParam(name = "message", type = StringType.class), + @OperationParam(name = "display", type = StringType.class) + }) + public Parameters validateCode( + HttpServletRequest theServletRequest, + @OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl, + @OperationParam(name = "code", min = 0, max = 1) CodeType theCode, + @OperationParam(name = "system", min = 0, max = 1) UriType theSystem, + @OperationParam(name = "display", min = 0, max = 1) StringType theDisplay, + @OperationParam(name = "valueSet") ValueSet theValueSet + ) { + myValidatedCodes.add(toValue(theValueSetUrl) + "#" + toValue(theSystem) + "#" + toValue(theCode)); + + Parameters retVal = new Parameters(); + retVal.addParameter("result", new BooleanType(true)); + return retVal; + } + + @Search + public List find(@OptionalParam(name = "url") UriParam theUrlParam) { + String url = theUrlParam != null ? theUrlParam.getValue() : null; + mySearchUrls.add(url); + List retVal = myUrlToValueSets.get(defaultString(url)); + ourLog.info("Remote terminology fetch ValueSet[{}] - Found: {}", url, !retVal.isEmpty()); + return retVal; + } + + @Override + public Class getResourceType() { + return ValueSet.class; + } + + public void add(ValueSet theVs) { + assert theVs != null; + assert isNotBlank(theVs.getUrl()); + myUrlToValueSets.put(theVs.getUrl(), theVs); + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 8f3e849719f..1da8eb0e6db 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -2366,18 +2366,19 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { IIdType id = idCreated.toUnqualifiedVersionless(); for (int i = 0; i < 10; i++) { - sleepOneClick(); + sleepAtLeast(100); preDates.add(new Date()); - sleepOneClick(); + sleepAtLeast(100); patient.setId(id); patient.getName().get(0).getFamilyElement().setValue(methodName + "_i" + i); ids.add(myPatientDao.update(patient, mySrd).getId().toUnqualified().getValue()); - sleepOneClick(); } List idValues; + myCaptureQueriesListener.clear(); idValues = searchAndReturnUnqualifiedIdValues(myServerBase + "/Patient/" + id.getIdPart() + "/_history?_at=gt" + toStr(preDates.get(0)) + "&_at=lt" + toStr(preDates.get(3))); + myCaptureQueriesListener.logSelectQueries(); assertThat(idValues).as(idValues.toString()).containsExactly(ids.get(3), ids.get(2), ids.get(1), ids.get(0)); idValues = searchAndReturnUnqualifiedIdValues(myServerBase + "/Patient/_history?_at=gt" + toStr(preDates.get(0)) + "&_at=lt" + toStr(preDates.get(3))); diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index e84136c59f7..bb5bfc312e1 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 - 7.7.7-SNAPSHOT + 7.7.8-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 9ebac21f0b6..d17bb5b2f29 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java index 8d96a32e00d..b1ed8242826 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5ValueSetTest.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.jpa.dao.r5; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; @@ -17,7 +15,6 @@ import ca.uhn.fhir.jpa.util.ValueSetTestUtil; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r5.model.CodeSystem; @@ -38,10 +35,12 @@ import java.io.IOException; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { @@ -51,7 +50,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { @Autowired protected ITermDeferredStorageSvc myTerminologyDeferredStorageSvc; - private IIdType myExtensionalVsId; @AfterEach @@ -61,7 +59,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { myStorageSettings.setExpungeEnabled(new JpaStorageSettings().isExpungeEnabled()); } - @BeforeEach @Transactional public void before02() throws IOException { @@ -251,7 +248,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { } - /** * See #4305 */ @@ -267,7 +263,7 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { // Update valueset vs.setName("Hello"); assertEquals("2", myValueSetDao.update(vs, mySrd).getId().getVersionIdPart()); - runInTransaction(()->{ + runInTransaction(() -> { Optional resource = myResourceTableDao.findById(id.getIdPartAsLong()); assertTrue(resource.isPresent()); }); @@ -287,13 +283,12 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { myValueSetDao.expunge(id, new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), mySrd); // Verify expunged - runInTransaction(()->{ + runInTransaction(() -> { Optional resource = myResourceTableDao.findById(id.getIdPartAsLong()); assertFalse(resource.isPresent()); }); } - @Test public void testExpandByValueSet_ExceedsMaxSize() { // Add a bunch of codes @@ -336,12 +331,12 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); logAllValueSets(); - myCachingValidationSupport.invalidateCaches(); + myValidationSupport.invalidateCaches(); // Validate code ValidationSupportContext ctx = new ValidationSupportContext(myValidationSupport); - ConceptValidationOptions options= new ConceptValidationOptions(); + ConceptValidationOptions options = new ConceptValidationOptions(); IValidationSupport.CodeValidationResult outcome = myValidationSupport.validateCode(ctx, options, "http://cs", "CODE4", null, "http://vs"); assertNotNull(outcome); assertTrue(outcome.isOk()); @@ -354,9 +349,6 @@ public class FhirResourceDaoR5ValueSetTest extends BaseJpaR5Test { assertThat(expansionMessage).startsWith("ValueSet was expanded using an expansion that was pre-calculated"); } - @Autowired - protected CachingValidationSupport myCachingValidationSupport; - @Test public void testValidateCodeAgainstBuiltInValueSetAndCodeSystemWithValidCode() { IPrimitiveType display = null; diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 1ac2a59d4e2..c942a7a4ab9 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java index f379ee2cf0a..b29c4a76ea2 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java @@ -118,7 +118,6 @@ import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import ca.uhn.test.util.LogbackTestExtension; import jakarta.persistence.EntityManager; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -246,8 +245,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Autowired protected ITermReadSvc myHapiTerminologySvc; @Autowired - protected CachingValidationSupport myCachingValidationSupport; - @Autowired protected ITermCodeSystemStorageSvc myTermCodeSystemStorageSvc; @Autowired protected ISearchDao mySearchEntityDao; 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 dba57255f05..8423f9e899a 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 @@ -271,6 +271,8 @@ public abstract class BaseJpaTest extends BaseTest { @Autowired private IValidationSupport myJpaPersistedValidationSupport; @Autowired + private IValidationSupport myValidationSupport; + @Autowired private FhirInstanceValidator myFhirInstanceValidator; @Autowired private IResourceTableDao myResourceTableDao; @@ -398,6 +400,10 @@ public abstract class BaseJpaTest extends BaseTest { if (myFhirInstanceValidator != null) { myFhirInstanceValidator.invalidateCaches(); } + if (myValidationSupport != null) { + myValidationSupport.invalidateCaches(); + } + JpaStorageSettings defaultConfig = new JpaStorageSettings(); myStorageSettings.setAdvancedHSearchIndexing(defaultConfig.isAdvancedHSearchIndexing()); myStorageSettings.setAllowContainsSearches(defaultConfig.isAllowContainsSearches()); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 37b62d52c3c..c0b49b0e380 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-server-cds-hooks/pom.xml b/hapi-fhir-server-cds-hooks/pom.xml index 0c3871b2a8c..cca741e8e44 100644 --- a/hapi-fhir-server-cds-hooks/pom.xml +++ b/hapi-fhir-server-cds-hooks/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 00258dc7ebd..8aa2a2d33a9 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index d0671ee96a5..052fc447623 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index cb990bf5cfa..642311c8ff5 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java index 56dabb1d52e..27a8cc0396d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/subscription/SubscriptionConstants.java @@ -58,4 +58,7 @@ public class SubscriptionConstants { "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-payload-content"; public static final String SUBSCRIPTION_TOPIC_STATUS = "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription-status-r4"; + + public static final int ORDER_SUBSCRIPTION_VALIDATING = 100; + public static final int ORDER_SUBSCRIPTION_ACTIVATING = 200; } diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index acedc81e55a..334ada1b88a 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 - 7.7.7-SNAPSHOT + 7.7.8-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 e7f7d5b7212..55c95b5b01e 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml @@ -21,7 +21,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index 79741373a8a..8bdee9dffc1 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 - 7.7.7-SNAPSHOT + 7.7.8-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 c54fc19604a..6a6f3b5a624 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index 314519bf752..4eaa08227ee 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/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 6afde2bfd71..9fb961a7de3 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 - 7.7.7-SNAPSHOT + 7.7.8-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 018576bb564..c0404daee27 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 - 7.7.7-SNAPSHOT + 7.7.8-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 59b7c769df0..a03857982c3 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT 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 2ed7be33b1d..97caf2be8ae 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT 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 02a7740e4c9..00431db20ca 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT 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 ee4b04cf6e7..1cb18798c2a 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 51d5c49944f..8485992ca06 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index acca99e04de..9db01829031 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 - 7.7.7-SNAPSHOT + 7.7.8-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 5421f5e2793..61df69802a8 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-test-utilities/pom.xml b/hapi-fhir-storage-batch2-test-utilities/pom.xml index 93b435fc026..1c4f8874056 100644 --- a/hapi-fhir-storage-batch2-test-utilities/pom.xml +++ b/hapi-fhir-storage-batch2-test-utilities/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index d662c258d6e..7c8c0512358 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index 6f31b62b9ee..5f7d299e4a1 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index 4cf64576707..5a1e19e54e9 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index 305ca40e896..e76bfbc8bbd 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index d90d02dfa75..804588a7555 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java index f2c8d92ec93..5d56284f450 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/BaseCaptureQueriesListener.java @@ -27,6 +27,8 @@ import net.ttddyy.dsproxy.QueryInfo; import net.ttddyy.dsproxy.listener.MethodExecutionContext; import net.ttddyy.dsproxy.proxy.ParameterSetOperation; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.List; @@ -38,7 +40,7 @@ import static org.apache.commons.lang3.StringUtils.trim; public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution, ProxyDataSourceBuilder.SingleMethodExecution { - + private static final Logger ourLog = LoggerFactory.getLogger(BaseCaptureQueriesListener.class); private boolean myCaptureQueryStackTrace = false; /** @@ -112,6 +114,9 @@ public abstract class BaseCaptureQueriesListener @Nullable protected abstract AtomicInteger provideCommitCounter(); + @Nullable + protected abstract AtomicInteger provideGetConnectionCounter(); + @Nullable protected abstract AtomicInteger provideRollbackCounter(); @@ -125,6 +130,9 @@ public abstract class BaseCaptureQueriesListener case "rollback": counter = provideRollbackCounter(); break; + case "getConnection": + counter = provideGetConnectionCounter(); + break; } if (counter != null) { @@ -132,10 +140,23 @@ public abstract class BaseCaptureQueriesListener } } + /** + * @return Returns the number of times the connection pool was asked for a new connection + */ + public int countGetConnections() { + return provideGetConnectionCounter().get(); + } + + /** + * @return Returns the number of DB commits which have happened against connections from the pool + */ public int countCommits() { return provideCommitCounter().get(); } + /** + * @return Returns the number of DB rollbacks which have happened against connections from the pool + */ public int countRollbacks() { return provideRollbackCounter().get(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java index 4739b26afde..5714b97fd04 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CircularQueueCaptureQueriesListener.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.util; import ca.uhn.fhir.util.StopWatch; import com.google.common.collect.Queues; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.collections4.queue.CircularFifoQueue; import org.hl7.fhir.r4.model.InstantType; @@ -58,6 +59,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe private static final int CAPACITY = 10000; private static final Logger ourLog = LoggerFactory.getLogger(CircularQueueCaptureQueriesListener.class); private Queue myQueries; + private AtomicInteger myGetConnectionCounter; private AtomicInteger myCommitCounter; private AtomicInteger myRollbackCounter; @@ -91,6 +93,12 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe return myCommitCounter; } + @Nullable + @Override + protected AtomicInteger provideGetConnectionCounter() { + return myGetConnectionCounter; + } + @Override protected AtomicInteger provideRollbackCounter() { return myRollbackCounter; @@ -101,6 +109,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe */ public void clear() { myQueries.clear(); + myGetConnectionCounter.set(0); myCommitCounter.set(0); myRollbackCounter.set(0); } @@ -110,6 +119,7 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe */ public void startCollecting() { myQueries = Queues.synchronizedQueue(new CircularFifoQueue<>(CAPACITY)); + myGetConnectionCounter = new AtomicInteger(0); myCommitCounter = new AtomicInteger(0); myRollbackCounter = new AtomicInteger(0); } @@ -167,10 +177,18 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe return getQueriesMatching(thePredicate, threadName); } + /** + * @deprecated Use {@link #countCommits()} + */ + @Deprecated public int getCommitCount() { return myCommitCounter.get(); } + /** + * @deprecated Use {@link #countRollbacks()} + */ + @Deprecated public int getRollbackCount() { return myRollbackCounter.get(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java index a9a211b009f..391ba1be456 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/util/CurrentThreadCaptureQueriesListener.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.util; +import jakarta.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +33,7 @@ import java.util.stream.Collectors; public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListener { private static final ThreadLocal> ourQueues = new ThreadLocal<>(); + private static final ThreadLocal ourGetConnections = new ThreadLocal<>(); private static final ThreadLocal ourCommits = new ThreadLocal<>(); private static final ThreadLocal ourRollbacks = new ThreadLocal<>(); private static final Logger ourLog = LoggerFactory.getLogger(CurrentThreadCaptureQueriesListener.class); @@ -46,6 +48,12 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe return ourCommits.get(); } + @Nullable + @Override + protected AtomicInteger provideGetConnectionCounter() { + return ourGetConnections.get(); + } + @Override protected AtomicInteger provideRollbackCounter() { return ourRollbacks.get(); @@ -57,6 +65,7 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe public static SqlQueryList getCurrentQueueAndStopCapturing() { Queue retVal = ourQueues.get(); ourQueues.remove(); + ourGetConnections.remove(); ourCommits.remove(); ourRollbacks.remove(); if (retVal == null) { @@ -76,6 +85,7 @@ public class CurrentThreadCaptureQueriesListener extends BaseCaptureQueriesListe */ public static void startCapturing() { ourQueues.set(new ArrayDeque<>()); + ourGetConnections.set(new AtomicInteger(0)); ourCommits.set(new AtomicInteger(0)); ourRollbacks.set(new AtomicInteger(0)); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java index 14813f8b394..db777db0c33 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.validation; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; @@ -75,10 +76,16 @@ public class ValidatorResourceFetcher implements IValidatorResourceFetcher { target = dao.read(id, (RequestDetails) appContext); } catch (ResourceNotFoundException e) { ourLog.info("Failed to resolve local reference: {}", theUrl); - try { - target = fetchByUrl(theUrl, dao, (RequestDetails) appContext); - } catch (ResourceNotFoundException e2) { - ourLog.info("Failed to find resource by URL: {}", theUrl); + + RuntimeResourceDefinition def = myFhirContext.getResourceDefinition(resourceType); + if (def.getChildByName("url") != null) { + try { + target = fetchByUrl(theUrl, dao, (RequestDetails) appContext); + } catch (ResourceNotFoundException e2) { + ourLog.info("Failed to find resource by URL: {}", theUrl); + return null; + } + } else { return null; } } @@ -86,7 +93,7 @@ public class ValidatorResourceFetcher implements IValidatorResourceFetcher { return new JsonParser(myVersionSpecificContextWrapper) .parse(myFhirContext.newJsonParser().encodeResourceToString(target), resourceType); } catch (Exception e) { - throw new FHIRException(Msg.code(576) + e); + throw new FHIRException(Msg.code(576) + e, e); } } diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index d8d55d0d193..c14dc3a7001 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 12ef01f1c22..7cc77115a18 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index eb06ff89d41..6d1abc83f12 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index adc0100a995..2d678f794fc 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index b862ed06928..81332f0b285 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index 0ecd3520d9b..e9bb68a0ced 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index fb636ac3592..20fc936b45e 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 21ac27a7bfa..8f3810005ca 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 7cca45e4626..b726d4f06b9 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index c96a4fa6d72..771c5dfce90 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 - 7.7.7-SNAPSHOT + 7.7.8-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 9e1cb753cc3..97d409315ea 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 - 7.7.7-SNAPSHOT + 7.7.8-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 9aa79b6c5bd..a049c6e0326 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 - 7.7.7-SNAPSHOT + 7.7.8-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 65bc10b13f4..8758c8a3f27 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4b/pom.xml b/hapi-fhir-validation-resources-r4b/pom.xml index 69417a1d0eb..70968ba423b 100644 --- a/hapi-fhir-validation-resources-r4b/pom.xml +++ b/hapi-fhir-validation-resources-r4b/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-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 e0a2ac82d76..a08bea7582d 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index a4067215f26..7991424e435 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -256,6 +256,16 @@ ${project.version} test + + io.opentelemetry.javaagent + opentelemetry-testing-common + test + + + io.opentelemetry.javaagent + opentelemetry-agent-for-testing + test + commons-lang diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 7edb7effd39..d62b19d2ecc 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -1,51 +1,14 @@ package org.hl7.fhir.common.hapi.validation.support; -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.context.support.LookupCodeRequest; -import ca.uhn.fhir.context.support.TranslateConceptResults; -import ca.uhn.fhir.context.support.ValidationSupportContext; -import ca.uhn.fhir.context.support.ValueSetExpansionOptions; -import ca.uhn.fhir.sl.cache.Cache; -import ca.uhn.fhir.sl.cache.CacheFactory; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.time.DateUtils; -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 java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; -import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -@SuppressWarnings("unchecked") +/** + * @deprecated This should no longer be used, caching functionality is provided by {@link ValidationSupportChain} + */ +@Deprecated(since = "8.0.0", forRemoval = true) public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport { - private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class); - public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); - - private final Cache myCache; - private final Cache myValidateCodeCache; - private final Cache myTranslateCodeCache; - private final Cache myLookupCodeCache; - private final ThreadPoolExecutor myBackgroundExecutor; - private final Map myNonExpiringCache; - private final Cache myExpandValueSetCache; private final boolean myIsEnabledValidationForCodingsLogicalAnd; /** @@ -76,312 +39,53 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple CacheTimeouts theCacheTimeouts, boolean theIsEnabledValidationForCodingsLogicalAnd) { super(theWrap.getFhirContext(), theWrap); - myExpandValueSetCache = CacheFactory.build(theCacheTimeouts.getExpandValueSetMillis(), 100); - myValidateCodeCache = CacheFactory.build(theCacheTimeouts.getValidateCodeMillis(), 5000); - myLookupCodeCache = CacheFactory.build(theCacheTimeouts.getLookupCodeMillis(), 5000); - myTranslateCodeCache = CacheFactory.build(theCacheTimeouts.getTranslateCodeMillis(), 5000); - myCache = CacheFactory.build(theCacheTimeouts.getMiscMillis(), 5000); - myNonExpiringCache = Collections.synchronizedMap(new HashMap<>()); - - LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); - BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() - .namingPattern("CachingValidationSupport-%d") - .daemon(false) - .priority(Thread.NORM_PRIORITY) - .build(); - myBackgroundExecutor = new ThreadPoolExecutor( - 1, 1, 0L, TimeUnit.MILLISECONDS, executorQueue, threadFactory, new ThreadPoolExecutor.DiscardPolicy()); - myIsEnabledValidationForCodingsLogicalAnd = theIsEnabledValidationForCodingsLogicalAnd; } - @Override - public List fetchAllConformanceResources() { - String key = "fetchAllConformanceResources"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources()); - } - - @Override - public List fetchAllStructureDefinitions() { - String key = "fetchAllStructureDefinitions"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions()); - } - - @Nullable - @Override - public List fetchAllSearchParameters() { - String key = "fetchAllSearchParameters"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllSearchParameters()); - } - - @Override - public List fetchAllNonBaseStructureDefinitions() { - String key = "fetchAllNonBaseStructureDefinitions"; - return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions()); - } - - @Override - public IBaseResource fetchCodeSystem(String theSystem) { - return loadFromCache(myCache, "fetchCodeSystem " + theSystem, t -> super.fetchCodeSystem(theSystem)); - } - - @Override - public IBaseResource fetchValueSet(String theUri) { - return loadFromCache(myCache, "fetchValueSet " + theUri, t -> super.fetchValueSet(theUri)); - } - - @Override - public IBaseResource fetchStructureDefinition(String theUrl) { - return loadFromCache( - myCache, "fetchStructureDefinition " + theUrl, t -> super.fetchStructureDefinition(theUrl)); - } - - @Override - public byte[] fetchBinary(String theBinaryKey) { - return loadFromCache(myCache, "fetchBinary " + theBinaryKey, t -> super.fetchBinary(theBinaryKey)); - } - - @Override - public T fetchResource(@Nullable Class theClass, String theUri) { - return loadFromCache( - myCache, "fetchResource " + theClass + " " + theUri, t -> super.fetchResource(theClass, theUri)); - } - - @Override - public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { - String key = "isCodeSystemSupported " + theSystem; - Boolean retVal = loadFromCacheReentrantSafe( - myCache, key, t -> super.isCodeSystemSupported(theValidationSupportContext, theSystem)); - assert retVal != null; - return retVal; - } - - @Override - public ValueSetExpansionOutcome expandValueSet( - ValidationSupportContext theValidationSupportContext, - ValueSetExpansionOptions theExpansionOptions, - @Nonnull IBaseResource theValueSetToExpand) { - if (!theValueSetToExpand.getIdElement().hasIdPart()) { - return super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand); - } - - ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); - String key = "expandValueSet " + theValueSetToExpand.getIdElement().getValue() - + " " + expansionOptions.isIncludeHierarchy() - + " " + expansionOptions.getFilter() - + " " + expansionOptions.getOffset() - + " " + expansionOptions.getCount(); - return loadFromCache( - myExpandValueSetCache, - key, - t -> super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand)); - } - - @Override - public CodeValidationResult validateCode( - @Nonnull ValidationSupportContext theValidationSupportContext, - @Nonnull ConceptValidationOptions theOptions, - String theCodeSystem, - String theCode, - String theDisplay, - String theValueSetUrl) { - String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultString(theDisplay) + " " - + defaultIfBlank(theValueSetUrl, "NO_VS"); - return loadFromCache( - myValidateCodeCache, - key, - t -> super.validateCode( - theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl)); - } - - @Override - public LookupCodeResult lookupCode( - ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { - String key = "lookupCode " + theLookupCodeRequest.getSystem() + " " - + theLookupCodeRequest.getCode() - + " " + defaultIfBlank(theLookupCodeRequest.getDisplayLanguage(), "NO_LANG") - + " " + theLookupCodeRequest.getPropertyNames().toString(); - return loadFromCache( - myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theLookupCodeRequest)); - } - - @Override - public IValidationSupport.CodeValidationResult validateCodeInValueSet( - ValidationSupportContext theValidationSupportContext, - ConceptValidationOptions theValidationOptions, - String theCodeSystem, - String theCode, - String theDisplay, - @Nonnull IBaseResource theValueSet) { - - BaseRuntimeChildDefinition urlChild = - myCtx.getResourceDefinition(theValueSet).getChildByName("url"); - Optional valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream() - .map(t -> ((IPrimitiveType) t).getValueAsString()) - .filter(t -> isNotBlank(t)) - .findFirst(); - if (valueSetUrl.isPresent()) { - String key = - "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem) - + " " + defaultString(theCode) + " " + defaultString(theDisplay) + " " + valueSetUrl.get(); - return loadFromCache( - myValidateCodeCache, - key, - t -> super.validateCodeInValueSet( - theValidationSupportContext, - theValidationOptions, - theCodeSystem, - theCode, - theDisplay, - theValueSet)); - } - - return super.validateCodeInValueSet( - theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet); - } - - @Override - public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { - return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest)); - } - - @SuppressWarnings("OptionalAssignedToNull") - @Nullable - private T loadFromCache(Cache theCache, S theKey, Function theLoader) { - ourLog.trace("Fetching from cache: {}", theKey); - - Function> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey)); - Optional result = (Optional) theCache.get(theKey, loaderWrapper); - assert result != null; - - // UGH! Animal sniffer :( - if (!result.isPresent()) { - ourLog.debug( - "Invalidating cache entry for key: {} since the result of the underlying query is empty", theKey); - theCache.invalidate(theKey); - } - - return result.orElse(null); - } - - /** - * The Caffeine cache uses ConcurrentHashMap which is not reentrant, so if we get unlucky and the hashtable - * needs to grow at the same time as we are in a reentrant cache lookup, the thread will deadlock. Use this - * method in place of loadFromCache in situations where a cache lookup calls another cache lookup within its lambda - */ - @Nullable - private T loadFromCacheReentrantSafe(Cache theCache, S theKey, Function theLoader) { - ourLog.trace("Reentrant fetch from cache: {}", theKey); - - Optional result = (Optional) theCache.getIfPresent(theKey); - if (result != null && result.isPresent()) { - return result.get(); - } - T value = theLoader.apply(theKey); - assert value != null; - - theCache.put(theKey, Optional.of(value)); - - return value; - } - - private T loadFromCacheWithAsyncRefresh(Cache theCache, S theKey, Function theLoader) { - T retVal = (T) theCache.getIfPresent(theKey); - if (retVal == null) { - retVal = (T) myNonExpiringCache.get(theKey); - if (retVal != null) { - - Runnable loaderTask = () -> { - T loadedItem = loadFromCache(theCache, theKey, theLoader); - myNonExpiringCache.put(theKey, loadedItem); - }; - myBackgroundExecutor.execute(loaderTask); - - return retVal; - } - } - - retVal = loadFromCache(theCache, theKey, theLoader); - myNonExpiringCache.put(theKey, retVal); - return retVal; - } - - @Override - public void invalidateCaches() { - myExpandValueSetCache.invalidateAll(); - myLookupCodeCache.invalidateAll(); - myCache.invalidateAll(); - myValidateCodeCache.invalidateAll(); - myNonExpiringCache.clear(); - } - /** * @since 5.4.0 + * @deprecated */ + @Deprecated public static class CacheTimeouts { - private long myTranslateCodeMillis; - private long myLookupCodeMillis; - private long myValidateCodeMillis; - private long myMiscMillis; - private long myExpandValueSetMillis; - - public long getExpandValueSetMillis() { - return myExpandValueSetMillis; - } - public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) { - myExpandValueSetMillis = theExpandValueSetMillis; + // nothing return this; } - public long getTranslateCodeMillis() { - return myTranslateCodeMillis; - } - public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) { - myTranslateCodeMillis = theTranslateCodeMillis; + // nothing return this; } - public long getLookupCodeMillis() { - return myLookupCodeMillis; - } - public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) { - myLookupCodeMillis = theLookupCodeMillis; + // nothibng return this; } - public long getValidateCodeMillis() { - return myValidateCodeMillis; - } - public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) { - myValidateCodeMillis = theValidateCodeMillis; + // nothing return this; } - public long getMiscMillis() { - return myMiscMillis; - } - public CacheTimeouts setMiscMillis(long theMiscMillis) { - myMiscMillis = theMiscMillis; + // nothing return this; } public static CacheTimeouts defaultValues() { return new CacheTimeouts() .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) - .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE) + .setExpandValueSetMillis(DateUtils.MILLIS_PER_MINUTE) .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE); } } - public boolean isEnabledValidationForCodingsLogicalAnd() { + @Override + public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { return myIsEnabledValidationForCodingsLogicalAnd; } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java index a4cf3642b0a..1b4e3ccb2f7 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/SnapshotGeneratingValidationSupport.java @@ -16,6 +16,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,14 +40,23 @@ public class SnapshotGeneratingValidationSupport implements IValidationSupport { private static final Logger ourLog = LoggerFactory.getLogger(SnapshotGeneratingValidationSupport.class); private final FhirContext myCtx; private final VersionCanonicalizer myVersionCanonicalizer; + private final IWorkerContext myWorkerContext; + private final FHIRPathEngine myFHIRPathEngine; /** * Constructor */ - public SnapshotGeneratingValidationSupport(FhirContext theCtx) { - Validate.notNull(theCtx); - myCtx = theCtx; - myVersionCanonicalizer = new VersionCanonicalizer(theCtx); + public SnapshotGeneratingValidationSupport(FhirContext theFhirContext) { + this(theFhirContext, null, null); + } + + public SnapshotGeneratingValidationSupport( + FhirContext theFhirContext, IWorkerContext theWorkerContext, FHIRPathEngine theFHIRPathEngine) { + Validate.notNull(theFhirContext); + myCtx = theFhirContext; + myVersionCanonicalizer = new VersionCanonicalizer(theFhirContext); + myWorkerContext = theWorkerContext; + myFHIRPathEngine = theFHIRPathEngine; } @SuppressWarnings("EnhancedSwitchMigration") @@ -97,9 +107,17 @@ public class SnapshotGeneratingValidationSupport implements IValidationSupport { ArrayList messages = new ArrayList<>(); ProfileKnowledgeProvider profileKnowledgeProvider = new ProfileKnowledgeWorkerR5(myCtx); - IWorkerContext context = - new VersionSpecificWorkerContextWrapper(theValidationSupportContext, myVersionCanonicalizer); - ProfileUtilities profileUtilities = new ProfileUtilities(context, messages, profileKnowledgeProvider); + + ProfileUtilities profileUtilities; + if (myWorkerContext == null) { + IWorkerContext context = + new VersionSpecificWorkerContextWrapper(theValidationSupportContext, myVersionCanonicalizer); + profileUtilities = new ProfileUtilities(context, messages, profileKnowledgeProvider); + } else { + profileUtilities = + new ProfileUtilities(myWorkerContext, messages, profileKnowledgeProvider, myFHIRPathEngine); + } + profileUtilities.generateSnapshot(baseCanonical, inputCanonical, theUrl, theWebUrl, theProfileName); switch (getFhirVersionEnum( diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java index 4eea50efc11..16b3dadb3da 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChain.java @@ -1,5 +1,6 @@ package org.hl7.fhir.common.hapi.validation.support; +import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; @@ -10,40 +11,231 @@ import ca.uhn.fhir.context.support.TranslateConceptResults; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.sl.cache.Cache; +import ca.uhn.fhir.sl.cache.CacheFactory; +import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.Logs; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; +import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.function.Supplier; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +/** + * This validation support module has two primary purposes: It can be used to + * chain multiple backing modules together, and it can optionally cache the + * results. + *

    + * The following chaining logic is used: + *

      + *
    • + * Calls to {@literal fetchAll...} methods such as {@link #fetchAllConformanceResources()} + * and {@link #fetchAllStructureDefinitions()} will call every method in the chain in + * order, and aggregate the results into a single list to return. + *
    • + *
    • + * Calls to fetch or validate codes, such as {@link #validateCode(ValidationSupportContext, ConceptValidationOptions, String, String, String, String)} + * and {@link #lookupCode(ValidationSupportContext, LookupCodeRequest)} will first test + * each module in the chain using the {@link #isCodeSystemSupported(ValidationSupportContext, String)} + * or {@link #isValueSetSupported(ValidationSupportContext, String)} + * methods (depending on whether a ValueSet URL is present in the method parameters) + * and will invoke any methods in the chain which return that they can handle the given + * CodeSystem/ValueSet URL. The first non-null value returned by a method in the chain + * that can support the URL will be returned to the caller. + *
    • + *
    • + * All other methods will invoke each method in the chain in order, and will stop processing and return + * immediately as soon as the first non-null value is returned. + *
    • + *
    + *

    + *

    + * The following caching logic is used if caching is enabled using {@link CacheConfiguration}. + * You can use {@link CacheConfiguration#disabled()} if you want to disable caching. + *

      + *
    • + * Calls to fetch StructureDefinitions including {@link #fetchAllStructureDefinitions()} + * and {@link #fetchStructureDefinition(String)} are cached in a non-expiring cache. + * This is because the {@link org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator} + * module makes assumptions that these objects will not change for the lifetime + * of the validator for performance reasons. + *
    • + *
    • + * Calls to all other {@literal fetchAll...} methods including + * {@link #fetchAllConformanceResources()} and {@link #fetchAllSearchParameters()} + * cache their results in an expiring cache, but will refresh that cache asynchronously. + *
    • + *
    • + * Results of {@link #generateSnapshot(ValidationSupportContext, IBaseResource, String, String, String)} + * are not cached, since this method is generally called in contexts where the results + * are cached. + *
    • + *
    • + * Results of all other methods are stored in an expiring cache. + *
    • + *
    + *

    + *

    + * Note that caching functionality used to be provided by a separate provider + * called {@literal CachingValidationSupport} but that functionality has been + * moved into this class as of HAPI FHIR 8.0.0, because it is possible to + * provide a more efficient chain when these functions are combined. + *

    + */ public class ValidationSupportChain implements IValidationSupport { + public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); static Logger ourLog = Logs.getTerminologyTroubleshootingLog(); + private final List myChain = new ArrayList<>(); - private List myChain; + @Nullable + private final Cache, Object> myExpiringCache; + + @Nullable + private final Map, Object> myNonExpiringCache; /** - * Constructor + * See class documentation for an explanation of why this is separate + * and non-expiring. Note that this field is non-synchronized. If you + * access it, you should first wrap the call in + * synchronized(myStructureDefinitionsByUrl). + */ + @Nonnull + private final Map myStructureDefinitionsByUrl = new HashMap<>(); + /** + * See class documentation for an explanation of why this is separate + * and non-expiring. Note that this field is non-synchronized. If you + * access it, you should first wrap the call in + * synchronized(myStructureDefinitionsByUrl) (synchronize on + * the other field because both collections are expected to be modified + * at the same time). + */ + @Nonnull + private final List myStructureDefinitionsAsList = new ArrayList<>(); + + private final ThreadPoolExecutor myBackgroundExecutor; + private final CacheConfiguration myCacheConfiguration; + private boolean myEnabledValidationForCodingsLogicalAnd; + private String myName = getClass().getSimpleName(); + private ValidationSupportChainMetrics myMetrics; + private volatile boolean myHaveFetchedAllStructureDefinitions = false; + + /** + * Constructor which initializes the chain with no modules (modules + * must subsequently be registered using {@link #addValidationSupport(IValidationSupport)}). + * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. */ public ValidationSupportChain() { - myChain = new ArrayList<>(); + /* + * Note, this constructor is called by default when + * FhirContext#getValidationSupport() is called, so it should + * provide sensible defaults. + */ + this(Collections.emptyList()); + } + + /** + * Constructor which initializes the chain with the given modules. + * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. + */ + public ValidationSupportChain(IValidationSupport... theValidationSupportModules) { + this( + theValidationSupportModules != null + ? Arrays.asList(theValidationSupportModules) + : Collections.emptyList()); + } + + /** + * Constructor which initializes the chain with the given modules. + * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. + */ + public ValidationSupportChain(List theValidationSupportModules) { + this(CacheConfiguration.defaultValues(), theValidationSupportModules); } /** * Constructor + * + * @param theCacheConfiguration The caching configuration + * @param theValidationSupportModules The initial modules to add to the chain */ - public ValidationSupportChain(IValidationSupport... theValidationSupportModules) { - this(); + public ValidationSupportChain( + @Nonnull CacheConfiguration theCacheConfiguration, IValidationSupport... theValidationSupportModules) { + this( + theCacheConfiguration, + theValidationSupportModules != null + ? Arrays.asList(theValidationSupportModules) + : Collections.emptyList()); + } + + /** + * Constructor + * + * @param theCacheConfiguration The caching configuration + * @param theValidationSupportModules The initial modules to add to the chain + */ + public ValidationSupportChain( + @Nonnull CacheConfiguration theCacheConfiguration, + @Nonnull List theValidationSupportModules) { + + Validate.notNull(theCacheConfiguration, "theCacheConfiguration must not be null"); + Validate.notNull(theValidationSupportModules, "theValidationSupportModules must not be null"); + + myCacheConfiguration = theCacheConfiguration; + if (theCacheConfiguration.getCacheSize() == 0 || theCacheConfiguration.getCacheTimeout() == 0) { + myExpiringCache = null; + myNonExpiringCache = null; + myBackgroundExecutor = null; + } else { + myExpiringCache = + CacheFactory.build(theCacheConfiguration.getCacheTimeout(), theCacheConfiguration.getCacheSize()); + myNonExpiringCache = Collections.synchronizedMap(new HashMap<>()); + + LinkedBlockingQueue executorQueue = new LinkedBlockingQueue<>(1000); + BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() + .namingPattern("CachingValidationSupport-%d") + .daemon(false) + .priority(Thread.NORM_PRIORITY) + .build(); + + // NOTE: We're not using ThreadPoolUtil here, because that class depends on Spring and + // we want the validator infrastructure to not require spring dependencies. + myBackgroundExecutor = new ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + executorQueue, + threadFactory, + new ThreadPoolExecutor.DiscardPolicy()); + } + for (IValidationSupport next : theValidationSupportModules) { if (next != null) { addValidationSupport(next); @@ -51,63 +243,174 @@ public class ValidationSupportChain implements IValidationSupport { } } + @Override + public String getName() { + return myName; + } + + /** + * Sets a name for this chain. This name will be returned by + * {@link #getName()} and used by OpenTelemetry. + */ + public void setName(String theName) { + Validate.notBlank(theName, "theName must not be blank"); + myName = theName; + } + + @PostConstruct + public void start() { + if (myMetrics == null) { + myMetrics = new ValidationSupportChainMetrics(this); + myMetrics.start(); + } + } + + @PreDestroy + public void stop() { + if (myMetrics != null) { + myMetrics.stop(); + myMetrics = null; + } + } + + @Override + public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { + return myEnabledValidationForCodingsLogicalAnd; + } + + /** + * When validating a CodeableConcept containing multiple codings, this method can be used to control whether + * the validator requires all codings in the CodeableConcept to be valid in order to consider the + * CodeableConcept valid. + *

    + * See VersionSpecificWorkerContextWrapper#validateCode in hapi-fhir-validation, and the refer to the values below + * for the behaviour associated with each value. + *

    + *

    + *

      + *
    • If false (default setting) the validation for codings will return a positive result only if + * ALL codings are valid.
    • + *
    • If true the validation for codings will return a positive result if ANY codings are valid. + *
    • + *
    + *

    + * + * @return true or false depending on the desired coding validation behaviour. + */ + public ValidationSupportChain setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid( + boolean theEnabledValidationForCodingsLogicalAnd) { + myEnabledValidationForCodingsLogicalAnd = theEnabledValidationForCodingsLogicalAnd; + return this; + } + @Override public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { - TranslateConceptResults retVal = null; - for (IValidationSupport next : myChain) { - TranslateConceptResults translations = next.translateConcept(theRequest); - if (translations != null) { - if (retVal == null) { - retVal = new TranslateConceptResults(); - } + TranslateConceptKey key = new TranslateConceptKey(theRequest); + CacheValue retVal = getFromCache(key); + if (retVal == null) { - if (retVal.getMessage() == null) { - retVal.setMessage(translations.getMessage()); - } + /* + * The chain behaviour for this method is to call every element in the + * chain and aggregate the results (as opposed to just using the first + * module which provides a response). + */ + retVal = CacheValue.empty(); - if (translations.getResult() && !retVal.getResult()) { - retVal.setResult(translations.getResult()); - retVal.setMessage(translations.getMessage()); - } + TranslateConceptResults outcome = null; + for (IValidationSupport next : myChain) { + TranslateConceptResults translations = next.translateConcept(theRequest); + if (translations != null) { + if (outcome == null) { + outcome = new TranslateConceptResults(); + } - if (!translations.isEmpty()) { - if (ourLog.isDebugEnabled()) { + if (outcome.getMessage() == null) { + outcome.setMessage(translations.getMessage()); + } + + if (translations.getResult() && !outcome.getResult()) { + outcome.setResult(translations.getResult()); + outcome.setMessage(translations.getMessage()); + } + + if (!translations.isEmpty()) { ourLog.debug( "{} found {} concept translation{} for {}", next.getName(), translations.size(), translations.size() > 1 ? "s" : "", theRequest); + outcome.getResults().addAll(translations.getResults()); } - retVal.getResults().addAll(translations.getResults()); } } + + if (outcome != null) { + retVal = new CacheValue<>(outcome); + } + + putInCache(key, retVal); } - return retVal; + + return retVal.getValue(); } @Override public void invalidateCaches() { ourLog.debug("Invalidating caches in {} validation support modules", myChain.size()); + myHaveFetchedAllStructureDefinitions = false; for (IValidationSupport next : myChain) { next.invalidateCaches(); } + if (myNonExpiringCache != null) { + myNonExpiringCache.clear(); + } + if (myExpiringCache != null) { + myExpiringCache.invalidateAll(); + } + synchronized (myStructureDefinitionsByUrl) { + myStructureDefinitionsByUrl.clear(); + myStructureDefinitionsAsList.clear(); + } + } + + /** + * Invalidate the expiring cache, but not the permanent StructureDefinition cache + * + * @since 8.0.0 + */ + public void invalidateExpiringCaches() { + if (myExpiringCache != null) { + myExpiringCache.invalidateAll(); + } } @Override public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { for (IValidationSupport next : myChain) { - boolean retVal = next.isValueSetSupported(theValidationSupportContext, theValueSetUrl); + boolean retVal = isValueSetSupported(theValidationSupportContext, next, theValueSetUrl); if (retVal) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName()); - } + ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName()); return true; } } return false; } + private boolean isValueSetSupported( + ValidationSupportContext theValidationSupportContext, + IValidationSupport theValidationSupport, + String theValueSetUrl) { + IsValueSetSupportedKey key = new IsValueSetSupportedKey(theValidationSupport, theValueSetUrl); + CacheValue value = getFromCache(key); + if (value == null) { + value = new CacheValue<>( + theValidationSupport.isValueSetSupported(theValidationSupportContext, theValueSetUrl)); + putInCache(key, value); + } + return value.getValue(); + } + @Override public IBaseResource generateSnapshot( ValidationSupportContext theValidationSupportContext, @@ -115,13 +418,22 @@ public class ValidationSupportChain implements IValidationSupport { String theUrl, String theWebUrl, String theProfileName) { + + /* + * No caching for this method because we typically cache the results anyhow. + * If this ever changes, make sure to update the class javadocs and the + * HAPI FHIR documentation which indicate that this isn't cached. + */ + for (IValidationSupport next : myChain) { IBaseResource retVal = next.generateSnapshot(theValidationSupportContext, theInput, theUrl, theWebUrl, theProfileName); if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("Profile snapshot for {} generated by {}", theInput.getIdElement(), next.getName()); - } + ourLog.atDebug() + .setMessage("Profile snapshot for {} generated by {}") + .addArgument(() -> theInput.getIdElement()) + .addArgument(() -> next.getName()) + .log(); return retVal; } } @@ -130,7 +442,7 @@ public class ValidationSupportChain implements IValidationSupport { @Override public FhirContext getFhirContext() { - if (myChain.size() == 0) { + if (myChain.isEmpty()) { return null; } return myChain.get(0).getFhirContext(); @@ -160,6 +472,7 @@ public class ValidationSupportChain implements IValidationSupport { */ public void addValidationSupport(int theIndex, IValidationSupport theValidationSupport) { Validate.notNull(theValidationSupport, "theValidationSupport must not be null"); + invalidateCaches(); if (theValidationSupport.getFhirContext() == null) { String message = "Can not add validation support: getFhirContext() returns null"; @@ -189,58 +502,134 @@ public class ValidationSupportChain implements IValidationSupport { myChain.remove(theValidationSupport); } + @Nullable + @Override + public ValueSetExpansionOutcome expandValueSet( + ValidationSupportContext theValidationSupportContext, + @Nullable ValueSetExpansionOptions theExpansionOptions, + @Nonnull String theValueSetUrlToExpand) + throws ResourceNotFoundException { + ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); + ExpandValueSetKey key = new ExpandValueSetKey(expansionOptions, null, theValueSetUrlToExpand); + CacheValue retVal = getFromCache(key); + + if (retVal == null) { + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + if (isValueSetSupported(theValidationSupportContext, next, theValueSetUrlToExpand)) { + ValueSetExpansionOutcome expanded = + next.expandValueSet(theValidationSupportContext, expansionOptions, theValueSetUrlToExpand); + if (expanded != null) { + ourLog.debug("ValueSet {} expanded by URL by {}", theValueSetUrlToExpand, next.getName()); + retVal = new CacheValue<>(expanded); + break; + } + } + } + + putInCache(key, retVal); + } + + return retVal.getValue(); + } + @Override public ValueSetExpansionOutcome expandValueSet( ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) { - for (IValidationSupport next : myChain) { - // TODO: test if code system is supported? - ValueSetExpansionOutcome expanded = - next.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand); - if (expanded != null) { - if (ourLog.isDebugEnabled()) { + + ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); + String id = theValueSetToExpand.getIdElement().getValue(); + ExpandValueSetKey key = null; + CacheValue retVal = null; + if (isNotBlank(id)) { + key = new ExpandValueSetKey(expansionOptions, id, null); + retVal = getFromCache(key); + } + if (retVal == null) { + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + ValueSetExpansionOutcome expanded = + next.expandValueSet(theValidationSupportContext, expansionOptions, theValueSetToExpand); + if (expanded != null) { ourLog.debug("ValueSet {} expanded by {}", theValueSetToExpand.getIdElement(), next.getName()); + retVal = new CacheValue<>(expanded); + break; } - return expanded; + } + + if (key != null) { + putInCache(key, retVal); } } - return null; + + return retVal.getValue(); } @Override public boolean isRemoteTerminologyServiceConfigured() { - if (myChain != null) { - Optional remoteTerminologyService = myChain.stream() - .filter(RemoteTerminologyServiceValidationSupport.class::isInstance) - .findFirst(); - if (remoteTerminologyService.isPresent()) { - return true; - } - } - return false; + return myChain.stream().anyMatch(RemoteTerminologyServiceValidationSupport.class::isInstance); } @Override public List fetchAllConformanceResources() { - List retVal = new ArrayList<>(); - for (IValidationSupport next : myChain) { - List candidates = next.fetchAllConformanceResources(); - if (candidates != null) { - retVal.addAll(candidates); + FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL); + Supplier> loader = () -> { + List allCandidates = new ArrayList<>(); + for (IValidationSupport next : myChain) { + List candidates = next.fetchAllConformanceResources(); + if (candidates != null) { + allCandidates.addAll(candidates); + } } - } - return retVal; + return allCandidates; + }; + + return getFromCacheWithAsyncRefresh(key, loader); } + @SuppressWarnings("unchecked") @Override + @Nonnull public List fetchAllStructureDefinitions() { - return doFetchStructureDefinitions(t -> t.fetchAllStructureDefinitions()); + if (!myHaveFetchedAllStructureDefinitions) { + FhirTerser terser = getFhirContext().newTerser(); + List allStructureDefinitions = + doFetchStructureDefinitions(IValidationSupport::fetchAllStructureDefinitions); + if (myExpiringCache != null) { + synchronized (myStructureDefinitionsByUrl) { + for (IBaseResource structureDefinition : allStructureDefinitions) { + String url = terser.getSinglePrimitiveValueOrNull(structureDefinition, "url"); + url = defaultIfBlank(url, UUID.randomUUID().toString()); + if (myStructureDefinitionsByUrl.putIfAbsent(url, structureDefinition) == null) { + myStructureDefinitionsAsList.add(structureDefinition); + } + } + } + } + myHaveFetchedAllStructureDefinitions = true; + } + return Collections.unmodifiableList(new ArrayList<>(myStructureDefinitionsAsList)); } + @SuppressWarnings("unchecked") @Override public List fetchAllNonBaseStructureDefinitions() { - return doFetchStructureDefinitions(t -> t.fetchAllNonBaseStructureDefinitions()); + FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_NON_BASE_STRUCTUREDEFINITIONS); + Supplier> loader = + () -> doFetchStructureDefinitions(IValidationSupport::fetchAllNonBaseStructureDefinitions); + return getFromCacheWithAsyncRefresh(key, loader); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public List fetchAllSearchParameters() { + FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_SEARCHPARAMETERS); + Supplier> loader = + () -> doFetchStructureDefinitions(IValidationSupport::fetchAllSearchParameters); + return (List) getFromCacheWithAsyncRefresh(key, loader); } private List doFetchStructureDefinitions( @@ -267,84 +656,98 @@ public class ValidationSupportChain implements IValidationSupport { @Override public IBaseResource fetchCodeSystem(String theSystem) { - for (IValidationSupport next : myChain) { - IBaseResource retVal = next.fetchCodeSystem(theSystem); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "CodeSystem {} with System {} fetched by {}", - retVal.getIdElement(), - theSystem, - next.getName()); + Function invoker = v -> v.fetchCodeSystem(theSystem); + ResourceByUrlKey key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.CODESYSTEM, theSystem); + return fetchValue(key, invoker, theSystem); + } + + private T fetchValue(ResourceByUrlKey theKey, Function theInvoker, String theUrl) { + CacheValue retVal = getFromCache(theKey); + + if (retVal == null) { + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + T outcome = theInvoker.apply(next); + if (outcome != null) { + ourLog.debug("{} {} with URL {} fetched by {}", theKey.myType, outcome, theUrl, next.getName()); + retVal = new CacheValue<>(outcome); + break; } - return retVal; } + putInCache(theKey, retVal); } - return null; + + return retVal.getValue(); } @Override public IBaseResource fetchValueSet(String theUrl) { - for (IValidationSupport next : myChain) { - IBaseResource retVal = next.fetchValueSet(theUrl); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "ValueSet {} with URL {} fetched by {}", retVal.getIdElement(), theUrl, next.getName()); - } - return retVal; - } - } - return null; + Function invoker = v -> v.fetchValueSet(theUrl); + ResourceByUrlKey key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.VALUESET, theUrl); + return fetchValue(key, invoker, theUrl); } + @SuppressWarnings("unchecked") @Override public T fetchResource(Class theClass, String theUri) { - for (IValidationSupport next : myChain) { - T retVal = next.fetchResource(theClass, theUri); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "Resource {} with URI {} fetched by {}", retVal.getIdElement(), theUri, next.getName()); + + /* + * If we're looking for a common type with a dedicated fetch method, use that + * so that we can use a common cache location for lookups wanting a given + * URL on both methods (the validator will call both paths when looking for a + * specific URL so this improves cache efficiency). + */ + if (theClass != null) { + BaseRuntimeElementDefinition elementDefinition = getFhirContext().getElementDefinition(theClass); + if (elementDefinition != null) { + switch (elementDefinition.getName()) { + case "ValueSet": + return (T) fetchValueSet(theUri); + case "CodeSystem": + return (T) fetchCodeSystem(theUri); + case "StructureDefinition": + return (T) fetchStructureDefinition(theUri); } - return retVal; } } - return null; + + Function invoker = v -> v.fetchResource(theClass, theUri); + TypedResourceByUrlKey key = new TypedResourceByUrlKey<>(theClass, theUri); + return fetchValue(key, invoker, theUri); } @Override - public byte[] fetchBinary(String key) { - for (IValidationSupport next : myChain) { - byte[] retVal = next.fetchBinary(key); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("Binary with key {} fetched by {}", key, next.getName()); - } - return retVal; - } - } - return null; + public byte[] fetchBinary(String theKey) { + Function invoker = v -> v.fetchBinary(theKey); + ResourceByUrlKey key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.BINARY, theKey); + return fetchValue(key, invoker, theKey); } @Override public IBaseResource fetchStructureDefinition(String theUrl) { - for (IValidationSupport next : myChain) { - IBaseResource retVal = next.fetchStructureDefinition(theUrl); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug("StructureDefinition with URL {} fetched by {}", theUrl, next.getName()); + synchronized (myStructureDefinitionsByUrl) { + IBaseResource candidate = myStructureDefinitionsByUrl.get(theUrl); + if (candidate == null) { + Function invoker = v -> v.fetchStructureDefinition(theUrl); + ResourceByUrlKey key = + new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.STRUCTUREDEFINITION, theUrl); + candidate = fetchValue(key, invoker, theUrl); + if (myExpiringCache != null) { + if (candidate != null) { + if (myStructureDefinitionsByUrl.putIfAbsent(theUrl, candidate) == null) { + myStructureDefinitionsAsList.add(candidate); + } + } } - return retVal; } + return candidate; } - return null; } @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { for (IValidationSupport next : myChain) { - if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) { + if (isCodeSystemSupported(theValidationSupportContext, next, theSystem)) { if (ourLog.isDebugEnabled()) { ourLog.debug("CodeSystem with System {} is supported by {}", theSystem, next.getName()); } @@ -354,6 +757,20 @@ public class ValidationSupportChain implements IValidationSupport { return false; } + private boolean isCodeSystemSupported( + ValidationSupportContext theValidationSupportContext, + IValidationSupport theValidationSupport, + String theCodeSystemUrl) { + IsCodeSystemSupportedKey key = new IsCodeSystemSupportedKey(theValidationSupport, theCodeSystemUrl); + CacheValue value = getFromCache(key); + if (value == null) { + value = new CacheValue<>( + theValidationSupport.isCodeSystemSupported(theValidationSupportContext, theCodeSystemUrl)); + putInCache(key, value); + } + return value.getValue(); + } + @Override public CodeValidationResult validateCode( @Nonnull ValidationSupportContext theValidationSupportContext, @@ -362,14 +779,24 @@ public class ValidationSupportChain implements IValidationSupport { String theCode, String theDisplay, String theValueSetUrl) { - for (IValidationSupport next : myChain) { - if ((isBlank(theValueSetUrl) && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) - || (isNotBlank(theValueSetUrl) - && next.isValueSetSupported(theValidationSupportContext, theValueSetUrl))) { - CodeValidationResult retVal = next.validateCode( - theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { + + ValidateCodeKey key = new ValidateCodeKey(theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); + CacheValue retVal = getFromCache(key); + if (retVal == null) { + retVal = CacheValue.empty(); + + for (IValidationSupport next : myChain) { + if ((isBlank(theValueSetUrl) && isCodeSystemSupported(theValidationSupportContext, next, theCodeSystem)) + || (isNotBlank(theValueSetUrl) + && isValueSetSupported(theValidationSupportContext, next, theValueSetUrl))) { + CodeValidationResult outcome = next.validateCode( + theValidationSupportContext, + theOptions, + theCodeSystem, + theCode, + theDisplay, + theValueSetUrl); + if (outcome != null) { ourLog.debug( "Code {}|{} '{}' in ValueSet {} validated by {}", theCodeSystem, @@ -377,12 +804,16 @@ public class ValidationSupportChain implements IValidationSupport { theDisplay, theValueSetUrl, next.getName()); + retVal = new CacheValue<>(outcome); + break; } - return retVal; } } + + putInCache(key, retVal); } - return null; + + return retVal.getValue(); } @Override @@ -393,56 +824,522 @@ public class ValidationSupportChain implements IValidationSupport { String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { + String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); + + ValidateCodeKey key = null; + CacheValue retVal = null; + if (isNotBlank(url)) { + key = new ValidateCodeKey(theOptions, theCodeSystem, theCode, theDisplay, url); + retVal = getFromCache(key); + } + if (retVal != null) { + return retVal.getValue(); + } + + retVal = CacheValue.empty(); for (IValidationSupport next : myChain) { - String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); - if (isBlank(url) || next.isValueSetSupported(theValidationSupportContext, url)) { - CodeValidationResult retVal = next.validateCodeInValueSet( + if (isBlank(url) || isValueSetSupported(theValidationSupportContext, next, url)) { + CodeValidationResult outcome = next.validateCodeInValueSet( theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet); - if (retVal != null) { - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "Code {}|{} '{}' in ValueSet {} validated by {}", - theCodeSystem, - theCode, - theDisplay, - theValueSet.getIdElement(), - next.getName()); - } - return retVal; + if (outcome != null) { + ourLog.debug( + "Code {}|{} '{}' in ValueSet {} validated by {}", + theCodeSystem, + theCode, + theDisplay, + theValueSet.getIdElement(), + next.getName()); + retVal = new CacheValue<>(outcome); + break; } } } - return null; + + if (key != null) { + putInCache(key, retVal); + } + + return retVal.getValue(); } @Override public LookupCodeResult lookupCode( ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { - for (IValidationSupport next : myChain) { - final String system = theLookupCodeRequest.getSystem(); - final String code = theLookupCodeRequest.getCode(); - final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); - if (next.isCodeSystemSupported(theValidationSupportContext, system)) { - LookupCodeResult lookupCodeResult = next.lookupCode(theValidationSupportContext, theLookupCodeRequest); - if (lookupCodeResult == null) { - /* - This branch has been added as a fall-back mechanism for supporting lookupCode - methods marked as deprecated in interface IValidationSupport. - */ - lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage); + + LookupCodeKey key = new LookupCodeKey(theLookupCodeRequest); + CacheValue retVal = getFromCache(key); + if (retVal == null) { + + retVal = CacheValue.empty(); + for (IValidationSupport next : myChain) { + final String system = theLookupCodeRequest.getSystem(); + final String code = theLookupCodeRequest.getCode(); + final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); + if (isCodeSystemSupported(theValidationSupportContext, next, system)) { + LookupCodeResult lookupCodeResult = + next.lookupCode(theValidationSupportContext, theLookupCodeRequest); + if (lookupCodeResult == null) { + /* + This branch has been added as a fall-back mechanism for supporting lookupCode + methods marked as deprecated in interface IValidationSupport. + */ + //noinspection deprecation + lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage); + } + if (lookupCodeResult != null) { + ourLog.debug( + "Code {}|{}{} {} by {}", + system, + code, + isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")", + lookupCodeResult.isFound() ? "found" : "not found", + next.getName()); + retVal = new CacheValue<>(lookupCodeResult); + break; + } } - if (ourLog.isDebugEnabled()) { - ourLog.debug( - "Code {}|{}{} {} by {}", - system, - code, - isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")", - lookupCodeResult != null && lookupCodeResult.isFound() ? "found" : "not found", - next.getName()); - } - return lookupCodeResult; + } + + putInCache(key, retVal); + } + + return retVal.getValue(); + } + + /** + * Returns a view of the {@link IValidationSupport} modules within + * this chain. The returned collection is unmodifiable and will reflect + * changes to the underlying list. + * + * @since 8.0.0 + */ + public List getValidationSupports() { + return Collections.unmodifiableList(myChain); + } + + private void putInCache(BaseKey key, CacheValue theValue) { + if (myExpiringCache != null) { + myExpiringCache.put(key, theValue); + } + } + + @SuppressWarnings("unchecked") + private CacheValue getFromCache(BaseKey key) { + if (myExpiringCache != null) { + return (CacheValue) myExpiringCache.getIfPresent(key); + } else { + return null; + } + } + + @SuppressWarnings("unchecked") + private List getFromCacheWithAsyncRefresh( + FetchAllKey theKey, Supplier> theLoader) { + if (myExpiringCache == null || myNonExpiringCache == null) { + return theLoader.get(); + } + + CacheValue> retVal = getFromCache(theKey); + if (retVal == null) { + retVal = (CacheValue>) myNonExpiringCache.get(theKey); + if (retVal != null) { + Runnable loaderTask = () -> { + List loadedItem = theLoader.get(); + CacheValue> value = new CacheValue<>(loadedItem); + myNonExpiringCache.put(theKey, value); + putInCache(theKey, value); + }; + List returnValue = retVal.getValue(); + + myBackgroundExecutor.execute(loaderTask); + + return returnValue; + } else { + retVal = new CacheValue<>(theLoader.get()); + myNonExpiringCache.put(theKey, retVal); + putInCache(theKey, retVal); } } - return null; + + return retVal.getValue(); + } + + public void logCacheSizes() { + String b = "Cache sizes:" + "\n * Expiring: " + + (myExpiringCache != null ? myExpiringCache.estimatedSize() : "(disabled)") + + "\n * Non-Expiring: " + + (myNonExpiringCache != null ? myNonExpiringCache.size() : "(disabled)"); + ourLog.info(b); + } + + long getMetricExpiringCacheEntries() { + if (myExpiringCache != null) { + return myExpiringCache.estimatedSize(); + } else { + return 0; + } + } + + int getMetricNonExpiringCacheEntries() { + synchronized (myStructureDefinitionsByUrl) { + int size = myNonExpiringCache != null ? myNonExpiringCache.size() : 0; + return size + myStructureDefinitionsAsList.size(); + } + } + + int getMetricExpiringCacheMaxSize() { + return myCacheConfiguration.getCacheSize(); + } + + /** + * @since 5.4.0 + */ + public static class CacheConfiguration { + + private long myCacheTimeout; + private int myCacheSize; + + /** + * Non-instantiable. Use the factory methods. + */ + private CacheConfiguration() { + super(); + } + + public long getCacheTimeout() { + return myCacheTimeout; + } + + public CacheConfiguration setCacheTimeout(Duration theCacheTimeout) { + Validate.isTrue(theCacheTimeout.toMillis() >= 0, "Cache timeout must not be negative"); + myCacheTimeout = theCacheTimeout.toMillis(); + return this; + } + + public int getCacheSize() { + return myCacheSize; + } + + public CacheConfiguration setCacheSize(int theCacheSize) { + Validate.isTrue(theCacheSize >= 0, "Cache size must not be negative"); + myCacheSize = theCacheSize; + return this; + } + + /** + * Creates a cache configuration with sensible default values: + * 10 minutes expiry, and 5000 cache entries. + */ + public static CacheConfiguration defaultValues() { + return new CacheConfiguration() + .setCacheTimeout(Duration.ofMinutes(10)) + .setCacheSize(5000); + } + + public static CacheConfiguration disabled() { + return new CacheConfiguration().setCacheSize(0).setCacheTimeout(Duration.ofMillis(0)); + } + } + + /** + * @param The value type associated with this key + */ + @SuppressWarnings("unused") + abstract static class BaseKey { + + @Override + public abstract boolean equals(Object theO); + + @Override + public abstract int hashCode(); + } + + static class ExpandValueSetKey extends BaseKey { + + private final ValueSetExpansionOptions myOptions; + private final String myId; + private final String myUrl; + private final int myHashCode; + + private ExpandValueSetKey(ValueSetExpansionOptions theOptions, String theId, String theUrl) { + myOptions = theOptions; + myId = theId; + myUrl = theUrl; + myHashCode = Objects.hash(myOptions, myId, myUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ExpandValueSetKey)) return false; + ExpandValueSetKey that = (ExpandValueSetKey) theO; + return Objects.equals(myOptions, that.myOptions) + && Objects.equals(myId, that.myId) + && Objects.equals(myUrl, that.myUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class FetchAllKey extends BaseKey> { + + private final TypeEnum myType; + private final int myHashCode; + + private FetchAllKey(TypeEnum theType) { + myType = theType; + myHashCode = Objects.hash(myType); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof FetchAllKey)) return false; + FetchAllKey that = (FetchAllKey) theO; + return myType == that.myType; + } + + @Override + public int hashCode() { + return myHashCode; + } + + private enum TypeEnum { + ALL, + ALL_STRUCTUREDEFINITIONS, + ALL_NON_BASE_STRUCTUREDEFINITIONS, + ALL_SEARCHPARAMETERS + } + } + + static class ResourceByUrlKey extends BaseKey { + + private final TypeEnum myType; + private final String myUrl; + private final int myHashCode; + + private ResourceByUrlKey(TypeEnum theType, String theUrl) { + this(theType, theUrl, Objects.hash("ResourceByUrl", theType, theUrl)); + } + + private ResourceByUrlKey(TypeEnum theType, String theUrl, int theHashCode) { + myType = theType; + myUrl = theUrl; + myHashCode = theHashCode; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ResourceByUrlKey)) return false; + ResourceByUrlKey that = (ResourceByUrlKey) theO; + return myType == that.myType && Objects.equals(myUrl, that.myUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + + private enum TypeEnum { + CODESYSTEM, + VALUESET, + RESOURCE, + BINARY, + STRUCTUREDEFINITION + } + } + + static class TypedResourceByUrlKey extends ResourceByUrlKey { + + private final Class myType; + + private TypedResourceByUrlKey(Class theType, String theUrl) { + super(ResourceByUrlKey.TypeEnum.RESOURCE, theUrl, Objects.hash("TypedResourceByUrl", theType, theUrl)); + myType = theType; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof TypedResourceByUrlKey)) return false; + if (!super.equals(theO)) return false; + TypedResourceByUrlKey that = (TypedResourceByUrlKey) theO; + return Objects.equals(myType, that.myType); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), myType); + } + } + + static class IsValueSetSupportedKey extends BaseKey { + + private final String myValueSetUrl; + private final IValidationSupport myValidationSupport; + private final int myHashCode; + + private IsValueSetSupportedKey(IValidationSupport theValidationSupport, String theValueSetUrl) { + myValidationSupport = theValidationSupport; + myValueSetUrl = theValueSetUrl; + myHashCode = Objects.hash("IsValueSetSupported", theValidationSupport, myValueSetUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof IsValueSetSupportedKey)) return false; + IsValueSetSupportedKey that = (IsValueSetSupportedKey) theO; + return myValidationSupport == that.myValidationSupport && Objects.equals(myValueSetUrl, that.myValueSetUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class IsCodeSystemSupportedKey extends BaseKey { + + private final String myCodeSystemUrl; + private final IValidationSupport myValidationSupport; + private final int myHashCode; + + private IsCodeSystemSupportedKey(IValidationSupport theValidationSupport, String theCodeSystemUrl) { + myValidationSupport = theValidationSupport; + myCodeSystemUrl = theCodeSystemUrl; + myHashCode = Objects.hash("IsCodeSystemSupported", theValidationSupport, myCodeSystemUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof IsCodeSystemSupportedKey)) return false; + IsCodeSystemSupportedKey that = (IsCodeSystemSupportedKey) theO; + return myValidationSupport == that.myValidationSupport + && Objects.equals(myCodeSystemUrl, that.myCodeSystemUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class LookupCodeKey extends BaseKey { + + private final LookupCodeRequest myRequest; + private final int myHashCode; + + private LookupCodeKey(LookupCodeRequest theRequest) { + myRequest = theRequest; + myHashCode = Objects.hash("LookupCode", myRequest); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof LookupCodeKey)) return false; + LookupCodeKey that = (LookupCodeKey) theO; + return Objects.equals(myRequest, that.myRequest); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class TranslateConceptKey extends BaseKey { + + private final TranslateCodeRequest myRequest; + private final int myHashCode; + + private TranslateConceptKey(TranslateCodeRequest theRequest) { + myRequest = theRequest; + myHashCode = Objects.hash("TranslateConcept", myRequest); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof TranslateConceptKey)) return false; + TranslateConceptKey that = (TranslateConceptKey) theO; + return Objects.equals(myRequest, that.myRequest); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + static class ValidateCodeKey extends BaseKey { + private final String mySystem; + private final String myCode; + private final String myDisplay; + private final String myValueSetUrl; + private final int myHashCode; + private final ConceptValidationOptions myOptions; + + private ValidateCodeKey( + ConceptValidationOptions theOptions, + String theSystem, + String theCode, + String theDisplay, + String theValueSetUrl) { + myOptions = theOptions; + mySystem = theSystem; + myCode = theCode; + myDisplay = theDisplay; + myValueSetUrl = theValueSetUrl; + myHashCode = Objects.hash("ValidateCodeKey", myOptions, mySystem, myCode, myDisplay, myValueSetUrl); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (!(theO instanceof ValidateCodeKey)) return false; + ValidateCodeKey that = (ValidateCodeKey) theO; + return Objects.equals(myOptions, that.myOptions) + && Objects.equals(mySystem, that.mySystem) + && Objects.equals(myCode, that.myCode) + && Objects.equals(myDisplay, that.myDisplay) + && Objects.equals(myValueSetUrl, that.myValueSetUrl); + } + + @Override + public int hashCode() { + return myHashCode; + } + } + + /** + * This class is basically the same thing as Optional, but is a distinct thing + * because we want to use it as a method parameter value, and compare instances of + * it with null. Both of these things generate warnings in various linters. + */ + private static class CacheValue { + + private static final CacheValue EMPTY = new CacheValue<>(null); + + private final T myValue; + + private CacheValue(T theValue) { + myValue = theValue; + } + + public T getValue() { + return myValue; + } + + @SuppressWarnings("unchecked") + public static CacheValue empty() { + return (CacheValue) EMPTY; + } } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java new file mode 100644 index 00000000000..05fb8229541 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainMetrics.java @@ -0,0 +1,93 @@ +package org.hl7.fhir.common.hapi.validation.support; + +import ca.uhn.fhir.rest.api.Constants; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BatchCallback; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +/** + * This class provides OpenTelemetry metrics for the {@link ValidationSupportChain} cache. + */ +public class ValidationSupportChainMetrics { + + /* + * See ValidationSupportChainTest#testMetrics for a unit test + * which exercises the functionality in this class. + */ + + public static final String CLASS_OPENTELEMETRY_BASE_NAME = + Constants.OPENTELEMETRY_BASE_NAME + ".validation_support_chain"; + static final String INSTRUMENTATION_NAME = CLASS_OPENTELEMETRY_BASE_NAME; + private static final AttributeKey INSTANCE_NAME = stringKey(INSTRUMENTATION_NAME + ".instance_name"); + public static final String EXPIRING_CACHE_MAXIMUM_SIZE = + CLASS_OPENTELEMETRY_BASE_NAME + ".expiring_cache.maximum_size"; + public static final String EXPIRING_CACHE_CURRENT_ENTRIES = + CLASS_OPENTELEMETRY_BASE_NAME + ".expiring_cache.current_entries"; + public static final String NON_EXPIRING_CACHE_CURRENT_ENTRIES = + CLASS_OPENTELEMETRY_BASE_NAME + ".non_expiring_cache.current_entries"; + private static final Logger ourLog = LoggerFactory.getLogger(ValidationSupportChainMetrics.class); + private final ValidationSupportChain myValidationSupportChain; + private BatchCallback myBatchCallback; + + public ValidationSupportChainMetrics(ValidationSupportChain theValidationSupportChain) { + myValidationSupportChain = theValidationSupportChain; + } + + public void start() { + OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + MeterBuilder meterBuilder = openTelemetry.getMeterProvider().meterBuilder(INSTRUMENTATION_NAME); + Meter meter = meterBuilder.build(); + + Attributes baseAttribute = Attributes.of(INSTANCE_NAME, myValidationSupportChain.getName()); + + ObservableLongMeasurement expiringCacheMaxSize = meter.gaugeBuilder(EXPIRING_CACHE_MAXIMUM_SIZE) + .ofLongs() + .setUnit("{entries}") + .setDescription("The maximum number of cache entries in the expiring cache.") + .buildObserver(); + ObservableLongMeasurement expiringCacheCurrentEntries = meter.gaugeBuilder(EXPIRING_CACHE_CURRENT_ENTRIES) + .ofLongs() + .setUnit("{entries}") + .setDescription("The current number of cache entries in the expiring cache.") + .buildObserver(); + ObservableLongMeasurement nonExpiringCacheCurrentEntries = meter.gaugeBuilder( + NON_EXPIRING_CACHE_CURRENT_ENTRIES) + .ofLongs() + .setUnit("{entries}") + .setDescription("The current number of cache entries in the non-expiring cache.") + .buildObserver(); + + myBatchCallback = meter.batchCallback( + () -> { + long expiringCacheEntries = myValidationSupportChain.getMetricExpiringCacheEntries(); + int expiringCacheMaxSizeValue = myValidationSupportChain.getMetricExpiringCacheMaxSize(); + int nonExpiringCacheEntries = myValidationSupportChain.getMetricNonExpiringCacheEntries(); + ourLog.trace( + "ExpiringMax[{}] ExpiringEntries[{}] NonExpiringEntries[{}]", + expiringCacheMaxSizeValue, + expiringCacheEntries, + nonExpiringCacheEntries); + expiringCacheMaxSize.record(expiringCacheMaxSizeValue, baseAttribute); + expiringCacheCurrentEntries.record(expiringCacheEntries, baseAttribute); + nonExpiringCacheCurrentEntries.record(nonExpiringCacheEntries, baseAttribute); + }, + expiringCacheMaxSize, + expiringCacheCurrentEntries, + nonExpiringCacheCurrentEntries); + } + + public void stop() { + if (myBatchCallback != null) { + myBatchCallback.close(); + } + } +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index 393d8ce1dc5..ac4eb92581c 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -1,21 +1,23 @@ package org.hl7.fhir.common.hapi.validation.validator; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.sl.cache.CacheFactory; -import ca.uhn.fhir.sl.cache.LoadingCache; -import ca.uhn.fhir.system.HapiSystemProperties; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.fhir.ucum.UcumService; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportUtils; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.TerminologyServiceException; @@ -23,6 +25,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.context.IContextResourceLoader; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContextManager; +import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; @@ -56,11 +59,13 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import static ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssueCoding.INVALID_DISPLAY; import static java.util.stream.Collectors.collectingAndThen; @@ -70,68 +75,33 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWorkerContext { private static final Logger ourLog = LoggerFactory.getLogger(VersionSpecificWorkerContextWrapper.class); + + /** + * When we fetch conformance resources such as StructureDefinitions from {@link IValidationSupport} + * they will be returned using whatever version of FHIR the underlying infrastructure is + * configured to support. But we need to convert it to R5 since that's what the validator + * uses. In order to avoid repeated conversions, we put the converted version of the resource + * in the {@link org.hl7.fhir.instance.model.api.IAnyResource#getUserData(String)} map + * using this key. Since conformance resources returned by validation support are typically + * cached by {@link org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain}, + * the converted version gets cached too. + */ + private static final String CANONICAL_USERDATA_KEY = + VersionSpecificWorkerContextWrapper.class.getName() + "_CANONICAL_USERDATA_KEY"; + + public static final FhirContext FHIR_CONTEXT_R5 = FhirContext.forR5(); private final ValidationSupportContext myValidationSupportContext; private final VersionCanonicalizer myVersionCanonicalizer; - private final LoadingCache myFetchResourceCache; private volatile List myAllStructures; private volatile Set myAllPrimitiveTypes; private Parameters myExpansionProfile; + private volatile FHIRPathEngine myFHIRPathEngine; public VersionSpecificWorkerContextWrapper( ValidationSupportContext theValidationSupportContext, VersionCanonicalizer theVersionCanonicalizer) { myValidationSupportContext = theValidationSupportContext; myVersionCanonicalizer = theVersionCanonicalizer; - long timeoutMillis = HapiSystemProperties.getTestValidationResourceCachesMs(); - - myFetchResourceCache = CacheFactory.build(timeoutMillis, 10000, key -> { - String fetchResourceName = key.getResourceName(); - if (myValidationSupportContext - .getRootValidationSupport() - .getFhirContext() - .getVersion() - .getVersion() - == FhirVersionEnum.DSTU2) { - if ("CodeSystem".equals(fetchResourceName)) { - fetchResourceName = "ValueSet"; - } - } - - Class fetchResourceType; - if (fetchResourceName.equals("Resource")) { - fetchResourceType = null; - } else { - fetchResourceType = myValidationSupportContext - .getRootValidationSupport() - .getFhirContext() - .getResourceDefinition(fetchResourceName) - .getImplementingClass(); - } - - IBaseResource fetched = myValidationSupportContext - .getRootValidationSupport() - .fetchResource(fetchResourceType, key.getUri()); - - Resource canonical = myVersionCanonicalizer.resourceToValidatorCanonical(fetched); - - if (canonical instanceof StructureDefinition) { - StructureDefinition canonicalSd = (StructureDefinition) canonical; - if (canonicalSd.getSnapshot().isEmpty()) { - ourLog.info("Generating snapshot for StructureDefinition: {}", canonicalSd.getUrl()); - fetched = myValidationSupportContext - .getRootValidationSupport() - .generateSnapshot(theValidationSupportContext, fetched, "", null, ""); - Validate.isTrue( - fetched != null, - "StructureDefinition %s has no snapshot, and no snapshot generator is configured", - key.getUri()); - canonical = myVersionCanonicalizer.resourceToValidatorCanonical(fetched); - } - } - - return canonical; - }); - setValidationMessageLanguage(getLocale()); } @@ -237,21 +207,45 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo myExpansionProfile = expParameters; } - private List allStructures() { + private List allStructureDefinitions() { List retVal = myAllStructures; if (retVal == null) { - retVal = new ArrayList<>(); - for (IBaseResource next : - myValidationSupportContext.getRootValidationSupport().fetchAllStructureDefinitions()) { - try { - StructureDefinition converted = myVersionCanonicalizer.structureDefinitionToCanonical(next); - retVal.add(converted); - } catch (FHIRException e) { - throw new InternalErrorException(Msg.code(659) + e); - } - } + List allStructureDefinitions = + myValidationSupportContext.getRootValidationSupport().fetchAllStructureDefinitions(); + assert allStructureDefinitions != null; + + /* + * This method (allStructureDefinitions()) gets called recursively - As we + * try to return all StructureDefinitions, we want to generate snapshots for + * any that don't already have a snapshot. But the snapshot generator in turn + * also calls allStructureDefinitions() - That specific call doesn't require + * that the returned SDs have snapshots generated though. + * + * So, we first just convert to canonical version and store a list containing + * the canonical versions. That way any recursive calls will return the + * stored list. But after that we'll generate all the snapshots and + * store that list instead. If the canonicalization fails with an + * unexpected exception, we wipe the stored list. This is probably an + * unrecoverable failure since this method will probably always + * fail if it fails once. But at least this way we're likley to + * generate useful error messages for the user. + */ + retVal = allStructureDefinitions.stream() + .map(t -> myVersionCanonicalizer.structureDefinitionToCanonical(t)) + .collect(Collectors.toList()); myAllStructures = retVal; + + try { + for (IBaseResource next : allStructureDefinitions) { + Resource converted = convertToCanonicalVersionAndGenerateSnapshot(next, false); + retVal.add((StructureDefinition) converted); + } + myAllStructures = retVal; + } catch (Exception e) { + ourLog.error("Failure during snapshot generation", e); + myAllStructures = null; + } } return retVal; @@ -477,9 +471,9 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } } - ResourceKey key = new ResourceKey(class_.getSimpleName(), uri); + String resourceType = getResourceType(class_); @SuppressWarnings("unchecked") - T retVal = (T) myFetchResourceCache.get(key); + T retVal = (T) fetchResource(resourceType, uri); return retVal; } @@ -574,7 +568,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo @Override public List fetchTypeDefinitions(String theTypeName) { - List allStructures = new ArrayList<>(allStructures()); + List allStructures = new ArrayList<>(allStructureDefinitions()); allStructures.removeIf(sd -> !sd.hasType() || !sd.getType().equals(theTypeName)); return allStructures; } @@ -593,7 +587,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo Set retVal = myAllPrimitiveTypes; if (retVal == null) { // Collector may be changed to Collectors.toUnmodifiableSet() when switching to Android API level >= 33 - retVal = allStructures().stream() + retVal = allStructureDefinitions().stream() .filter(structureDefinition -> structureDefinition.getKind() == StructureDefinition.StructureDefinitionKind.PRIMITIVETYPE) .map(StructureDefinition::getName) @@ -636,8 +630,15 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return false; } - ResourceKey key = new ResourceKey(class_.getSimpleName(), uri); - return myFetchResourceCache.get(key) != null; + String resourceType = getResourceType(class_); + return fetchResource(resourceType, uri) != null; + } + + private static String getResourceType(Class theClass) { + if (theClass.getSimpleName().equals("Resource")) { + return "Resource"; + } + return FHIR_CONTEXT_R5.getResourceType(theClass); } @Override @@ -815,7 +816,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo .getRootValidationSupport() .validateCodeInValueSet( myValidationSupportContext, theValidationOptions, theSystem, theCode, theDisplay, theValueSet); - if (result != null && theSystem != null) { + if (result != null && isNotBlank(theSystem)) { /* We got a value set result, which could be successful, or could contain errors/warnings. The code might also be invalid in the code system, so we will check that as well and add those issues to our result. @@ -878,7 +879,7 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } if (code.getCoding().size() > 0) { - if (!myValidationSupportContext.isEnabledValidationForCodingsLogicalAnd()) { + if (!myValidationSupportContext.isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid()) { if (validationResultsOk.size() == code.getCoding().size()) { return validationResultsOk.get(0); } @@ -905,13 +906,13 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } public void invalidateCaches() { - myFetchResourceCache.invalidateAll(); + // nothing for now } @Override public List fetchResourcesByType(Class theClass) { if (theClass.equals(StructureDefinition.class)) { - return (List) allStructures(); + return (List) allStructureDefinitions(); } throw new UnsupportedOperationException(Msg.code(650) + "Unable to fetch resources of type: " + theClass); } @@ -1033,4 +1034,109 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo public boolean isServerSideSystem(String url) { return false; } + + private IBaseResource fetchResource(String theResourceType, String theUrl) { + String fetchResourceName = theResourceType; + if (myValidationSupportContext + .getRootValidationSupport() + .getFhirContext() + .getVersion() + .getVersion() + == FhirVersionEnum.DSTU2) { + if ("CodeSystem".equals(fetchResourceName)) { + fetchResourceName = "ValueSet"; + } + } + + Class fetchResourceType; + if (fetchResourceName.equals("Resource")) { + fetchResourceType = null; + } else { + fetchResourceType = myValidationSupportContext + .getRootValidationSupport() + .getFhirContext() + .getResourceDefinition(fetchResourceName) + .getImplementingClass(); + } + + IBaseResource fetched = + myValidationSupportContext.getRootValidationSupport().fetchResource(fetchResourceType, theUrl); + + if (fetched == null) { + return null; + } + + return convertToCanonicalVersionAndGenerateSnapshot(fetched, true); + } + + private Resource convertToCanonicalVersionAndGenerateSnapshot( + @Nonnull IBaseResource theResource, boolean thePropagateSnapshotException) { + Resource canonical; + synchronized (theResource) { + canonical = (Resource) theResource.getUserData(CANONICAL_USERDATA_KEY); + if (canonical == null) { + boolean storeCanonical = true; + canonical = myVersionCanonicalizer.resourceToValidatorCanonical(theResource); + + if (canonical instanceof StructureDefinition) { + StructureDefinition canonicalSd = (StructureDefinition) canonical; + if (canonicalSd.getSnapshot().isEmpty()) { + ourLog.info("Generating snapshot for StructureDefinition: {}", canonicalSd.getUrl()); + IBaseResource resource = theResource; + try { + + FhirContext fhirContext = myValidationSupportContext + .getRootValidationSupport() + .getFhirContext(); + SnapshotGeneratingValidationSupport snapshotGenerator = + new SnapshotGeneratingValidationSupport(fhirContext, this, getFHIRPathEngine()); + resource = snapshotGenerator.generateSnapshot( + myValidationSupportContext, resource, "", null, ""); + Validate.isTrue( + resource != null, + "StructureDefinition %s has no snapshot, and no snapshot generator is configured", + canonicalSd.getUrl()); + + } catch (BaseServerResponseException e) { + if (thePropagateSnapshotException) { + throw e; + } + String message = e.toString(); + Throwable rootCause = ExceptionUtils.getRootCause(e); + if (rootCause != null) { + message = rootCause.getMessage(); + } + ourLog.warn( + "Failed to generate snapshot for profile with URL[{}]: {}", + canonicalSd.getUrl(), + message); + storeCanonical = false; + } + + canonical = myVersionCanonicalizer.resourceToValidatorCanonical(resource); + } + } + + String sourcePackageId = + (String) theResource.getUserData(DefaultProfileValidationSupport.SOURCE_PACKAGE_ID); + if (sourcePackageId != null) { + canonical.setSourcePackage(new PackageInformation(sourcePackageId, null, null, new Date())); + } + + if (storeCanonical) { + theResource.setUserData(CANONICAL_USERDATA_KEY, canonical); + } + } + } + return canonical; + } + + private FHIRPathEngine getFHIRPathEngine() { + FHIRPathEngine retVal = myFHIRPathEngine; + if (retVal == null) { + retVal = new FHIRPathEngine(this); + myFHIRPathEngine = retVal; + } + return retVal; + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java index 6eb2894630e..d59fe708c0d 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java @@ -7,6 +7,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.StructureDefinition; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullSource; @@ -21,98 +22,43 @@ import static ca.uhn.fhir.util.TestUtil.sleepAtLeast; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +/** + * CachingValidationSupport is deprecated and is just a passthrough now. This + * test verifies that it works that way. + */ +@SuppressWarnings("removal") @ExtendWith(MockitoExtension.class) public class CachingValidationSupportTest { private static final FhirContext ourCtx = FhirContext.forR4Cached(); @Mock - private IValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport0; - @ParameterizedTest - @NullSource - @ValueSource(booleans = {true, false}) - public void testAsyncBackgroundLoading(Boolean theIsEnabledValidationForCodingsLogicalAnd) { - StructureDefinition sd0 = (StructureDefinition) new StructureDefinition().setId("SD0"); - StructureDefinition sd1 = (StructureDefinition) new StructureDefinition().setId("SD1"); - StructureDefinition sd2 = (StructureDefinition) new StructureDefinition().setId("SD2"); - List responses = Collections.synchronizedList(Lists.newArrayList( - sd0, sd1, sd2 - )); + @Test + public void testNoCaching() { + when(myValidationSupport0.getFhirContext()).thenReturn(ourCtx); + when(myValidationSupport0.fetchStructureDefinition(any())).thenAnswer(t->new StructureDefinition()); - when(myValidationSupport.getFhirContext()).thenReturn(ourCtx); - when(myValidationSupport.fetchAllNonBaseStructureDefinitions()).thenAnswer(t -> { - Thread.sleep(2000); - return Collections.singletonList(responses.remove(0)); - }); + CachingValidationSupport support = new CachingValidationSupport(myValidationSupport0); - final CachingValidationSupport.CacheTimeouts cacheTimeouts = CachingValidationSupport.CacheTimeouts - .defaultValues() - .setMiscMillis(1000); - final CachingValidationSupport support = getSupport(cacheTimeouts, theIsEnabledValidationForCodingsLogicalAnd); - - assertThat(responses).hasSize(3); - List fetched = support.fetchAllNonBaseStructureDefinitions(); - assert fetched != null; - assertThat(fetched.get(0)).isSameAs(sd0); - assertThat(responses).hasSize(2); - - sleepAtLeast(1200); - fetched = support.fetchAllNonBaseStructureDefinitions(); - assert fetched != null; - assertThat(fetched.get(0)).isSameAs(sd0); - assertThat(responses).hasSize(2); - - await().until(() -> responses.size() == 1); - assertThat(responses).hasSize(1); - fetched = support.fetchAllNonBaseStructureDefinitions(); - assert fetched != null; - assertThat(fetched.get(0)).isSameAs(sd1); - assertThat(responses).hasSize(1); - - assertEquals(theIsEnabledValidationForCodingsLogicalAnd != null && theIsEnabledValidationForCodingsLogicalAnd, support.isEnabledValidationForCodingsLogicalAnd()); + IBaseResource actual0 = support.fetchStructureDefinition("http://foo"); + IBaseResource actual1 = support.fetchStructureDefinition("http://foo"); + assertNotSame(actual0, actual1); } - @ParameterizedTest - @NullSource - @ValueSource(booleans = {true, false}) - public void fetchBinary_normally_accessesSuperOnlyOnce(Boolean theIsEnabledValidationForCodingsLogicalAnd) { - final byte[] EXPECTED_BINARY = "dummyBinaryContent".getBytes(); - final String EXPECTED_BINARY_KEY = "dummyBinaryKey"; - when(myValidationSupport.getFhirContext()).thenReturn(ourCtx); - when(myValidationSupport.fetchBinary(EXPECTED_BINARY_KEY)).thenReturn(EXPECTED_BINARY); - - final CachingValidationSupport support = getSupport(null, theIsEnabledValidationForCodingsLogicalAnd); - - final byte[] firstActualBinary = support.fetchBinary(EXPECTED_BINARY_KEY); - assertEquals(EXPECTED_BINARY, firstActualBinary); - verify(myValidationSupport, times(1)).fetchBinary(EXPECTED_BINARY_KEY); - - final byte[] secondActualBinary = support.fetchBinary(EXPECTED_BINARY_KEY); - assertEquals(EXPECTED_BINARY, secondActualBinary); - verify(myValidationSupport, times(1)).fetchBinary(EXPECTED_BINARY_KEY); - - assertEquals(theIsEnabledValidationForCodingsLogicalAnd != null && theIsEnabledValidationForCodingsLogicalAnd, support.isEnabledValidationForCodingsLogicalAnd()); + @Test + public void testEnabledValidationForCodingsLogicalAnd() { + when(myValidationSupport0.getFhirContext()).thenReturn(ourCtx); + CachingValidationSupport support = new CachingValidationSupport(myValidationSupport0, true); + assertTrue(support.isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid()); } - @Nonnull - private CachingValidationSupport getSupport(@Nullable CachingValidationSupport.CacheTimeouts theCacheTimeouts, @Nullable Boolean theIsEnabledValidationForCodingsLogicalAnd) { - if (theCacheTimeouts == null) { - if (theIsEnabledValidationForCodingsLogicalAnd == null) { - return new CachingValidationSupport(myValidationSupport); - } - - return new CachingValidationSupport(myValidationSupport, theIsEnabledValidationForCodingsLogicalAnd); - } - - if (theIsEnabledValidationForCodingsLogicalAnd == null) { - return new CachingValidationSupport(myValidationSupport, theCacheTimeouts); - } - - return new CachingValidationSupport(myValidationSupport, theCacheTimeouts, theIsEnabledValidationForCodingsLogicalAnd); - } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java index cbb8f87ac4d..c04c9af3376 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/ValidationSupportChainTest.java @@ -2,41 +2,108 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; +import ca.uhn.fhir.context.support.LookupCodeRequest; +import ca.uhn.fhir.context.support.TranslateConceptResult; +import ca.uhn.fhir.context.support.TranslateConceptResults; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.test.BaseTest; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.collect.Lists; +import io.opentelemetry.instrumentation.testing.LibraryTestRunner; +import io.opentelemetry.sdk.metrics.data.Data; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.ListResource; +import org.hl7.fhir.r4.model.SearchParameter; +import org.hl7.fhir.r4.model.StructureDefinition; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -public class ValidationSupportChainTest extends BaseValidationTestWithInlineMocks { +@ExtendWith(MockitoExtension.class) +public class ValidationSupportChainTest extends BaseTest { + public static final String CODE_SYSTEM_URL_0 = "http://code-system-url-0"; + public static final String CODE_0 = "code-0"; + public static final String DISPLAY_0 = "display-0"; + public static final String VALUE_SET_URL_0 = "http://value-set-url-0"; + private static final Logger ourLog = LoggerFactory.getLogger(ValidationSupportChainTest.class); + @Mock(strictness = Mock.Strictness.LENIENT) + private IValidationSupport myValidationSupport0; + @Mock(strictness = Mock.Strictness.LENIENT) + private IValidationSupport myValidationSupport1; + @Mock(strictness = Mock.Strictness.LENIENT) + private IValidationSupport myValidationSupport2; @Test public void testVersionCheck() { - - DefaultProfileValidationSupport ctx3 = new DefaultProfileValidationSupport(FhirContext.forDstu3()); - DefaultProfileValidationSupport ctx4 = new DefaultProfileValidationSupport(FhirContext.forR4()); + DefaultProfileValidationSupport ctx3 = new DefaultProfileValidationSupport(FhirContext.forDstu3Cached()); + DefaultProfileValidationSupport ctx4 = new DefaultProfileValidationSupport(FhirContext.forR4Cached()); try { new ValidationSupportChain(ctx3, ctx4); } catch (ConfigurationException e) { assertEquals(Msg.code(709) + "Trying to add validation support of version R4 to chain with 1 entries of version DSTU3", e.getMessage()); } + } + + + @Test + public void testFetchIndividualStructureDefinitionThenAll() { + DefaultProfileValidationSupport ctx = new DefaultProfileValidationSupport(FhirContext.forR4Cached()); + ValidationSupportChain chain = new ValidationSupportChain(ctx); + + assertNotNull(chain.fetchStructureDefinition("http://hl7.org/fhir/StructureDefinition/Patient")); + assertEquals(649, chain.fetchAllStructureDefinitions().size()); } + @Test public void testMissingContext() { - IValidationSupport ctx = mock(IValidationSupport.class); + when(myValidationSupport0.getFhirContext()).thenReturn(null); + try { - new ValidationSupportChain(ctx); + new ValidationSupportChain(myValidationSupport0); } catch (ConfigurationException e) { assertEquals(Msg.code(708) + "Can not add validation support: getFhirContext() returns null", e.getMessage()); } @@ -44,39 +111,657 @@ public class ValidationSupportChainTest extends BaseValidationTestWithInlineMock @Test public void fetchBinary_normally_returnsExpectedBinaries() { - + // Setup final byte[] EXPECTED_BINARY_CONTENT_1 = "dummyBinaryContent1".getBytes(); final byte[] EXPECTED_BINARY_CONTENT_2 = "dummyBinaryContent2".getBytes(); final String EXPECTED_BINARY_KEY_1 = "dummyBinaryKey1"; final String EXPECTED_BINARY_KEY_2 = "dummyBinaryKey2"; + prepareMock(myValidationSupport0); + prepareMock(myValidationSupport1); + createMockValidationSupportWithSingleBinary(myValidationSupport0, EXPECTED_BINARY_KEY_1, EXPECTED_BINARY_CONTENT_1); + createMockValidationSupportWithSingleBinary(myValidationSupport1, EXPECTED_BINARY_KEY_2, EXPECTED_BINARY_CONTENT_2); - IValidationSupport validationSupport1 = createMockValidationSupportWithSingleBinary(EXPECTED_BINARY_KEY_1, EXPECTED_BINARY_CONTENT_1); - IValidationSupport validationSupport2 = createMockValidationSupportWithSingleBinary(EXPECTED_BINARY_KEY_2, EXPECTED_BINARY_CONTENT_2); - - ValidationSupportChain validationSupportChain = new ValidationSupportChain(validationSupport1, validationSupport2); - - final byte[] actualBinaryContent1 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_1 ); - final byte[] actualBinaryContent2 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_2 ); + // Test + ValidationSupportChain validationSupportChain = new ValidationSupportChain(myValidationSupport0, myValidationSupport1); + final byte[] actualBinaryContent1 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_1); + final byte[] actualBinaryContent2 = validationSupportChain.fetchBinary(EXPECTED_BINARY_KEY_2); + // Verify assertThat(actualBinaryContent1).containsExactly(EXPECTED_BINARY_CONTENT_1); assertThat(actualBinaryContent2).containsExactly(EXPECTED_BINARY_CONTENT_2); assertNull(validationSupportChain.fetchBinary("nonExistentKey")); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testValidateCode_WithValueSetUrl(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); - private static IValidationSupport createMockValidationSupport() { - IValidationSupport validationSupport; - validationSupport = mock(IValidationSupport.class); - FhirContext mockContext = mock(FhirContext.class); - when(mockContext.getVersion()).thenReturn(FhirVersionEnum.R4.getVersionImplementation()); - when(validationSupport.getFhirContext()).thenReturn(mockContext); - return validationSupport; + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test + IValidationSupport.CodeValidationResult result = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + + // Setup for second execution (should use cache this time) + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test again (should use cache) + IValidationSupport.CodeValidationResult result2 = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verifyNoInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); + } else { + assertNotSame(result, result2); + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testValidateCode_WithoutValueSetUrl(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); + when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test + IValidationSupport.CodeValidationResult result = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, null); + + // Verify + verify(myValidationSupport0, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport1, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport2, never()).isCodeSystemSupported(any(), any()); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + + // Setup for second execution (should use cache this time) + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(false); + when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCode(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test again (should use cache) + IValidationSupport.CodeValidationResult result2 = chain.validateCode(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, null); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verifyNoInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); + } else { + assertNotSame(result, result2); + verify(myValidationSupport0, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport1, times(1)).isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0)); + verify(myValidationSupport2, never()).isCodeSystemSupported(any(), any()); + verify(myValidationSupport0, never()).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCode(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCode(any(), any(), any(), any(), any(), any()); + } + } + + @ParameterizedTest + @CsvSource({ + "true, true", + "true, false", + "false, true", + "false, false", + }) + public void testValidateCodeInValueSet(boolean theUseCache, boolean theValueSetHasUrl) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + ValueSet inputValueSet = new ValueSet(); + if (theValueSetHasUrl) { + inputValueSet.setUrl(VALUE_SET_URL_0); + } + + // Test + IValidationSupport.CodeValidationResult result = chain.validateCodeInValueSet(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, inputValueSet); + + // Verify + if (theValueSetHasUrl) { + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } else { + verify(myValidationSupport0, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport1, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } + + // Setup for second execution (should use cache this time) + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(false); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenAnswer(t -> new IValidationSupport.CodeValidationResult()); + + // Test again (should use cache) + IValidationSupport.CodeValidationResult result2 = chain.validateCodeInValueSet(newValidationCtx(chain), new ConceptValidationOptions(), CODE_SYSTEM_URL_0, CODE_0, DISPLAY_0, inputValueSet); + + // Verify + if (theUseCache && theValueSetHasUrl) { + assertSame(result, result2); + if (theValueSetHasUrl) { + verify(myValidationSupport0, times(1)).getFhirContext(); + } + verifyNoMoreInteractions(myValidationSupport0, myValidationSupport1, myValidationSupport2); + } else { + assertNotSame(result, result2); + if (theValueSetHasUrl) { + verify(myValidationSupport0, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, times(1)).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } else { + verify(myValidationSupport0, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport1, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport2, never()).isValueSetSupported(any(), eq(VALUE_SET_URL_0)); + verify(myValidationSupport0, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } + + verify(myValidationSupport1, times(1)).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + verify(myValidationSupport2, never()).validateCodeInValueSet(any(), any(), any(), any(), any(), any()); + } + } + + @ParameterizedTest + @CsvSource({ + "true, true", + "true, false", + "false, true", + "false, false", + }) + public void testExpandValueSet_ValueSetParam(boolean theUseCache, boolean theValueSetHasUrl) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport0.expandValueSet(any(), any(), any(IBaseResource.class))).thenReturn(null); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.expandValueSet(any(), any(), any(IBaseResource.class))).thenAnswer(t -> new IValidationSupport.ValueSetExpansionOutcome(new ValueSet())); + + ValueSet valueSetToExpand = new ValueSet(); + if (theValueSetHasUrl) { + valueSetToExpand.setId("123"); + valueSetToExpand.setUrl("http://foo"); + } + + // Test + IValidationSupport.ValueSetExpansionOutcome result = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), valueSetToExpand); + + // Verify + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(IBaseResource.class)); + + // Test again (should use cache) + IValidationSupport.ValueSetExpansionOutcome result2 = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), valueSetToExpand); + + // Verify + if (theUseCache && theValueSetHasUrl) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(IBaseResource.class)); + } else { + verify(myValidationSupport0, times(2)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport1, times(2)).expandValueSet(any(), any(), any(IBaseResource.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(IBaseResource.class)); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testExpandValueSet_StringParam(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport0.expandValueSet(any(), any(), any(String.class))).thenReturn(null); + when(myValidationSupport1.isValueSetSupported(any(), eq(VALUE_SET_URL_0))).thenReturn(true); + when(myValidationSupport1.expandValueSet(any(), any(), any(String.class))).thenAnswer(t -> new IValidationSupport.ValueSetExpansionOutcome(new ValueSet())); + + // Test + IValidationSupport.ValueSetExpansionOutcome result = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(String.class)); + + // Test again (should use cache) + IValidationSupport.ValueSetExpansionOutcome result2 = chain.expandValueSet(newValidationCtx(chain), new ValueSetExpansionOptions(), VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport1, times(1)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(String.class)); + } else { + verify(myValidationSupport0, times(2)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport1, times(2)).expandValueSet(any(), any(), any(String.class)); + verify(myValidationSupport2, times(0)).expandValueSet(any(), any(), any(String.class)); + } + } + + @Test + public void testFetchAllNonBaseStructureDefinitions() { + // Setup + prepareMock(myValidationSupport0); + + StructureDefinition sd0 = (StructureDefinition) new StructureDefinition().setId("SD0"); + StructureDefinition sd1 = (StructureDefinition) new StructureDefinition().setId("SD1"); + StructureDefinition sd2 = (StructureDefinition) new StructureDefinition().setId("SD2"); + List responses = Collections.synchronizedList(Lists.newArrayList( + sd0, sd1, sd2 + )); + + // Each time this is called it will return a slightly shorter list + when(myValidationSupport0.fetchAllNonBaseStructureDefinitions()).thenAnswer(t -> { + Thread.sleep(1000); + return new ArrayList<>(responses); + }); + + final ValidationSupportChain.CacheConfiguration cacheTimeouts = ValidationSupportChain.CacheConfiguration + .defaultValues() + .setCacheTimeout(Duration.ofMillis(500)); + ValidationSupportChain chain = new ValidationSupportChain(cacheTimeouts, myValidationSupport0); + + // First call should return the full list + assertEquals(3, chain.fetchAllNonBaseStructureDefinitions().size()); + assertEquals(3, chain.fetchAllNonBaseStructureDefinitions().size()); + + // Remove one from the backing list and wait for the cache to expire + responses.remove(0); + TestUtil.sleepAtLeast(750); + + // The cache is expired, but we should still return the old list and + // start a background job to update the backing list + assertEquals(3, chain.fetchAllNonBaseStructureDefinitions().size()); + + // Eventually we should refresh + await().until(() -> chain.fetchAllNonBaseStructureDefinitions().size(), t -> t == 2); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testFetchAllSearchParameters(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.fetchAllSearchParameters()).thenReturn(List.of( + new SearchParameter().setId("01"), + new SearchParameter().setId("02") + )); + when(myValidationSupport1.fetchAllSearchParameters()).thenReturn(List.of( + new SearchParameter().setId("11"), + new SearchParameter().setId("12") + )); + when(myValidationSupport2.fetchAllSearchParameters()).thenReturn(null); + ValidationSupportChain.CacheConfiguration cache = theUseCache ? ValidationSupportChain.CacheConfiguration.defaultValues() : ValidationSupportChain.CacheConfiguration.disabled(); + ValidationSupportChain chain = new ValidationSupportChain(cache, myValidationSupport0, myValidationSupport1, myValidationSupport2); + + // Test + List actual = chain.fetchAllSearchParameters(); + + // Verify + assert actual != null; + assertThat(actual.stream().map(t -> t.getIdElement().getIdPart()).toList()).asList().containsExactly( + "01", "02", "11", "12" + ); + verify(myValidationSupport0, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport1, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport2, times(1)).fetchAllSearchParameters(); + + // Test a second time + actual = chain.fetchAllSearchParameters(); + + // Verify + assert actual != null; + assertThat(actual.stream().map(t -> t.getIdElement().getIdPart()).toList()).asList().containsExactly( + "01", "02", "11", "12" + ); + if (theUseCache) { + verify(myValidationSupport0, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport1, times(1)).fetchAllSearchParameters(); + verify(myValidationSupport2, times(1)).fetchAllSearchParameters(); + } else { + verify(myValidationSupport0, times(2)).fetchAllSearchParameters(); + verify(myValidationSupport1, times(2)).fetchAllSearchParameters(); + verify(myValidationSupport2, times(2)).fetchAllSearchParameters(); + } + + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testLookupCode(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport1.isCodeSystemSupported(any(), eq(CODE_SYSTEM_URL_0))).thenReturn(true); + when(myValidationSupport0.lookupCode(any(), any(LookupCodeRequest.class))).thenReturn(null); + when(myValidationSupport1.lookupCode(any(), any(LookupCodeRequest.class))).thenAnswer(t -> new IValidationSupport.LookupCodeResult()); + + // Test + IValidationSupport.LookupCodeResult result = chain.lookupCode(newValidationCtx(chain), new LookupCodeRequest(CODE_SYSTEM_URL_0, CODE_0)); + + // Verify + verify(myValidationSupport0, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport1, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport2, times(0)).lookupCode(any(), any(LookupCodeRequest.class)); + + // Test again (should use cache) + IValidationSupport.LookupCodeResult result2 = chain.lookupCode(newValidationCtx(chain), new LookupCodeRequest(CODE_SYSTEM_URL_0, CODE_0)); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport1, times(1)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport2, times(0)).lookupCode(any(), any(LookupCodeRequest.class)); + } else { + verify(myValidationSupport0, times(2)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport1, times(2)).lookupCode(any(), any(LookupCodeRequest.class)); + verify(myValidationSupport2, times(0)).lookupCode(any(), any(LookupCodeRequest.class)); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testFetchValueSet(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.fetchValueSet(any())).thenReturn(null); + when(myValidationSupport1.fetchValueSet(any())).thenAnswer(t -> new ValueSet()); + + // Test + IBaseResource result = chain.fetchValueSet(VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).fetchValueSet(any()); + verify(myValidationSupport1, times(1)).fetchValueSet(any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + + // Test again (should use cache) + IBaseResource result2 = chain.fetchValueSet(VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).fetchValueSet(any()); + verify(myValidationSupport1, times(1)).fetchValueSet(any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + } else { + verify(myValidationSupport0, times(2)).fetchValueSet(any()); + verify(myValidationSupport1, times(2)).fetchValueSet(any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testResource_ValueSet(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.fetchValueSet(any())).thenReturn(null); + when(myValidationSupport1.fetchValueSet(any())).thenAnswer(t -> new ValueSet()); + + // Test + IBaseResource result = chain.fetchResource(ValueSet.class, VALUE_SET_URL_0); + + // Verify + verify(myValidationSupport0, times(1)).fetchValueSet( any()); + verify(myValidationSupport1, times(1)).fetchValueSet( any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + + // Test again (should use cache) + IBaseResource result2 = chain.fetchResource(ValueSet.class, VALUE_SET_URL_0); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).fetchValueSet( any()); + verify(myValidationSupport1, times(1)).fetchValueSet( any()); + verify(myValidationSupport2, times(0)).fetchValueSet( any()); + } else { + verify(myValidationSupport0, times(2)).fetchValueSet( any()); + verify(myValidationSupport1, times(2)).fetchValueSet( any()); + verify(myValidationSupport2, times(0)).fetchValueSet(any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testResource_Arbitrary(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.fetchResource(any(), any())).thenReturn(null); + when(myValidationSupport1.fetchResource(any(), any())).thenAnswer(t -> new ListResource()); + + // Test + IBaseResource result = chain.fetchResource(ListResource.class, "http://foo"); + + // Verify + verify(myValidationSupport0, times(1)).fetchResource(any(), any()); + verify(myValidationSupport1, times(1)).fetchResource(any(), any()); + verify(myValidationSupport2, times(0)).fetchResource(any(), any()); + + // Test again (should use cache) + IBaseResource result2 = chain.fetchResource(ListResource.class, "http://foo"); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).fetchResource(any(), any()); + verify(myValidationSupport1, times(1)).fetchResource(any(), any()); + verify(myValidationSupport2, times(0)).fetchResource(any(), any()); + } else { + verify(myValidationSupport0, times(2)).fetchResource(any(), any()); + verify(myValidationSupport1, times(2)).fetchResource(any(), any()); + verify(myValidationSupport2, times(0)).fetchResource(any(), any()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testTranslateCode(boolean theUseCache) { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + + when(myValidationSupport0.translateConcept(any())).thenReturn(null); + TranslateConceptResults backingResult1 = new TranslateConceptResults(); + backingResult1.setMessage("Message 1"); + backingResult1.setResult(true); + backingResult1.getResults().add(new TranslateConceptResult().setCode("A")); + when(myValidationSupport1.translateConcept(any())).thenReturn(backingResult1); + TranslateConceptResults backingResult2 = new TranslateConceptResults(); + backingResult2.setMessage("Message 2"); + backingResult2.setResult(true); + backingResult2.getResults().add(new TranslateConceptResult().setCode("B")); + when(myValidationSupport2.translateConcept(any())).thenReturn(backingResult2); + + // Test + TranslateConceptResults result = chain.translateConcept(new IValidationSupport.TranslateCodeRequest(List.of(), CODE_SYSTEM_URL_0)); + + // Verify + assertEquals("Message 1", result.getMessage()); + assertTrue(result.getResult()); + assertEquals(2, result.getResults().size()); + verify(myValidationSupport0, times(1)).translateConcept(any()); + verify(myValidationSupport1, times(1)).translateConcept(any()); + verify(myValidationSupport2, times(1)).translateConcept(any()); + + // Test again (should use cache) + TranslateConceptResults result2 = chain.translateConcept(new IValidationSupport.TranslateCodeRequest(List.of(), CODE_SYSTEM_URL_0)); + + // Verify + if (theUseCache) { + assertSame(result, result2); + verify(myValidationSupport0, times(1)).translateConcept(any()); + verify(myValidationSupport1, times(1)).translateConcept(any()); + verify(myValidationSupport2, times(1)).translateConcept(any()); + } else { + assertNotSame(result, result2); + verify(myValidationSupport0, times(2)).translateConcept(any()); + verify(myValidationSupport1, times(2)).translateConcept(any()); + verify(myValidationSupport2, times(2)).translateConcept(any()); + } + } + + /** + * Verify that OpenTelemetry metrics are generated correctly + */ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testMetrics(boolean theUseCache) { + LibraryTestRunner libraryTestRunner = LibraryTestRunner.instance(); + + /* + * As of version 2.10.0 of the opentelemetry-testing-common library, + * the following doesn't actually clear the stored metrics. Hopefully + * this will be fixed in a future release. + */ + libraryTestRunner.clearAllExportedData(); + + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.fetchStructureDefinition("http://foo")).thenReturn(new StructureDefinition().setUrl("http://foo")); + ValidationSupportChain chain = new ValidationSupportChain(newCacheConfiguration(theUseCache), myValidationSupport0, myValidationSupport1, myValidationSupport2); + chain.setName("FOO_NAME"); + + chain.start(); + try { + if (theUseCache) { + assertEquals(5000L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_MAXIMUM_SIZE)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_CURRENT_ENTRIES)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.NON_EXPIRING_CACHE_CURRENT_ENTRIES)); + } else { + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_MAXIMUM_SIZE)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.EXPIRING_CACHE_CURRENT_ENTRIES)); + assertEquals(0L, getLastMetricValue(libraryTestRunner, ValidationSupportChainMetrics.NON_EXPIRING_CACHE_CURRENT_ENTRIES)); + } + + // Test + assertNotNull(chain.fetchStructureDefinition("http://foo")); + + // Verify + if (theUseCache) { + assertEquals(5000L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.maximum_size")); + assertEquals(1L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.current_entries")); + assertEquals(1L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.non_expiring_cache.current_entries")); + } else { + assertEquals(0L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.maximum_size")); + assertEquals(0L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.expiring_cache.current_entries")); + assertEquals(0L, getLastMetricValue(libraryTestRunner, "io.hapifhir.validation_support_chain.non_expiring_cache.current_entries")); + } + } finally { + chain.stop(); + } } - private static IValidationSupport createMockValidationSupportWithSingleBinary(String expected_binary_key, byte[] expected_binary_content) { - IValidationSupport validationSupport1 = createMockValidationSupport(); - when(validationSupport1.fetchBinary(expected_binary_key)).thenReturn(expected_binary_content); - return validationSupport1; + @Test + public void testModifyingServiceInvalidatesCache() { + // Setup + prepareMock(myValidationSupport0, myValidationSupport1, myValidationSupport2); + when(myValidationSupport0.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(true); + when(myValidationSupport1.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(true); + + ValidationSupportChain svc = new ValidationSupportChain(myValidationSupport0, myValidationSupport1); + assertTrue(svc.isCodeSystemSupported(newValidationCtx(svc), "http://foo")); + + // Test + svc.addValidationSupport(myValidationSupport2); + when(myValidationSupport0.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(false); + when(myValidationSupport1.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(false); + when(myValidationSupport2.isCodeSystemSupported(any(), eq("http://foo"))).thenReturn(false); + boolean actual = svc.isCodeSystemSupported(newValidationCtx(svc), "http://foo"); + + // Verify + assertFalse(actual); } + + + private static long getLastMetricValue(LibraryTestRunner libraryTestRunner, String metricName) { + List metrics = libraryTestRunner.getExportedMetrics(); + List metricsList = metrics.stream().filter(t -> t.getName().equals(metricName)).toList(); + ourLog.info("Have metrics {}\n * {}", metricName, metricsList.stream().map(t -> t.getData().getPoints().toString()).collect(Collectors.joining("\n * "))); + MetricData metric = metricsList.get(metricsList.size() - 1); + assertEquals("io.hapifhir.validation_support_chain", metric.getInstrumentationScopeInfo().getName()); + Data data = metric.getData(); + ArrayList dataPoints = new ArrayList<>(data.getPoints()); + assertEquals(1, dataPoints.size()); + LongPointData pointData = (LongPointData) dataPoints.get(0); + return pointData.getValue(); + } + + + private static ValidationSupportChain.CacheConfiguration newCacheConfiguration(boolean theUseCache) { + return theUseCache ? ValidationSupportChain.CacheConfiguration.defaultValues() : ValidationSupportChain.CacheConfiguration.disabled(); + } + + @Nonnull + private static ValidationSupportContext newValidationCtx(ValidationSupportChain validationSupportChain) { + return new ValidationSupportContext(validationSupportChain); + } + + + private static void prepareMock(IValidationSupport... theMock) { + reset(theMock); + for (var mock : theMock) { + when(mock.getFhirContext()).thenReturn(FhirContext.forR4Cached()); + } + } + + private static void createMockValidationSupportWithSingleBinary(IValidationSupport theValidationSupport, String theExpectedBinaryKey, byte[] theExpectedBinaryContent) { + when(theValidationSupport.fetchBinary(theExpectedBinaryKey)).thenReturn(theExpectedBinaryContent); + } + } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java index 10548169c0f..9a03a81d262 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperCoreTest.java @@ -2,16 +2,13 @@ package org.hl7.fhir.common.hapi.validation.validator; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; - import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.test.utilities.LoggingExtension; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import com.google.common.base.Charsets; - import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; @@ -24,6 +21,8 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.Constants; import org.hl7.fhir.r5.model.Parameters; @@ -41,6 +40,9 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.io.File; import java.io.FileNotFoundException; @@ -50,14 +52,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; -import org.hl7.fhir.r5.formats.JsonParser; -import org.hl7.fhir.r5.formats.XmlParser; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.quality.Strictness; -import org.mockito.stubbing.Answer; - import static org.junit.jupiter.api.Assertions.assertNull; - import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; @@ -114,7 +109,7 @@ public class VersionSpecificWorkerContextWrapperCoreTest { private FhirInstanceValidator myInstanceVal; private Map mySupportedCodeSystemsForExpansion; private FhirValidator myVal; - private CachingValidationSupport myValidationSupport; + private ValidationSupportChain myValidationSupport; private static final String VALIDATE_CODE_OPERATION = "validate-code"; @@ -176,14 +171,13 @@ public class VersionSpecificWorkerContextWrapperCoreTest { UnknownCodeSystemWarningValidationSupport unknownCodeSystemWarningValidationSupport = new UnknownCodeSystemWarningValidationSupport(ourCtx); unknownCodeSystemWarningValidationSupport.setNonExistentCodeSystemSeverity(IValidationSupport.IssueSeverity.WARNING); - myValidationSupport = new CachingValidationSupport( + myValidationSupport = new ValidationSupportChain( mockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), - unknownCodeSystemWarningValidationSupport) - ); + unknownCodeSystemWarningValidationSupport); myInstanceVal = new FhirInstanceValidator(myValidationSupport); wrapper = new VersionSpecificWorkerContextWrapper(new ValidationSupportContext(myValidationSupport), versionCanonicalizer); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java index cbc79fadc98..a6162c2b67c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapperTest.java @@ -1,39 +1,50 @@ package org.hl7.fhir.common.hapi.validation.validator; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; -import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; -import ca.uhn.fhir.i18n.HapiLocalizer; +import ca.uhn.fhir.test.BaseTest; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; +import org.hl7.fhir.r5.model.PackageInformation; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.junit.jupiter.api.Test; -import org.mockito.quality.Strictness; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; -public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestWithInlineMocks { +@ExtendWith(MockitoExtension.class) +public class VersionSpecificWorkerContextWrapperTest extends BaseTest { final byte[] EXPECTED_BINARY_CONTENT_1 = "dummyBinaryContent1".getBytes(); final byte[] EXPECTED_BINARY_CONTENT_2 = "dummyBinaryContent2".getBytes(); final String EXPECTED_BINARY_KEY_1 = "dummyBinaryKey1"; final String EXPECTED_BINARY_KEY_2 = "dummyBinaryKey2"; final String NON_EXISTENT_BINARY_KEY = "nonExistentBinaryKey"; + @Mock + private ValidationSupportContext myValidationSupportContext; + @Mock + private IValidationSupport myValidationSupport; @Test public void hasBinaryKey_normally_returnsExpected() { @@ -90,7 +101,7 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW ValueSet valueSet = new ValueSet(); valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(mock(IValidationSupport.CodeValidationResult.class)); + when(validationSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any())).thenReturn(new IValidationSupport.CodeValidationResult()); // execute wrapper.validateCode(new ValidationOptions(), "code0", valueSet); @@ -157,6 +168,49 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW assertThat(wrapper.isPrimitiveType("Unknown")).isFalse(); } + @Test + public void testFetchResource_ResourceParameter() { + // setup + IValidationSupport validationSupport = mockValidationSupport(); + ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport); + VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(validationSupport.getFhirContext()); + VersionSpecificWorkerContextWrapper wrapper = new VersionSpecificWorkerContextWrapper(mockContext, versionCanonicalizer); + + org.hl7.fhir.r4.model.StructureDefinition expected = new org.hl7.fhir.r4.model.StructureDefinition(); + expected.setUrl("http://foo"); + expected.getSnapshot().addElement().setId("FOO"); + when(mockContext.getRootValidationSupport().fetchResource(isNull(), eq("http://foo"))).thenReturn(expected); + + // Test + StructureDefinition actual = (StructureDefinition) wrapper.fetchResource(Resource.class, "http://foo"); + + // Verify + assertEquals("FOO", actual.getSnapshot().getElementFirstRep().getId()); + } + + @Test + public void testFetchResource_StructureDefinitionParameter() { + // setup + IValidationSupport validationSupport = mockValidationSupport(); + ValidationSupportContext mockContext = mockValidationSupportContext(validationSupport); + VersionCanonicalizer versionCanonicalizer = new VersionCanonicalizer(validationSupport.getFhirContext()); + VersionSpecificWorkerContextWrapper wrapper = new VersionSpecificWorkerContextWrapper(mockContext, versionCanonicalizer); + + org.hl7.fhir.r4.model.StructureDefinition expected = new org.hl7.fhir.r4.model.StructureDefinition(); + expected.setUrl("http://foo"); + expected.getSnapshot().addElement().setId("FOO"); + expected.setUserData(DefaultProfileValidationSupport.SOURCE_PACKAGE_ID, "hl7.fhir.r999.core"); + when(mockContext.getRootValidationSupport().fetchResource(eq(org.hl7.fhir.r4.model.StructureDefinition.class), eq("http://foo"))).thenReturn(expected); + + // Test + StructureDefinition actual = wrapper.fetchResource(StructureDefinition.class, "http://foo"); + + // Verify + assertEquals("FOO", actual.getSnapshot().getElementFirstRep().getId()); + PackageInformation sourcePackage = actual.getSourcePackage(); + assertEquals("hl7.fhir.r999.core", sourcePackage.getId()); + } + private List createStructureDefinitions() { StructureDefinition stringType = createPrimitive("string"); StructureDefinition boolType = createPrimitive("boolean"); @@ -166,20 +220,25 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW return List.of(personType, boolType, orgType, stringType); } - private StructureDefinition createComplex(String name){ + private StructureDefinition createComplex(String name) { return createStructureDefinition(name).setKind(StructureDefinitionKind.COMPLEXTYPE); } - private StructureDefinition createPrimitive(String name){ + private StructureDefinition createPrimitive(String name) { return createStructureDefinition(name).setKind(StructureDefinitionKind.PRIMITIVETYPE); } private StructureDefinition createStructureDefinition(String name) { StructureDefinition sd = new StructureDefinition(); - sd.setUrl("http://hl7.org/fhir/StructureDefinition/"+name).setName(name); + sd.setUrl("http://hl7.org/fhir/StructureDefinition/" + name).setName(name); + addFakeSnapshot(sd); return sd; } + private static void addFakeSnapshot(StructureDefinition sd) { + sd.getSnapshot().addElement().setId("FOO"); + } + private IValidationSupport mockValidationSupportWithTwoBinaries() { IValidationSupport validationSupport; validationSupport = mockValidationSupport(); @@ -188,22 +247,14 @@ public class VersionSpecificWorkerContextWrapperTest extends BaseValidationTestW return validationSupport; } - - private static ValidationSupportContext mockValidationSupportContext(IValidationSupport validationSupport) { - ValidationSupportContext mockContext; - mockContext = mock(ValidationSupportContext.class); + private ValidationSupportContext mockValidationSupportContext(IValidationSupport validationSupport) { + ValidationSupportContext mockContext = myValidationSupportContext; when(mockContext.getRootValidationSupport()).thenReturn(validationSupport); return mockContext; } - - private static IValidationSupport mockValidationSupport() { - IValidationSupport mockValidationSupport; - mockValidationSupport = mock(IValidationSupport.class); - FhirContext mockFhirContext = mock(FhirContext.class, withSettings().strictness(Strictness.LENIENT)); - when(mockFhirContext.getLocalizer()).thenReturn(new HapiLocalizer()); - when(mockFhirContext.getVersion()).thenReturn(FhirVersionEnum.R4.getVersionImplementation()); - when(mockValidationSupport.getFhirContext()).thenReturn(mockFhirContext); - return mockValidationSupport; + private IValidationSupport mockValidationSupport() { + when(myValidationSupport.getFhirContext()).thenReturn(FhirContext.forR4Cached()); + return myValidationSupport; } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index cfbeca68626..34fc831525f 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -15,7 +15,6 @@ import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; @@ -115,7 +114,7 @@ public class FhirInstanceValidatorDstu3Test extends BaseValidationTestWithInline private HashMap myCodeSystems; private HashMap myValueSets; private HashMap myQuestionnaires; - private CachingValidationSupport myValidationSupport; + private ValidationSupportChain myValidationSupport; private void addValidConcept(String theSystem, String theCode) { addValidConcept(theSystem, theCode, true); @@ -139,12 +138,12 @@ public class FhirInstanceValidatorDstu3Test extends BaseValidationTestWithInline IValidationSupport mockSupport = mock(IValidationSupport.class, withSettings().strictness(Strictness.LENIENT)); when(mockSupport.getFhirContext()).thenReturn(ourCtx); - myValidationSupport = new CachingValidationSupport(new ValidationSupportChain( + myValidationSupport = new ValidationSupportChain( mockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), - new SnapshotGeneratingValidationSupport(ourCtx))); + new SnapshotGeneratingValidationSupport(ourCtx)); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myVal.registerValidatorModule(myInstanceVal); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 57591b31e76..4c7d0749585 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -169,7 +169,7 @@ public class QuestionnaireResponseValidatorDstu3Test { when(myValSupport.fetchResource(eq(Questionnaire.class), eq(QUESTIONNAIRE_URL))).thenReturn(q); when(myValSupport.fetchCodeSystem(eq("http://codesystems.com/system"))).thenReturn(codeSystem); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCodeInValueSet(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(ValueSet.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); @@ -242,7 +242,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); @@ -802,7 +802,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -858,7 +858,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -981,7 +981,7 @@ public class QuestionnaireResponseValidatorDstu3Test { .setValue(new Coding(SYSTEMURI_ICC_SCHOOLTYPE, CODE_ICC_SCHOOLTYPE_PT, "")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(questionnaireResponse.getQuestionnaire().getReference()))).thenReturn(questionnaire); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE.getValue()))).thenReturn(iccSchoolTypeVs); + when(myValSupport.fetchValueSet(eq(ID_VS_SCHOOLTYPE.getValue()))).thenReturn(iccSchoolTypeVs); when(myValSupport.validateCodeInValueSet(any(), any(), eq(SYSTEMURI_ICC_SCHOOLTYPE), eq(CODE_ICC_SCHOOLTYPE_PT), any(), nullable(ValueSet.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem); @@ -1053,7 +1053,7 @@ public class QuestionnaireResponseValidatorDstu3Test { options.setUrl("http://somevalueset"); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.isValueSetSupported(any(), eq("http://somevalueset"))).thenReturn(true); @@ -1095,7 +1095,7 @@ public class QuestionnaireResponseValidatorDstu3Test { assertThat(errors.getMessages()).hasSize(2); assertThat(errors.getMessages().get(0).getMessage()).contains("A code with no system has no defined meaning, and it cannot be validated. A system should be provided"); assertThat(errors.getMessages().get(0).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); - assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Validation failed"); + assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Unknown code 'code1' for in-memory expansion of ValueSet 'http://somevalueset'"); assertThat(errors.getMessages().get(1).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); qa = new QuestionnaireResponse(); @@ -1108,7 +1108,7 @@ public class QuestionnaireResponseValidatorDstu3Test { assertThat(errors.getMessages()).hasSize(2); assertThat(errors.getMessages().get(0).getMessage()).contains("A code with no system has no defined meaning, and it cannot be validated. A system should be provided"); assertThat(errors.getMessages().get(0).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); - assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Validation failed"); + assertThat(errors.getMessages().get(1).getMessage()).contains("The code 'code1' in the system 'null' is not in the options value set (ValueSet[http://somevalueset]) specified by the questionnaire. Terminology Error: Unknown code 'code1' for in-memory expansion of ValueSet 'http://somevalueset'"); assertThat(errors.getMessages().get(1).getLocationString()).contains("QuestionnaireResponse.item[0].answer[0]"); qa = new QuestionnaireResponse(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index fb46f7f8008..ff5d8e2e1fe 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -21,7 +21,6 @@ import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; @@ -133,7 +132,7 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc private Set myValidSystemsNotReturningIssues = new HashSet<>(); private Set myValidValueSets = new HashSet<>(); private Map myStructureDefinitionMap = new HashMap<>(); - private CachingValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport; private IValidationSupport myMockSupport; private void addValidConcept(String theSystem, String theCode) { @@ -909,7 +908,7 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc public void testValidateProfileWithExtension() throws IOException, FHIRException { PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(ourCtx); DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(ourCtx); - CachingValidationSupport support = new CachingValidationSupport(new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)), false); + ValidationSupportChain support = new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)).setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(false); // Prepopulate SDs valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/r4/myconsent-profile.xml")); @@ -1929,7 +1928,7 @@ public class FhirInstanceValidatorR4Test extends BaseValidationTestWithInlineMoc new CommonCodeSystemsTerminologyService(ourCtx), new InMemoryTerminologyServerValidationSupport(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); - myValidationSupport = new CachingValidationSupport(chain, theLogicalAnd); + myValidationSupport = chain.setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(theLogicalAnd); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myFhirValidator.registerValidatorModule(myInstanceVal); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java index 4a5b10e6cb3..17c347740b4 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java @@ -110,7 +110,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); int itemCnt = 16; QuestionnaireItemType[] questionnaireItemTypes = new QuestionnaireItemType[itemCnt]; @@ -241,7 +241,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); QuestionnaireResponse qa; ValidationResult errors; @@ -376,7 +376,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -433,7 +433,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -553,7 +553,7 @@ public class QuestionnaireResponseValidatorR4Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code1"), any(), nullable(String.class))) @@ -833,7 +833,7 @@ public class QuestionnaireResponseValidatorR4Test { .setValue(new Coding(SYSTEMURI_ICC_SCHOOLTYPE, CODE_ICC_SCHOOLTYPE_PT, "")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(qa.getQuestionnaire()))).thenReturn(questionnaire); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); + when(myValSupport.fetchValueSet(eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); when(myValSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any(ValueSet.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem); ValidationResult errors = myVal.validateWithResult(qa); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java index d5035a8048e..8b1d4df1051 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4b/validation/FhirInstanceValidatorR4BTest.java @@ -20,7 +20,6 @@ import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; @@ -118,7 +117,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo private ArrayList myValidConcepts; private Set myValidSystems = new HashSet<>(); private Map myStructureDefinitionMap = new HashMap<>(); - private CachingValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport; private IValidationSupport myMockSupport; private void addValidConcept(String theSystem, String theCode) { @@ -812,7 +811,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo public void testValidateProfileWithExtension() throws IOException, FHIRException { PrePopulatedValidationSupport valSupport = new PrePopulatedValidationSupport(ourCtx); DefaultProfileValidationSupport defaultSupport = new DefaultProfileValidationSupport(ourCtx); - CachingValidationSupport support = new CachingValidationSupport(new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)), false); + ValidationSupportChain support = new ValidationSupportChain(defaultSupport, valSupport, new InMemoryTerminologyServerValidationSupport(ourCtx)).setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(false); // Prepopulate SDs valSupport.addStructureDefinition(loadStructureDefinition(defaultSupport, "/r4/myconsent-profile.xml")); @@ -1694,7 +1693,7 @@ public class FhirInstanceValidatorR4BTest extends BaseValidationTestWithInlineMo new CommonCodeSystemsTerminologyService(ourCtx), new InMemoryTerminologyServerValidationSupport(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); - myValidationSupport = new CachingValidationSupport(chain, theLogicalAnd); + myValidationSupport = chain.setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid(theLogicalAnd); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myFhirValidator.registerValidatorModule(myInstanceVal); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java index abcb0f94704..7a58d4f7d25 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java @@ -16,9 +16,9 @@ import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; -import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -100,7 +100,7 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc private Set mySupportedValueSets = new HashSet<>(); private Set myValidSystems = new HashSet<>(); private Set myValidSystemsNotReturningIssues = new HashSet<>(); - private CachingValidationSupport myValidationSupport; + private IValidationSupport myValidationSupport; private void addValidConcept(String theSystem, String theCode) { addValidConcept(theSystem, theCode, true); @@ -141,7 +141,7 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc myMockSupport = mock(IValidationSupport.class); when(myMockSupport.getFhirContext()).thenReturn(ourCtx); - myValidationSupport = new CachingValidationSupport(new ValidationSupportChain(myMockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx))); + myValidationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport, new InMemoryTerminologyServerValidationSupport(ourCtx), new CommonCodeSystemsTerminologyService(ourCtx), new SnapshotGeneratingValidationSupport(ourCtx)); myInstanceVal = new FhirInstanceValidator(myValidationSupport); myVal.registerValidatorModule(myInstanceVal); @@ -222,7 +222,7 @@ public class FhirInstanceValidatorR5Test extends BaseValidationTestWithInlineMoc when(myMockSupport.fetchStructureDefinition(nullable(String.class))).thenAnswer(new Answer() { @Override public StructureDefinition answer(InvocationOnMock theInvocation) { - StructureDefinition retVal = (StructureDefinition) myDefaultValidationSupport.fetchStructureDefinition((String) theInvocation.getArguments()[1]); + StructureDefinition retVal = (StructureDefinition) myDefaultValidationSupport.fetchStructureDefinition((String) theInvocation.getArguments()[0]); ourLog.debug("fetchStructureDefinition({}) : {}", new Object[]{theInvocation.getArguments()[0], retVal}); return retVal; } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java index ebd1679278b..fbd25fbad33 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java @@ -112,7 +112,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), any(), any(), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setSeverity(IValidationSupport.IssueSeverity.ERROR).setMessage("Unknown code")); @@ -241,7 +241,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); QuestionnaireResponse qa; ValidationResult errors; @@ -374,7 +374,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -435,7 +435,8 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem(codeSystemUrl).addConcept().setCode(codeValue); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(valueSetRef))) + when(myValSupport.isValueSetSupported(any(), eq(valueSetRef))).thenReturn(true); + when(myValSupport.fetchValueSet(eq(valueSetRef))) .thenReturn(options); when(myValSupport.validateCode(any(), any(), eq(codeSystemUrl), eq(codeValue), any(String.class), anyString())) .thenReturn(new IValidationSupport.CodeValidationResult().setCode(codeValue)); @@ -564,7 +565,7 @@ public class QuestionnaireResponseValidatorR5Test { ValueSet options = new ValueSet(); options.getCompose().addInclude().setSystem("http://codesystems.com/system").addConcept().setCode("code0"); options.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2"); - when(myValSupport.fetchResource(eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); + when(myValSupport.fetchValueSet(eq("http://somevalueset"))).thenReturn(options); when(myValSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq("code0"), any(), nullable(String.class))) .thenReturn(new IValidationSupport.CodeValidationResult().setCode("code0")); @@ -721,7 +722,7 @@ public class QuestionnaireResponseValidatorR5Test { .setValue(new Coding(SYSTEMURI_ICC_SCHOOLTYPE, CODE_ICC_SCHOOLTYPE_PT, "")); when(myValSupport.fetchResource(eq(Questionnaire.class), eq(qa.getQuestionnaire()))).thenReturn(questionnaire); - when(myValSupport.fetchResource(eq(ValueSet.class), eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); + when(myValSupport.fetchValueSet(eq(ID_VS_SCHOOLTYPE))).thenReturn(iccSchoolTypeVs); when(myValSupport.validateCodeInValueSet(any(), any(), any(), any(), any(), any(ValueSet.class))).thenReturn(new IValidationSupport.CodeValidationResult().setCode(CODE_ICC_SCHOOLTYPE_PT)); when(myValSupport.fetchCodeSystem(eq(SYSTEMURI_ICC_SCHOOLTYPE))).thenReturn(codeSystem); ValidationResult errors = myVal.validateWithResult(qa); diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index d3fa30406ab..93b151848b7 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index db18a5dad1f..15b14e92ac8 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 9b19056b43d..05227daf415 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. @@ -1033,7 +1033,13 @@ 3.3.0 1.8 4.12.0 + 2.8.0 + ${otel_instrumentation.version}-alpha 4.1.2 1.4 6.2.9.Final @@ -2306,6 +2312,16 @@ pom import
    + + io.opentelemetry.javaagent + opentelemetry-testing-common + ${otel_agent_for_testing.version} + + + io.opentelemetry.javaagent + opentelemetry-agent-for-testing + ${otel_agent_for_testing.version} + org.assertj @@ -2623,7 +2639,7 @@ ca.uhn.hapi.fhir hapi-tinder-plugin - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 2618b695119..7310102bfc5 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.7-SNAPSHOT + 7.7.8-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 1f3098c2673..df98eac11cc 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 - 7.7.7-SNAPSHOT + 7.7.8-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 f4c44792a09..180c4f6347f 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 - 7.7.7-SNAPSHOT + 7.7.8-SNAPSHOT ../../pom.xml From d145e2a4bba771e8fc99cd07c7700b96b15e6e11 Mon Sep 17 00:00:00 2001 From: TipzCM Date: Thu, 28 Nov 2024 13:16:00 -0500 Subject: [PATCH 6/6] 6404 lucene search fulltext fix (#6517) --- .../6404-fulltext-search-with-lastupdate.yaml | 8 + .../uhn/hapi/fhir/changelog/7_8_0/upgrade.md | 4 + .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 2 +- .../fhir/jpa/dao/FulltextSearchSvcImpl.java | 4 +- .../uhn/fhir/jpa/dao/IFulltextSearchSvc.java | 2 +- .../search/ExtendedHSearchClauseBuilder.java | 2 +- .../search/ExtendedHSearchIndexExtractor.java | 45 ++- .../search/ExtendedHSearchSearchBuilder.java | 30 +- ...esourceDaoR4SearchWithElasticSearchIT.java | 30 +- .../search/ExtendedHSearchIndexData.java | 8 +- .../fhir/jpa/dao/r4/FhirSearchDaoR4Test.java | 164 --------- .../TestElasticsearchContainerHelper.java | 2 + .../test/config/TestHSearchAddInConfig.java | 2 - ...esourceDaoR4StandardQueriesLuceneTest.java | 35 +- .../fhir/jpa/dao/r4/ILuceneSearchR4Test.java | 315 ++++++++++++++++++ .../ExtendedHSearchIndexExtractorTest.java | 5 +- .../fhir/storage/test/DaoTestDataBuilder.java | 29 +- 17 files changed, 474 insertions(+), 213 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6404-fulltext-search-with-lastupdate.yaml create mode 100644 hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6404-fulltext-search-with-lastupdate.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6404-fulltext-search-with-lastupdate.yaml new file mode 100644 index 00000000000..9ada986bc56 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6404-fulltext-search-with-lastupdate.yaml @@ -0,0 +1,8 @@ +--- +type: fix +issue: 6404 +title: "Searches using fulltext search that combined `_lastUpdated` query parameter with + any other (supported) fulltext query parameter would find no matches, even + if matches existed. + This has been corrected. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md index e69de29bb2d..58969977a1b 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/upgrade.md @@ -0,0 +1,4 @@ +# Fulltext Search with _lastUpdated Filter + +Fulltext searches have been updated to support `_lastUpdated` search parameter. A reindexing of Search Parameters +is required to migrate old data to support the `_lastUpdated` search parameter. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 6335adde1b3..5dc46fc6138 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1689,7 +1689,7 @@ public abstract class BaseHapiFhirDao extends BaseStora theEntity.setContentText(parseContentTextIntoWords(theContext, theResource)); if (myStorageSettings.isAdvancedHSearchIndexing()) { ExtendedHSearchIndexData hSearchIndexData = - myFulltextSearchSvc.extractLuceneIndexData(theResource, theNewParams); + myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams); theEntity.setLuceneIndexData(hSearchIndexData); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java index c313854c355..c5a5dba6d94 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FulltextSearchSvcImpl.java @@ -135,13 +135,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc { @Override public ExtendedHSearchIndexData extractLuceneIndexData( - IBaseResource theResource, ResourceIndexedSearchParams theNewParams) { + IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) { String resourceType = myFhirContext.getResourceType(theResource); ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams( resourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor( myStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor); - return extractor.extract(theResource, theNewParams); + return extractor.extract(theResource, theEntity, theNewParams); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java index 52dd7589947..0b795fb36a8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/IFulltextSearchSvc.java @@ -89,7 +89,7 @@ public interface IFulltextSearchSvc { boolean isDisabled(); ExtendedHSearchIndexData extractLuceneIndexData( - IBaseResource theResource, ResourceIndexedSearchParams theNewParams); + IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams); /** * Returns true if the parameter map can be handled for hibernate search. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java index 37a2a8830ef..e33b4c293d3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchClauseBuilder.java @@ -392,7 +392,7 @@ public class ExtendedHSearchClauseBuilder { /** * Create date clause from date params. The date lower and upper bounds are taken - * into considertion when generating date query ranges + * into consideration when generating date query ranges * *

    Example 1 ('eq' prefix/empty): http://fhirserver/Observation?date=eq2020 * would generate the following search clause diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java index 63642b2b4f1..3b212338602 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractor.java @@ -25,6 +25,8 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData; import ca.uhn.fhir.jpa.model.search.DateSearchIndexData; import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData; @@ -37,6 +39,7 @@ import ca.uhn.fhir.rest.server.util.ResourceSearchParams; import ca.uhn.fhir.util.MetaUtil; import com.google.common.base.Strings; import jakarta.annotation.Nonnull; +import org.apache.commons.lang3.ObjectUtils; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -74,8 +77,10 @@ public class ExtendedHSearchIndexExtractor { } @Nonnull - public ExtendedHSearchIndexData extract(IBaseResource theResource, ResourceIndexedSearchParams theNewParams) { - ExtendedHSearchIndexData retVal = new ExtendedHSearchIndexData(myContext, myJpaStorageSettings, theResource); + public ExtendedHSearchIndexData extract( + IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) { + ExtendedHSearchIndexData retVal = + new ExtendedHSearchIndexData(myContext, myJpaStorageSettings, theResource, theEntity); if (myJpaStorageSettings.isStoreResourceInHSearchIndex()) { retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource)); @@ -113,11 +118,27 @@ public class ExtendedHSearchIndexExtractor { .filter(nextParam -> !nextParam.isMissing()) .forEach(nextParam -> retVal.addUriIndexData(nextParam.getParamName(), nextParam.getUri())); - theResource.getMeta().getTag().forEach(tag -> retVal.addTokenIndexData("_tag", tag)); + theEntity.getTags().forEach(tag -> { + TagDefinition td = tag.getTag(); - theResource.getMeta().getSecurity().forEach(sec -> retVal.addTokenIndexData("_security", sec)); - - theResource.getMeta().getProfile().forEach(prof -> retVal.addUriIndexData("_profile", prof.getValue())); + IBaseCoding coding = (IBaseCoding) myContext.getVersion().newCodingDt(); + coding.setVersion(td.getVersion()); + coding.setDisplay(td.getDisplay()); + coding.setCode(td.getCode()); + coding.setSystem(td.getSystem()); + coding.setUserSelected(ObjectUtils.defaultIfNull(td.getUserSelected(), false)); + switch (td.getTagType()) { + case TAG: + retVal.addTokenIndexData("_tag", coding); + break; + case PROFILE: + retVal.addUriIndexData("_profile", coding.getCode()); + break; + case SECURITY_LABEL: + retVal.addTokenIndexData("_security", coding); + break; + } + }); String source = MetaUtil.getSource(myContext, theResource.getMeta()); if (isNotBlank(source)) { @@ -127,20 +148,14 @@ public class ExtendedHSearchIndexExtractor { theNewParams.myCompositeParams.forEach(nextParam -> retVal.addCompositeIndexData(nextParam.getSearchParamName(), buildCompositeIndexData(nextParam))); - if (theResource.getMeta().getLastUpdated() != null) { - int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue( - theResource.getMeta().getLastUpdated()) + if (theEntity.getUpdated() != null && !theEntity.getUpdated().isEmpty()) { + int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theEntity.getUpdatedDate()) .intValue(); retVal.addDateIndexData( - "_lastUpdated", - theResource.getMeta().getLastUpdated(), - ordinal, - theResource.getMeta().getLastUpdated(), - ordinal); + "_lastUpdated", theEntity.getUpdatedDate(), ordinal, theEntity.getUpdatedDate(), ordinal); } if (!theNewParams.myLinks.isEmpty()) { - // awkwardly, links are indexed by jsonpath, not by search param. // so we re-build the linkage. Map> linkPathToParamName = new HashMap<>(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java index 79029f95585..c9b659e5def 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchSearchBuilder.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.SearchContainedModeEnum; import ca.uhn.fhir.rest.param.CompositeParam; import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -89,19 +90,28 @@ public class ExtendedHSearchSearchBuilder { * be inaccurate and wrong. */ public boolean canUseHibernateSearch( - String theResourceType, SearchParameterMap myParams, ISearchParamRegistry theSearchParamRegistry) { + String theResourceType, SearchParameterMap theParams, ISearchParamRegistry theSearchParamRegistry) { boolean canUseHibernate = false; ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams( theResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); - for (String paramName : myParams.keySet()) { + for (String paramName : theParams.keySet()) { + // special SearchParam handling: + // _lastUpdated + if (theParams.getLastUpdated() != null) { + canUseHibernate = !illegalForHibernateSearch(Constants.PARAM_LASTUPDATED, resourceActiveSearchParams); + if (!canUseHibernate) { + return false; + } + } + // is this parameter supported? if (illegalForHibernateSearch(paramName, resourceActiveSearchParams)) { canUseHibernate = false; } else { // are the parameter values supported? canUseHibernate = - myParams.get(paramName).stream() + theParams.get(paramName).stream() .flatMap(Collection::stream) .collect(Collectors.toList()) .stream() @@ -136,6 +146,7 @@ public class ExtendedHSearchSearchBuilder { // not yet supported in HSearch myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE + && supportsLastUpdated(myParams) && // ??? myParams.entrySet().stream() .filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey())) @@ -145,6 +156,19 @@ public class ExtendedHSearchSearchBuilder { .allMatch(this::isParamTypeSupported); } + private boolean supportsLastUpdated(SearchParameterMap theMap) { + if (theMap.getLastUpdated() == null || theMap.getLastUpdated().isEmpty()) { + return true; + } + + DateRangeParam lastUpdated = theMap.getLastUpdated(); + + return lastUpdated.getLowerBound() != null + && isParamTypeSupported(lastUpdated.getLowerBound()) + && lastUpdated.getUpperBound() != null + && isParamTypeSupported(lastUpdated.getUpperBound()); + } + /** * Do we support this query param type+modifier? *

    diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java index 1770bcd910c..957a4b2702a 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java @@ -294,6 +294,20 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl } } + @Test + public void testNoOpUpdateDoesNotModifyLastUpdated() throws InterruptedException { + myStorageSettings.setAdvancedHSearchIndexing(true); + Patient patient = new Patient(); + patient.getNameFirstRep().setFamily("graham").addGiven("gary"); + + patient = (Patient) myPatientDao.create(patient).getResource(); + Date originalLastUpdated = patient.getMeta().getLastUpdated(); + + patient = (Patient) myPatientDao.update(patient).getResource(); + Date newLastUpdated = patient.getMeta().getLastUpdated(); + + assertThat(originalLastUpdated).isEqualTo(newLastUpdated); + } @Test public void testFullTextSearchesArePerformanceLogged() { @@ -1804,68 +1818,52 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl @Test public void eq() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=eq" + myOldLastUpdatedDateTime); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myOldObsId); } @Test public void eqLessPrecisionRequest() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=eq2017-03-24"); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myOldObsId); } @Test public void ne() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=ne" + myOldLastUpdatedDateTime); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myNewObsId); } @Test void gt() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=gt2018-01-01"); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myNewObsId); } @Test public void ge() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=ge" + myOldLastUpdatedDateTime); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myOldObsId, myNewObsId); } @Test void lt() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=lt2018-01-01"); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myOldObsId); } @Test public void le() { - myCaptureQueriesListener.clear(); List allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=le" + myOldLastUpdatedDateTime); - assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("we build the bundle with no sql").isEqualTo(0); assertThat(allIds).containsExactly(myOldObsId); } - - } @Nested diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java index 9af2aabf4de..ef8f9967286 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/search/ExtendedHSearchIndexData.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.model.search; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.model.dstu2.composite.CodingDt; import com.google.common.collect.HashMultimap; @@ -57,12 +58,17 @@ public class ExtendedHSearchIndexData { private String myForcedId; private String myResourceJSON; private IBaseResource myResource; + private ResourceTable myEntity; public ExtendedHSearchIndexData( - FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { + FhirContext theFhirContext, + StorageSettings theStorageSettings, + IBaseResource theResource, + ResourceTable theEntity) { this.myFhirContext = theFhirContext; this.myStorageSettings = theStorageSettings; myResource = theResource; + myEntity = theEntity; } private BiConsumer ifNotContained(BiConsumer theIndexWriter) { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java index a0f80459464..1b4a5adbef2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSearchDaoR4Test.java @@ -305,168 +305,4 @@ public class FhirSearchDaoR4Test extends BaseJpaR4Test implements IR4SearchIndex } } - @Test - public void testSearchNarrativeWithLuceneSearch() { - final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; - List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); - - for (int i = 0; i < numberOfPatientsToCreate; i++) { - Patient patient = new Patient(); - patient.getText().setDivAsString("

    AAAS

    FOO

    CCC
    "); - expectedActivePatientIds.add(myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless().getIdPart()); - } - - { - Patient patient = new Patient(); - patient.getText().setDivAsString("
    AAAB

    FOO

    CCC
    "); - myPatientDao.create(patient, mySrd); - } - { - Patient patient = new Patient(); - patient.getText().setDivAsString("
    ZZYZXY
    "); - myPatientDao.create(patient, mySrd); - } - - SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); - map.add(Constants.PARAM_TEXT, new StringParam("AAAS")); - - IBundleProvider searchResultBundle = myPatientDao.search(map, mySrd); - List resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); - - assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); - } - - @Test - public void searchLuceneAndJPA_withLuceneMatchingButJpaNot_returnsNothing() { - // setup - int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10; - - // create resources - for (int i = 0; i < numToCreate; i++) { - Patient patient = new Patient(); - patient.setActive(true); - patient.addIdentifier() - .setSystem("http://fhir.com") - .setValue("ZYX"); - patient.getText().setDivAsString("
    ABC
    "); - myPatientDao.create(patient, mySrd); - } - - // test - SearchParameterMap map = new SearchParameterMap(); - map.setLoadSynchronous(true); - map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); - map.add("active", tokenAndListParam); - map.add(Constants.PARAM_TEXT, new StringParam("ABC")); - map.add("identifier", new TokenParam(null, "not found")); - IBundleProvider provider = myPatientDao.search(map, mySrd); - - // verify - assertEquals(0, provider.getAllResources().size()); - } - - @Test - public void searchLuceneAndJPA_withLuceneBroadAndJPASearchNarrow_returnsFoundResults() { - // setup - int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10; - String identifierToFind = "bcde"; - - // create patients - for (int i = 0; i < numToCreate; i++) { - Patient patient = new Patient(); - patient.setActive(true); - String identifierVal = i == numToCreate - 10 ? identifierToFind: - "abcd"; - patient.addIdentifier() - .setSystem("http://fhir.com") - .setValue(identifierVal); - - patient.getText().setDivAsString( - "
    FINDME
    " - ); - myPatientDao.create(patient, mySrd); - } - - // test - SearchParameterMap map = new SearchParameterMap(); - map.setLoadSynchronous(true); - map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); - map.add("active", tokenAndListParam); - map.add(Constants.PARAM_TEXT, new StringParam("FINDME")); - map.add("identifier", new TokenParam(null, identifierToFind)); - IBundleProvider provider = myPatientDao.search(map, mySrd); - - // verify - List ids = provider.getAllResourceIds(); - assertEquals(1, ids.size()); - } - - @Test - public void testLuceneNarrativeSearchQueryIntersectingJpaQuery() { - final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; - List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); - - // create active and non-active patients with the same narrative - for (int i = 0; i < numberOfPatientsToCreate; i++) { - Patient activePatient = new Patient(); - activePatient.getText().setDivAsString("
    AAAS

    FOO

    CCC
    "); - activePatient.setActive(true); - String patientId = myPatientDao.create(activePatient, mySrd).getId().toUnqualifiedVersionless().getIdPart(); - expectedActivePatientIds.add(patientId); - - Patient nonActivePatient = new Patient(); - nonActivePatient.getText().setDivAsString("
    AAAS

    FOO

    CCC
    "); - nonActivePatient.setActive(false); - myPatientDao.create(nonActivePatient, mySrd); - } - - SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); - - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); - - map.add("active", tokenAndListParam); - map.add(Constants.PARAM_TEXT, new StringParam("AAAS")); - - IBundleProvider searchResultBundle = myPatientDao.search(map, mySrd); - List resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); - - assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); - } - - @Test - public void testLuceneContentSearchQueryIntersectingJpaQuery() { - final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; - final String patientFamilyName = "Flanders"; - List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); - - // create active and non-active patients with the same narrative - for (int i = 0; i < numberOfPatientsToCreate; i++) { - Patient activePatient = new Patient(); - activePatient.addName().setFamily(patientFamilyName); - activePatient.setActive(true); - String patientId = myPatientDao.create(activePatient, mySrd).getId().toUnqualifiedVersionless().getIdPart(); - expectedActivePatientIds.add(patientId); - - Patient nonActivePatient = new Patient(); - nonActivePatient.addName().setFamily(patientFamilyName); - nonActivePatient.setActive(false); - myPatientDao.create(nonActivePatient, mySrd); - } - - SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); - map.add("active", tokenAndListParam); - map.add(Constants.PARAM_CONTENT, new StringParam(patientFamilyName)); - - IBundleProvider searchResultBundle = myPatientDao.search(map, mySrd); - List resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); - - assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); - } } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestElasticsearchContainerHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestElasticsearchContainerHelper.java index fdfcff8f435..fc60006dab5 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestElasticsearchContainerHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestElasticsearchContainerHelper.java @@ -38,6 +38,8 @@ public class TestElasticsearchContainerHelper { .withEnv("ES_JAVA_OPTS", "-Xms512m -Xmx512m") // turn off security warnings .withEnv("xpack.security.enabled", "false") + // turn off machine learning (we don't need it in tests anyways) + .withEnv("xpack.ml.enabled", "false") .withStartupTimeout(Duration.of(300, SECONDS)); } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java index be2846e5cbe..8cb83a77569 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java @@ -85,7 +85,6 @@ public class TestHSearchAddInConfig { Path tempDirPath = Files.createTempDirectory(null); String dirPath = tempDirPath.toString(); - Map luceneProperties = new HashMap<>(); luceneProperties.put(BackendSettings.backendKey(BackendSettings.TYPE), "lucene"); luceneProperties.put(BackendSettings.backendKey(LuceneBackendSettings.ANALYSIS_CONFIGURER), @@ -117,7 +116,6 @@ public class TestHSearchAddInConfig { } - /** * Our default config - Lucene in-memory. */ diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java index 814b342aab7..31ce2f00f93 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4StandardQueriesLuceneTest.java @@ -2,19 +2,29 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper; import ca.uhn.fhir.jpa.dao.TestDaoSearch; import ca.uhn.fhir.jpa.search.BaseSourceSearchParameterTestCases; import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases; import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases; +import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.test.BaseJpaTest; import ca.uhn.fhir.jpa.test.config.TestR4Config; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.storage.test.BaseDateSearchDaoTests; import ca.uhn.fhir.storage.test.DaoTestDataBuilder; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.Reference; @@ -32,15 +42,15 @@ import org.springframework.transaction.PlatformTransactionManager; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertThrows; - @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = { TestR4Config.class, DaoTestDataBuilder.Config.class, TestDaoSearch.Config.class }) -public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest { +public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest + implements ILuceneSearchR4Test { + FhirContext myFhirContext = FhirContext.forR4Cached(); @Autowired PlatformTransactionManager myTxManager; @@ -53,6 +63,19 @@ public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest { @Qualifier("myObservationDaoR4") IFhirResourceDao myObservationDao; @Autowired + @Qualifier("myPatientDaoR4") + protected IFhirResourceDaoPatient myPatientDao; + @Autowired + private IFhirSystemDao mySystemDao; + @Autowired + private IResourceReindexingSvc myResourceReindexingSvc; + @Autowired + protected ISearchCoordinatorSvc mySearchCoordinatorSvc; + @Autowired + protected ISearchParamRegistry mySearchParamRegistry; + @Autowired + private IBulkDataExportJobSchedulingHelper myBulkDataScheduleHelper; + @Autowired IFhirResourceDao myPractitionerDao; @Autowired IFhirResourceDao myPractitionerRoleDao; @@ -60,6 +83,7 @@ public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest { // todo mb create an extension to restore via clone or xstream + BeanUtils.copyProperties(). @BeforeEach void setUp() { + purgeDatabase(myStorageSettings, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataScheduleHelper); myStorageSettings.setAdvancedHSearchIndexing(true); } @@ -79,6 +103,11 @@ public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest { return myTxManager; } + @Override + public DaoRegistry getDaoRegistry() { + return myDaoRegistry; + } + @Nested public class DateSearchTests extends BaseDateSearchDaoTests { @Override diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java new file mode 100644 index 00000000000..7cd09e65c07 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/ILuceneSearchR4Test.java @@ -0,0 +1,315 @@ +package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.model.dao.JpaPid; +import ca.uhn.fhir.jpa.search.builder.SearchBuilder; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.SearchTotalModeEnum; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.util.DateRangeUtil; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public interface ILuceneSearchR4Test { + DaoRegistry getDaoRegistry(); + + @SuppressWarnings("rawtypes") + private IFhirResourceDao getResourceDao(String theResourceType) { + return getDaoRegistry() + .getResourceDao(theResourceType); + } + + void runInTransaction(Runnable theRunnable); + + @Test + default void testNoOpUpdateDoesNotModifyLastUpdated() throws InterruptedException { + IFhirResourceDao patientDao = getResourceDao("Patient"); + + Patient patient = new Patient(); + patient.getNameFirstRep().setFamily("graham").addGiven("gary"); + + patient = (Patient) patientDao.create(patient).getResource(); + Date originalLastUpdated = patient.getMeta().getLastUpdated(); + + patient = (Patient) patientDao.update(patient).getResource(); + Date newLastUpdated = patient.getMeta().getLastUpdated(); + + assertThat(originalLastUpdated).isEqualTo(newLastUpdated); + } + + @Test + default void luceneSearch_forTagsAndLastUpdated_shouldReturn() { + // setup + SystemRequestDetails requestDeatils = new SystemRequestDetails(); + String system = "http://fhir"; + String code = "cv"; + Date start = Date.from(Instant.now().minus(1, ChronoUnit.SECONDS).truncatedTo(ChronoUnit.SECONDS)); + Date end = Date.from(Instant.now().plus(10, ChronoUnit.SECONDS).truncatedTo(ChronoUnit.SECONDS)); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + // create a patient with some tag + Patient patient = new Patient(); + patient.getMeta() + .addTag(system, code, ""); + patient.addName().addGiven("homer") + .setFamily("simpson"); + patient.addAddress() + .setCity("springfield") + .addLine("742 evergreen terrace"); + Long id = patientDao.create(patient, requestDeatils).getId().toUnqualifiedVersionless().getIdPartAsLong(); + + // create base search map + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); + TokenOrListParam goldenRecordStatusToken = new TokenOrListParam(system, code); + map.add(Constants.PARAM_TAG, goldenRecordStatusToken); + DateRangeParam lastUpdated = DateRangeUtil.narrowDateRange(map.getLastUpdated(), start, end); + map.setLastUpdated(lastUpdated); + + runInTransaction(() -> { + Stream stream; + List list; + Optional first; + + // tag search only; should return our resource + map.setLastUpdated(null); + stream = patientDao.searchForIdStream(map, new SystemRequestDetails(), null); + list = stream.toList(); + assertEquals(1, list.size()); + first = list.stream().findFirst(); + assertTrue(first.isPresent()); + assertEquals(id, first.get().getId()); + + // last updated search only; should return our resource + map.setLastUpdated(lastUpdated); + map.remove(Constants.PARAM_TAG); + stream = patientDao.searchForIdStream(map, new SystemRequestDetails(), null); + list = stream.toList(); + assertEquals(1, list.size()); + first = list.stream().findFirst(); + assertTrue(first.isPresent()); + assertEquals(id, first.get().getId()); + + // both last updated and tags; should return our resource + map.add(Constants.PARAM_TAG, goldenRecordStatusToken); + stream = patientDao.searchForIdStream(map, new SystemRequestDetails(), null); + list = stream.toList(); + assertEquals(1, list.size()); + first = list.stream().findFirst(); + assertTrue(first.isPresent()); + assertEquals(id, first.get().getId()); + }); + } + + @Test + default void searchLuceneAndJPA_withLuceneMatchingButJpaNot_returnsNothing() { + // setup + int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10; + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + // create resources + for (int i = 0; i < numToCreate; i++) { + Patient patient = new Patient(); + patient.setActive(true); + patient.addIdentifier() + .setSystem("http://fhir.com") + .setValue("ZYX"); + patient.getText().setDivAsString("
    ABC
    "); + patientDao.create(patient, requestDetails); + } + + // test + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE); + TokenAndListParam tokenAndListParam = new TokenAndListParam(); + tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); + map.add("active", tokenAndListParam); + map.add(Constants.PARAM_TEXT, new StringParam("ABC")); + map.add("identifier", new TokenParam(null, "not found")); + IBundleProvider provider = patientDao.search(map, requestDetails); + + // verify + assertEquals(0, provider.getAllResources().size()); + } + + @Test + default void testLuceneNarrativeSearchQueryIntersectingJpaQuery() { + final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; + List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + + // create active and non-active patients with the same narrative + for (int i = 0; i < numberOfPatientsToCreate; i++) { + Patient activePatient = new Patient(); + activePatient.getText().setDivAsString("
    AAAS

    FOO

    CCC
    "); + activePatient.setActive(true); + String patientId = patientDao.create(activePatient, requestDetails).getId().toUnqualifiedVersionless().getIdPart(); + expectedActivePatientIds.add(patientId); + + Patient nonActivePatient = new Patient(); + nonActivePatient.getText().setDivAsString("
    AAAS

    FOO

    CCC
    "); + nonActivePatient.setActive(false); + patientDao.create(nonActivePatient, requestDetails); + } + + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); + + TokenAndListParam tokenAndListParam = new TokenAndListParam(); + tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); + + map.add("active", tokenAndListParam); + map.add(Constants.PARAM_TEXT, new StringParam("AAAS")); + + IBundleProvider searchResultBundle = patientDao.search(map, requestDetails); + List resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); + + assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); + } + + @Test + default void testLuceneContentSearchQueryIntersectingJpaQuery() { + final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; + final String patientFamilyName = "Flanders"; + List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + + // create active and non-active patients with the same narrative + for (int i = 0; i < numberOfPatientsToCreate; i++) { + Patient activePatient = new Patient(); + activePatient.addName().setFamily(patientFamilyName); + activePatient.setActive(true); + String patientId = patientDao.create(activePatient, requestDetails).getId().toUnqualifiedVersionless().getIdPart(); + expectedActivePatientIds.add(patientId); + + Patient nonActivePatient = new Patient(); + nonActivePatient.addName().setFamily(patientFamilyName); + nonActivePatient.setActive(false); + patientDao.create(nonActivePatient, requestDetails); + } + + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); + TokenAndListParam tokenAndListParam = new TokenAndListParam(); + tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); + map.add("active", tokenAndListParam); + map.add(Constants.PARAM_CONTENT, new StringParam(patientFamilyName)); + + IBundleProvider searchResultBundle = patientDao.search(map, requestDetails); + List resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); + + assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); + } + + @Test + default void searchLuceneAndJPA_withLuceneBroadAndJPASearchNarrow_returnsFoundResults() { + // setup + int numToCreate = 2 * SearchBuilder.getMaximumPageSize() + 10; + String identifierToFind = "bcde"; + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + // create patients + for (int i = 0; i < numToCreate; i++) { + Patient patient = new Patient(); + patient.setActive(true); + String identifierVal = i == numToCreate - 10 ? identifierToFind: + "abcd"; + patient.addIdentifier() + .setSystem("http://fhir.com") + .setValue(identifierVal); + + patient.getText().setDivAsString( + "
    FINDME
    " + ); + patientDao.create(patient, requestDetails); + } + + // test + SearchParameterMap map = new SearchParameterMap(); + map.setLoadSynchronous(true); + map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE); + TokenAndListParam tokenAndListParam = new TokenAndListParam(); + tokenAndListParam.addAnd(new TokenOrListParam().addOr(new TokenParam().setValue("true"))); + map.add("active", tokenAndListParam); + map.add(Constants.PARAM_TEXT, new StringParam("FINDME")); + map.add("identifier", new TokenParam(null, identifierToFind)); + IBundleProvider provider = patientDao.search(map, requestDetails); + + // verify + List ids = provider.getAllResourceIds(); + assertEquals(1, ids.size()); + } + + @Test + default void testSearchNarrativeWithLuceneSearch() { + final int numberOfPatientsToCreate = SearchBuilder.getMaximumPageSize() + 10; + List expectedActivePatientIds = new ArrayList<>(numberOfPatientsToCreate); + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + + @SuppressWarnings("unchecked") + IFhirResourceDao patientDao = getResourceDao("Patient"); + + + for (int i = 0; i < numberOfPatientsToCreate; i++) { + Patient patient = new Patient(); + patient.getText().setDivAsString("
    AAAS

    FOO

    CCC
    "); + expectedActivePatientIds.add(patientDao.create(patient, requestDetails).getId().toUnqualifiedVersionless().getIdPart()); + } + + { + Patient patient = new Patient(); + patient.getText().setDivAsString("
    AAAB

    FOO

    CCC
    "); + patientDao.create(patient, requestDetails); + } + { + Patient patient = new Patient(); + patient.getText().setDivAsString("
    ZZYZXY
    "); + patientDao.create(patient, requestDetails); + } + + SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true); + map.add(Constants.PARAM_TEXT, new StringParam("AAAS")); + + IBundleProvider searchResultBundle = patientDao.search(map, requestDetails); + List resourceIdsFromSearchResult = searchResultBundle.getAllResourceIds(); + + assertThat(resourceIdsFromSearchResult).containsExactlyInAnyOrderElementsOf(expectedActivePatientIds); + } + +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java index cb7ae2cf928..931784cfdd8 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/search/ExtendedHSearchIndexExtractorTest.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData; import ca.uhn.fhir.jpa.model.search.DateSearchIndexData; import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData; @@ -55,7 +56,7 @@ class ExtendedHSearchIndexExtractorTest implements ITestDataBuilder.WithSupport ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Observation", ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor( myJpaStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor); - ExtendedHSearchIndexData indexData = extractor.extract(new Observation(), extractedParams); + ExtendedHSearchIndexData indexData = extractor.extract(new Observation(), new ResourceTable(), extractedParams); // validate Set spIndexData = indexData.getSearchParamComposites().get("component-code-value-concept"); @@ -78,7 +79,7 @@ class ExtendedHSearchIndexExtractorTest implements ITestDataBuilder.WithSupport ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Patient", ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor( myJpaStorageSettings, myFhirContext, activeSearchParams, mySearchParamExtractor); - ExtendedHSearchIndexData indexData = extractor.extract(new SearchParameter(), searchParams); + ExtendedHSearchIndexData indexData = extractor.extract(new SearchParameter(), new ResourceTable(), searchParams); // validate Set dIndexData = indexData.getDateIndexData().get("Date"); diff --git a/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java b/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java index cd7f6019568..f8320ede706 100644 --- a/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java +++ b/hapi-fhir-storage-test-utilities/src/main/java/ca/uhn/fhir/storage/test/DaoTestDataBuilder.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; import ca.uhn.fhir.test.utilities.ITestDataBuilder; import ca.uhn.fhir.util.BundleBuilder; import com.google.common.collect.HashMultimap; @@ -66,18 +67,42 @@ public class DaoTestDataBuilder implements ITestDataBuilder.WithSupport, ITestDa } //noinspection rawtypes IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); + + // manipulate the transaction details to provide a fake transaction date + TransactionDetails details = null; + if (theResource.getMeta() != null && theResource.getMeta().getLastUpdated() != null) { + details = new TransactionDetails(theResource.getMeta().getLastUpdated()); + } else { + details = new TransactionDetails(); + } + //noinspection unchecked - IIdType id = dao.create(theResource, mySrd).getId().toUnqualifiedVersionless(); + IIdType id = dao.create(theResource, null, true, mySrd, details) + .getId().toUnqualifiedVersionless(); myIds.put(theResource.fhirType(), id); return id; } @Override public IIdType doUpdateResource(IBaseResource theResource) { + // manipulate the transaction details to provdie a fake transaction date + TransactionDetails details = null; + if (theResource.getMeta() != null && theResource.getMeta().getLastUpdated() != null) { + details = new TransactionDetails(theResource.getMeta().getLastUpdated()); + } else { + details = new TransactionDetails(); + } + //noinspection rawtypes IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); //noinspection unchecked - IIdType id = dao.update(theResource, mySrd).getId().toUnqualifiedVersionless(); + IIdType id = dao.update(theResource, + null, + true, + false, + mySrd, + details) + .getId().toUnqualifiedVersionless(); myIds.put(theResource.fhirType(), id); return id; }