From 0f2fc7a8823e2836fe9ec29afc737540bf8fd7e5 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 9 Feb 2022 14:27:14 -0500 Subject: [PATCH] Allow search narrowing and auth interceptor by `token:in` (#3360) * Optmize valueset expansion * Working * Version bump * Add documentation * Add test * Checkstyle message cleanup * Add reverse rule * Test ficx * Test fix * Test fix * Test fixes * Test fixes * Test fix * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/security/search_narrowing_interceptor.md Co-authored-by: Ken Stevens * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/security/search_narrowing_interceptor.md Co-authored-by: Ken Stevens * Test fixes * Fix conflict * Add setter * Test fixes Co-authored-by: Ken Stevens --- hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- .../context/support/IValidationSupport.java | 2 +- .../src/main/java/ca/uhn/fhir/i18n/Msg.java | 2 +- .../java/ca/uhn/fhir/rest/api/Constants.java | 5 + .../main/java/ca/uhn/fhir/util/UrlUtil.java | 9 +- hapi-fhir-batch/pom.xml | 2 +- hapi-fhir-bom/pom.xml | 4 +- hapi-fhir-checkstyle/pom.xml | 2 +- .../uhn/fhir/checks/HapiErrorCodeCheck.java | 14 +- .../fhir/checks/HapiErrorCodeCheckTest.java | 4 +- hapi-fhir-cli/hapi-fhir-cli-api/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-app/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml | 2 +- hapi-fhir-cli/pom.xml | 2 +- hapi-fhir-client-okhttp/pom.xml | 2 +- hapi-fhir-client/pom.xml | 2 +- hapi-fhir-converter/pom.xml | 2 +- hapi-fhir-dist/pom.xml | 2 +- hapi-fhir-docs/pom.xml | 2 +- .../fhir/docs/AuthorizationInterceptors.java | 36 +- .../3360-add-support-for-not-in-search.yaml | 6 + ...mbership-in-authorization-interceptor.yaml | 5 + ...rship-in-search-narrowing-interceptor.yaml | 4 + .../3360-improve-vs-expand-performance.yaml | 5 + .../security/search_narrowing_interceptor.md | 16 +- 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/BaseConfig.java | 4 - .../fhir/jpa/config/BaseConfigDstu3Plus.java | 1 - .../uhn/fhir/jpa/config/r4/BaseR4Config.java | 4 +- .../fhir/jpa/dao/data/ITermConceptDao.java | 11 + .../jpa/entity/TermConceptDesignation.java | 15 + .../fhir/jpa/entity/TermConceptProperty.java | 1 + .../fhir/jpa/entity/TermValueSetConcept.java | 21 +- .../fhir/jpa/packages/JpaPackageCache.java | 2 +- .../provider/ValueSetOperationProvider.java | 39 +- .../TokenAutocompleteAggregation.java | 20 + .../autocomplete/TokenAutocompleteHit.java | 20 + .../autocomplete/TokenAutocompleteSearch.java | 22 +- .../ValueSetAutocompleteOptions.java | 20 + .../ValueSetAutocompleteSearch.java | 20 + .../jpa/search/autocomplete/package-info.java | 20 + .../predicate/TokenPredicateBuilder.java | 81 +++- .../fhir/jpa/term/BaseTermReadSvcImpl.java | 119 +++-- .../validation/JpaValidationSupportChain.java | 3 + .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 175 ++++--- .../fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java | 10 + .../fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java | 12 +- .../FhirResourceDaoDstu3TerminologyTest.java | 2 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 56 ++- ...FhirResourceDaoR4ComboUniqueParamTest.java | 38 +- .../r4/FhirResourceDaoR4QueryCountTest.java | 133 ++++++ .../r4/FhirResourceDaoR4TerminologyTest.java | 69 +-- ...ResourceDaoR4ValueSetMultiVersionTest.java | 1 + .../jpa/dao/r4/PartitioningSqlR4Test.java | 5 +- .../ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java | 8 +- .../ResourceProviderDstu3ValueSetTest.java | 4 +- ...rceProviderDstu3ValueSetVersionedTest.java | 4 +- .../r4/AuthorizationInterceptorJpaR4Test.java | 31 ++ ...rceProviderR4ValueSetNoVerCSNoVerTest.java | 24 +- ...ourceProviderR4ValueSetVerCSNoVerTest.java | 8 +- ...esourceProviderR4ValueSetVerCSVerTest.java | 4 +- .../r5/ResourceProviderR5ValueSetTest.java | 4 +- ...sourceProviderR5ValueSetVersionedTest.java | 4 +- hapi-fhir-jpaserver-cql/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 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 2 +- hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server-openapi/pom.xml | 2 +- hapi-fhir-server/pom.xml | 2 +- .../auth/AllowedCodeInValueSet.java | 59 +++ .../auth/AuthorizationInterceptor.java | 97 ++-- .../interceptor/auth/AuthorizedList.java | 57 +++ .../IAuthRuleBuilderRuleOpClassifier.java | 18 + .../server/interceptor/auth/IRuleApplier.java | 10 + .../server/interceptor/auth/RuleBuilder.java | 52 ++- .../server/interceptor/auth/RuleImplOp.java | 13 +- .../auth/SearchNarrowingInterceptor.java | 202 ++++++--- .../SearchParameterAndValueSetRuleImpl.java | 163 +++++++ .../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/pom.xml | 2 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- hapi-fhir-structures-r4/pom.xml | 2 +- .../auth/SearchNarrowingInterceptorTest.java | 258 ++++++++--- 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-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 2 +- .../support/CachingValidationSupport.java | 40 +- .../auth/AuthorizationInterceptorR4Test.java | 428 +++++++++++++++++- hapi-tinder-plugin/pom.xml | 16 +- hapi-tinder-test/pom.xml | 2 +- pom.xml | 4 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- 119 files changed, 2139 insertions(+), 515 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-not-in-search.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-authorization-interceptor.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-search-narrowing-interceptor.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-improve-vs-expand-performance.yaml create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AllowedCodeInValueSet.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchParameterAndValueSetRuleImpl.java rename {hapi-fhir-structures-r4 => hapi-fhir-validation}/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java (91%) diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index e4b05ad0de4..10b2e10688a 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 1455df2cf08..08b43342eaa 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 71be13fb650..98674c0b2b0 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml 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 fe784c58820..055a35f58dc 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 @@ -246,7 +246,7 @@ public interface IValidationSupport { * @return Returns a validation result object */ @Nullable - default CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { + default CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { return null; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java index 80283e37b3d..1ac0a704190 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/Msg.java @@ -25,7 +25,7 @@ public final class Msg { /** * IMPORTANT: Please update the following comment after you add a new code - * Last code value: 2024 + * Last code value: 2031 */ private Msg() {} 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 7269495194e..cf0ffaaeda6 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 @@ -218,6 +218,7 @@ public class Constants { public static final String PARAMQUALIFIER_TOKEN_TEXT = ":text"; public static final String PARAMQUALIFIER_MDM = ":mdm"; public static final String PARAMQUALIFIER_TOKEN_OF_TYPE = ":of-type"; + public static final String PARAMQUALIFIER_TOKEN_NOT = ":not"; public static final int STATUS_HTTP_200_OK = 200; public static final int STATUS_HTTP_201_CREATED = 201; public static final int STATUS_HTTP_204_NO_CONTENT = 204; @@ -291,6 +292,10 @@ public class Constants { public static final String SUBSCRIPTION_MULTITYPE_STAR = "*"; public static final String SUBSCRIPTION_STAR_CRITERIA = SUBSCRIPTION_MULTITYPE_PREFIX + SUBSCRIPTION_MULTITYPE_STAR + SUBSCRIPTION_MULTITYPE_SUFFIX; public static final String INCLUDE_STAR = "*"; + public static final String PARAMQUALIFIER_TOKEN_IN = ":in"; + public static final String PARAMQUALIFIER_TOKEN_NOT_IN = ":not-in"; + public static final String PARAMQUALIFIER_TOKEN_ABOVE = ":above"; + public static final String PARAMQUALIFIER_TOKEN_BELOW = ":below"; static { CHARSET_UTF8 = StandardCharsets.UTF_8; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java index 769db1ab355..0c90a2e7d92 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java @@ -346,10 +346,6 @@ public class UrlUtil { String url = theUrl; UrlParts retVal = new UrlParts(); if (url.startsWith("http")) { - if (url.startsWith("/")) { - url = url.substring(1); - } - int qmIdx = url.indexOf('?'); if (qmIdx != -1) { retVal.setParams(defaultIfBlank(url.substring(qmIdx + 1), null)); @@ -372,10 +368,7 @@ public class UrlUtil { } } - if (url.length() > 1 && url.charAt(0) == '/' && Character.isLetter(url.charAt(1)) && url.contains("?")) { - url = url.substring(1); - } - int nextStart = 0; + int nextStart = parsingStart; boolean nextIsHistory = false; for (int idx = parsingStart; idx < url.length(); idx++) { diff --git a/hapi-fhir-batch/pom.xml b/hapi-fhir-batch/pom.xml index 04229bccdac..ce02f2f33c0 100644 --- a/hapi-fhir-batch/pom.xml +++ b/hapi-fhir-batch/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index ff444f02701..f7569c889de 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -3,14 +3,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 8710451a5d4..06bac605854 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-checkstyle/src/main/java/ca/uhn/fhir/checks/HapiErrorCodeCheck.java b/hapi-fhir-checkstyle/src/main/java/ca/uhn/fhir/checks/HapiErrorCodeCheck.java index 1b1eea27f6f..8494b29e4ff 100644 --- a/hapi-fhir-checkstyle/src/main/java/ca/uhn/fhir/checks/HapiErrorCodeCheck.java +++ b/hapi-fhir-checkstyle/src/main/java/ca/uhn/fhir/checks/HapiErrorCodeCheck.java @@ -7,8 +7,8 @@ import com.puppycrawl.tools.checkstyle.api.TokenTypes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; /** * mvn -P CI,ALLMODULES checkstyle:check @@ -17,7 +17,7 @@ import java.util.Set; public final class HapiErrorCodeCheck extends AbstractCheck { private static final Logger ourLog = LoggerFactory.getLogger(HapiErrorCodeCheck.class); - private static final Set ourCodesUsed = new HashSet<>(); + private static final Map ourCodesUsed = new HashMap<>(); @Override public int[] getDefaultTokens() { @@ -68,10 +68,14 @@ public final class HapiErrorCodeCheck extends AbstractCheck { DetailAST numberNode = msgNode.getParent().getNextSibling().getFirstChild().getFirstChild(); if (TokenTypes.NUM_INT == numberNode.getType()) { Integer code = Integer.valueOf(numberNode.getText()); - if (!ourCodesUsed.add(code)) { + if (ourCodesUsed.containsKey(code)) { log(theAst.getLineNo(), "Two different exception messages call Msg.code(" + code + - "). Each thrown exception throw call Msg.code() with a different code."); + "). Each thrown exception throw call Msg.code() with a different code. " + + "Previously found at: " + ourCodesUsed.get(code)); + } else { + String location = getFileContents().getFileName() + ":" + instantiation.getLineNo() + ":" + instantiation.getColumnNo() + "(" + code + ")"; + ourCodesUsed.put(code, location); } } else { log(theAst.getLineNo(), "Called Msg.code() with a non-integer argument"); diff --git a/hapi-fhir-checkstyle/src/test/java/ca/uhn/fhir/checks/HapiErrorCodeCheckTest.java b/hapi-fhir-checkstyle/src/test/java/ca/uhn/fhir/checks/HapiErrorCodeCheckTest.java index 4d04204e215..662662adb7d 100644 --- a/hapi-fhir-checkstyle/src/test/java/ca/uhn/fhir/checks/HapiErrorCodeCheckTest.java +++ b/hapi-fhir-checkstyle/src/test/java/ca/uhn/fhir/checks/HapiErrorCodeCheckTest.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -47,7 +48,8 @@ class HapiErrorCodeCheckTest { assertThat(errorLines[0], startsWith("[ERROR] ")); assertThat(errorLines[0], endsWith("BadClass.java:7: Exception thrown that does not call Msg.code() [HapiErrorCode]")); assertThat(errorLines[1], startsWith("[ERROR] ")); - assertThat(errorLines[1], endsWith("BadClass.java:11: Two different exception messages call Msg.code(2). Each thrown exception throw call Msg.code() with a different code. [HapiErrorCode]")); + assertThat(errorLines[1], containsString("BadClass.java:11: Two different exception messages call Msg.code(2). Each thrown exception throw call Msg.code() with a different code.")); + assertThat(errorLines[1], containsString("BadClass.java:9:9")); } private Checker buildChecker() throws CheckstyleException { diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 1dfdec51906..d940421ee50 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 f576309d124..0494f01f6cd 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 557c8280db7..f2399ca4eb9 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 9c2507de9d2..4fb978dac34 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index f9d1d8d94bb..356cbc9377a 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index f47bb9076ec..798ff2a4cc3 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 4cb80cc560b..e8616b0bb80 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 2b671609c0a..c5b94bf3ba0 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index a11c07f1f3e..fdf28fb715c 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java index 01f160b5138..bba87506250 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java @@ -255,7 +255,7 @@ public class AuthorizationInterceptors { } else { - throw new AuthenticationException(Msg.code(645) + "Unknown bearer token"); + throw new AuthenticationException("Unknown bearer token"); } @@ -265,4 +265,38 @@ public class AuthorizationInterceptors { //END SNIPPET: narrowing + //START SNIPPET: narrowingByCode + public class MyCodeSearchNarrowingInterceptor extends SearchNarrowingInterceptor { + + /** + * This method must be overridden to provide the list of compartments + * and/or resources that the current user should have access to + */ + @Override + protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) { + // Process authorization header - The following is a fake + // implementation. Obviously we'd want something more real + // for a production scenario. + String authHeader = theRequestDetails.getHeader("Authorization"); + if ("Bearer dfw98h38r".equals(authHeader)) { + + return new AuthorizedList() + // When searching for Observations, narrow the search to only include Observations + // with a code indicating that it is a Vital Signs Observation + .addCodeInValueSet("Observation", "code", "http://hl7.org/fhir/ValueSet/observation-vitalsignresult") + // When searching for Encounters, narrow the search to exclude Encounters where + // the Encounter class is in a ValueSet containing forbidden class codes + .addCodeNotInValueSet("Encounter", "class", "http://my-forbidden-encounter-classes"); + + } else { + + throw new AuthenticationException("Unknown bearer token"); + + } + + } + + } + //END SNIPPET: narrowingByCode + } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-not-in-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-not-in-search.yaml new file mode 100644 index 00000000000..ccc511fe5f7 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-not-in-search.yaml @@ -0,0 +1,6 @@ +--- +type: add +issue: 3360 +title: "Support has been added to the JPA server for token `:not-in` queries. Similar to `:not` queries, resources will + currently be considered to match if any codes in the relevant resource field are not found in the given ValueSet (as + opposed to matching if *all* codes are not in the given ValueSet)." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-authorization-interceptor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-authorization-interceptor.yaml new file mode 100644 index 00000000000..13f8cc6c9dc --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-authorization-interceptor.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 3360 +title: "SearchNarrowingInterceptor can now be used to automatically narrow searches to include a `code:in` or `code:not-in` + expression, for mandating that results must be in a specified list of codes." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-search-narrowing-interceptor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-search-narrowing-interceptor.yaml new file mode 100644 index 00000000000..254170b634a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-add-support-for-vs-membership-in-search-narrowing-interceptor.yaml @@ -0,0 +1,4 @@ +--- +type: add +issue: 3360 +title: "The SearchNarrowingInterceptor can now narrow searches to require a `token:in` or `token:not-in` parameter." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-improve-vs-expand-performance.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-improve-vs-expand-performance.yaml new file mode 100644 index 00000000000..30da67b0abb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_0_0/3360-improve-vs-expand-performance.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 3360 +title: "Performance for JPA Server ValueSet expansion has been significantly optimized + in order to minimize database lookups, especially with large expansions." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/security/search_narrowing_interceptor.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/security/search_narrowing_interceptor.md index e866c5ff510..b1a99e63ab6 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/security/search_narrowing_interceptor.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/security/search_narrowing_interceptor.md @@ -1,11 +1,11 @@ # Search Narrowing Interceptor -HAPI FHIR 3.7.0 introduced a new interceptor, the [SearchNarrowingInterceptor](/hapi-fhir/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.html). +The [SearchNarrowingInterceptor](/hapi-fhir/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.html) can be used to automatically narrow or constrain the scope of FHIR searches. * [SearchNarrowingInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.html) * [SearchNarrowingInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java) -This interceptor is designed to be used in conjunction with AuthorizationInterceptor. It uses a similar strategy where a dynamic list is built up for each request, but the purpose of this interceptor is to modify client searches that are received (after HAPI FHIR received the HTTP request, but before the search is actually performed) to restrict the search to only search for specific resources or compartments that the user has access to. +This interceptor is designed to be used in conjunction with the [Authorization Interceptor](./authorization_interceptor.html). It uses a similar strategy where a dynamic list is built up for each request, but the purpose of this interceptor is to modify client searches that are received (after HAPI FHIR receives the HTTP request, but before the search is actually performed) to restrict the search to only search for specific resources or compartments that the user has access to. This could be used, for example, to allow the user to perform a search for: @@ -25,3 +25,15 @@ An example of this interceptor follows: {{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowing}} ``` +# Constraining by ValueSet Membership + +SearchNarrowingInterceptor can also be used to narrow searches by automatically appending `token:in` and `token:not-in` parameters. + +In the example below, searches are narrowed as shown below: + +* Searches for http://localhost:8000/Observation become http://localhost:8000/Observation?code:in=http://hl7.org/fhir/ValueSet/observation-vitalsignresult +* Searches for http://localhost:8000/Encounter become http://localhost:8000/Encounter?class:not-in=http://my-forbidden-encounter-classes + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowingByCode}} +``` diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 7de8b2c280e..d65f2b53137 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index a28ac8ceb94..d0183fbab68 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index 680f9ee9513..dc347c9e49e 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index aa72dad3168..2c9568fc46e 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 0e305b92d31..006199603f5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -16,7 +16,6 @@ import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; import ca.uhn.fhir.jpa.batch.config.BatchConstants; import ca.uhn.fhir.jpa.batch.config.NonPersistedBatchConfigurer; import ca.uhn.fhir.jpa.batch.job.PartitionedUrlValidator; -import ca.uhn.fhir.jpa.batch.mdm.MdmBatchJobSubmitterFactoryImpl; import ca.uhn.fhir.jpa.batch.mdm.MdmClearJobSubmitterImpl; import ca.uhn.fhir.jpa.batch.reader.BatchResourceSearcher; import ca.uhn.fhir.jpa.batch.svc.BatchJobSubmitterImpl; @@ -68,7 +67,6 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; -import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor; import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor; import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; @@ -140,7 +138,6 @@ import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.jpa.validation.JpaResourceLoader; import ca.uhn.fhir.jpa.validation.ValidationSettings; -import ca.uhn.fhir.mdm.api.IMdmBatchJobSubmitterFactory; import ca.uhn.fhir.mdm.api.IMdmClearJobSubmitter; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter; @@ -155,7 +152,6 @@ import org.hibernate.jpa.HibernatePersistenceProvider; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; -import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.PackageClient; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.BatchConfigurer; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java index b0bf040207a..ae97aa81f11 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfigDstu3Plus.java @@ -84,7 +84,6 @@ public abstract class BaseConfigDstu3Plus extends BaseConfig { @Primary @Bean public IValidationSupport validationSupportChain() { - // Short timeout for code translation because TermConceptMappingSvcImpl has its own caching CachingValidationSupport.CacheTimeouts cacheTimeouts = CachingValidationSupport.CacheTimeouts.defaultValues() .setTranslateCodeMillis(1000); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java index c0fd3f591c7..b6b07b5c1a0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java @@ -52,8 +52,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement public class BaseR4Config extends BaseConfigDstu3Plus { - public static FhirContext ourFhirContext = FhirContext.forR4(); - @Override public FhirContext fhirContext() { return fhirContextR4(); @@ -68,7 +66,7 @@ public class BaseR4Config extends BaseConfigDstu3Plus { @Bean @Primary public FhirContext fhirContextR4() { - FhirContext retVal = ourFhirContext; + FhirContext retVal = FhirContext.forR4(); // Don't strip versions in some places ParserOptions parserOptions = retVal.getParserOptions(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java index aa2e6584ec5..bedfe146ae1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ITermConceptDao.java @@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -34,6 +35,16 @@ import java.util.Optional; public interface ITermConceptDao extends JpaRepository, IHapiFhirJpaRepository { + @Query("SELECT t FROM TermConcept t " + + "LEFT JOIN FETCH t.myDesignations d " + + "WHERE t.myId IN :pids") + List fetchConceptsAndDesignationsByPid(@Param("pids") List thePids); + + @Query("SELECT t FROM TermConcept t " + + "LEFT JOIN FETCH t.myDesignations d " + + "WHERE t.myCodeSystemVersionPid = :pid") + List fetchConceptsAndDesignationsByVersionPid(@Param("pid") Long theCodeSystemVersionPid); + @Query("SELECT COUNT(t) FROM TermConcept t WHERE t.myCodeSystem.myId = :cs_pid") Integer countByCodeSystemVersion(@Param("cs_pid") Long thePid); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java index 63b0eeb083b..b34f788a4dd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptDesignation.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.entity; */ import ca.uhn.fhir.util.ValidateUtil; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import javax.annotation.Nonnull; import javax.persistence.Column; @@ -146,4 +148,17 @@ public class TermConceptDesignation implements Serializable { public Long getPid() { return myId; } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("conceptPid", myConcept.getId()) + .append("pid", myId) + .append("language", myLanguage) + .append("useSystem", myUseSystem) + .append("useCode", myUseCode) + .append("useDisplay", myUseDisplay) + .append("value", myValue) + .toString(); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java index a73ac120437..5d9b79cd470 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermConceptProperty.java @@ -218,6 +218,7 @@ public class TermConceptProperty implements Serializable { @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("conceptPid", myConcept.getId()) .append("key", myKey) .append("value", getValue()) .toString(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java index a2d4c6951a8..8329e2d6554 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/TermValueSetConcept.java @@ -238,16 +238,17 @@ public class TermValueSetConcept implements Serializable { @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("myId", myId) - .append(myValueSet != null ? ("myValueSet - id=" + myValueSet.getId()) : ("myValueSet=(null)")) - .append("myValueSetPid", myValueSetPid) - .append("myOrder", myOrder) - .append("myValueSetUrl", this.getValueSetUrl()) - .append("myValueSetName", this.getValueSetName()) - .append("mySystem", mySystem) - .append("myCode", myCode) - .append("myDisplay", myDisplay) - .append(myDesignations != null ? ("myDesignations - size=" + myDesignations.size()) : ("myDesignations=(null)")) + .append("id", myId) + .append("order", myOrder) + .append("system", mySystem) + .append("code", myCode) + .append("valueSet", myValueSet != null ? myValueSet.getId() : "(null)") + .append("valueSetPid", myValueSetPid) + .append("valueSetUrl", this.getValueSetUrl()) + .append("valueSetName", this.getValueSetName()) + .append("display", myDisplay) + .append("designationCount", myDesignations != null ? myDesignations.size() : "(null)") + .append("parentPids", mySourceConceptDirectParentPids) .toString(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 3d7ddd25c6c..b93fc9c48b6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -483,7 +483,7 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac byte[] bytes = Files.readAllBytes(Paths.get(new URI(thePackageUrl))); return bytes; } catch (IOException | URISyntaxException e) { - throw new InternalErrorException(Msg.code(2024) + "Error loading \"" + thePackageUrl + "\": " + e.getMessage()); + throw new InternalErrorException(Msg.code(2031) + "Error loading \"" + thePackageUrl + "\": " + e.getMessage()); } } else { HttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java index b33c023c6dd..881e2115e65 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ValueSetOperationProvider.java @@ -20,11 +20,11 @@ package ca.uhn.fhir.jpa.provider; * #L% */ -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; @@ -37,9 +37,13 @@ import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.util.ParametersUtil; +import ca.uhn.fhir.util.UrlUtil; import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -68,8 +72,14 @@ public class ValueSetOperationProvider extends BaseJpaProvider { @Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT_CHAIN) private ValidationSupportChain myValidationSupportChain; @Autowired + private IValidationSupport myValidationSupport; + @Autowired private IFulltextSearchSvc myFulltextSearch; + public void setValidationSupport(IValidationSupport theValidationSupport) { + myValidationSupport = theValidationSupport; + } + public void setDaoConfig(DaoConfig theDaoConfig) { myDaoConfig = theDaoConfig; } @@ -136,17 +146,34 @@ public class ValueSetOperationProvider extends BaseJpaProvider { try { IFhirResourceDaoValueSet dao = getDao(); + + IBaseResource valueSet = theValueSet; if (haveId) { - return dao.expand(theId, options, theRequestDetails); + valueSet = dao.read(theId, theRequestDetails); } else if (haveIdentifier) { + String url; if (haveValueSetVersion) { - return dao.expandByIdentifier(theUrl.getValue() + "|" + theValueSetVersion.getValue(), options); + url = theUrl.getValue() + "|" + theValueSetVersion.getValue(); + valueSet = myValidationSupport.fetchValueSet(url); } else { - return dao.expandByIdentifier(theUrl.getValue(), options); + url = theUrl.getValue(); + valueSet = myValidationSupport.fetchValueSet(url); + } + if (valueSet == null) { + throw new ResourceNotFoundException(Msg.code(2030) + "Can not find ValueSet with URL: " + UrlUtil.escapeUrlParam(url)); } - } else { - return dao.expand(theValueSet, options); } + + IValidationSupport.ValueSetExpansionOutcome outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, valueSet); + if (outcome == null) { + throw new InternalErrorException(Msg.code(2028) + outcome.getError()); + } + if (outcome.getError() != null) { + throw new PreconditionFailedException(Msg.code(2029) + outcome.getError()); + } + + return outcome.getValueSet(); + } finally { endRequest(theServletRequest); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteAggregation.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteAggregation.java index 800e762e392..ff591c0f1a7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteAggregation.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteAggregation.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search.autocomplete; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneClauseBuilder; import com.google.gson.Gson; import com.google.gson.JsonArray; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteHit.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteHit.java index 0de0ec7fc91..2a9e980219f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteHit.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteHit.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search.autocomplete; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.util.TerserUtil; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteSearch.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteSearch.java index 6290b64ce50..606d756ff9b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteSearch.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/TokenAutocompleteSearch.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search.autocomplete; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneClauseBuilder; @@ -80,7 +100,7 @@ class TokenAutocompleteSearch { break; case "": default: - throw new IllegalArgumentException(Msg.code(2023) + "Autocomplete only accepts text search for now."); + throw new IllegalArgumentException(Msg.code(2027) + "Autocomplete only accepts text search for now."); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteOptions.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteOptions.java index 74684843f10..320e47481eb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteOptions.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteOptions.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search.autocomplete; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteSearch.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteSearch.java index b8159892d7a..278ca7e92da 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteSearch.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/ValueSetAutocompleteSearch.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.search.autocomplete; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.TokenParam; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/package-info.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/package-info.java index 431fa3ba3d4..338ceba1708 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/package-info.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/autocomplete/package-info.java @@ -15,3 +15,23 @@ * */ package ca.uhn.fhir.jpa.search.autocomplete; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2022 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% + */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java index 8d13ad4996e..5f3e7aed9ee 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/TokenPredicateBuilder.java @@ -20,11 +20,17 @@ package ca.uhn.fhir.jpa.search.builder.predicate; * #L% */ -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser; @@ -43,20 +49,23 @@ import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParamModifier; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.FhirVersionIndependentConcept; +import ca.uhn.fhir.util.UrlUtil; import com.google.common.collect.Sets; import com.healthmarketscience.sqlbuilder.BinaryCondition; import com.healthmarketscience.sqlbuilder.Condition; -import com.healthmarketscience.sqlbuilder.InCondition; import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn; -import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -76,10 +85,14 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { private final DbColumn myColumnSystem; private final DbColumn myColumnValue; + @Autowired + private IValidationSupport myValidationSupport; @Autowired private ITermReadSvc myTerminologySvc; @Autowired private ModelConfig myModelConfig; + @Autowired + private FhirContext myContext; /** * Constructor @@ -119,10 +132,10 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { RuntimeSearchParam theSearchParam, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) { - - + + final List codes = new ArrayList<>(); - + String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName()); SearchFilterParser.CompareOperation operation = theOperation; @@ -165,8 +178,20 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { * Process token modifiers (:in, :below, :above) */ - if (modifier == TokenParamModifier.IN) { - codes.addAll(myTerminologySvc.expandValueSetIntoConceptList(null, code)); + if (modifier == TokenParamModifier.IN || modifier == TokenParamModifier.NOT_IN) { + if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) { + IBaseResource valueSet = myValidationSupport.fetchValueSet(code); + if (valueSet == null) { + throw new ResourceNotFoundException(Msg.code(2024) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(code)); + } + IValidationSupport.ValueSetExpansionOutcome expanded = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet); + codes.addAll(extractValueSetCodes(expanded.getValueSet())); + } else { + codes.addAll(myTerminologySvc.expandValueSetIntoConceptList(null, code)); + } + if (modifier == TokenParamModifier.NOT_IN) { + operation = SearchFilterParser.CompareOperation.ne; + } } else if (modifier == TokenParamModifier.ABOVE) { system = determineSystemIfMissing(theSearchParam, code, system); validateHaveSystemAndCodeForToken(paramName, code, system); @@ -235,6 +260,46 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder { return predicate; } + private List extractValueSetCodes(IBaseResource theValueSet) { + List retVal = new ArrayList<>(); + + RuntimeResourceDefinition vsDef = myContext.getResourceDefinition("ValueSet"); + BaseRuntimeChildDefinition expansionChild = vsDef.getChildByName("expansion"); + Optional expansionOpt = expansionChild.getAccessor().getFirstValueOrNull(theValueSet); + if (expansionOpt.isPresent()) { + IBase expansion = expansionOpt.get(); + BaseRuntimeElementCompositeDefinition expansionDef = (BaseRuntimeElementCompositeDefinition) myContext.getElementDefinition(expansion.getClass()); + BaseRuntimeChildDefinition containsChild = expansionDef.getChildByName("contains"); + List contains = containsChild.getAccessor().getValues(expansion); + + BaseRuntimeChildDefinition.IAccessor systemAccessor = null; + BaseRuntimeChildDefinition.IAccessor codeAccessor = null; + for (IBase nextContains : contains) { + if (systemAccessor == null) { + systemAccessor = myContext.getElementDefinition(nextContains.getClass()).getChildByName("system").getAccessor(); + } + if (codeAccessor == null) { + codeAccessor = myContext.getElementDefinition(nextContains.getClass()).getChildByName("code").getAccessor(); + } + String system = systemAccessor + .getFirstValueOrNull(nextContains) + .map(t->(IPrimitiveType)t) + .map(t->t.getValueAsString()) + .orElse(null); + String code = codeAccessor + .getFirstValueOrNull(nextContains) + .map(t->(IPrimitiveType)t) + .map(t->t.getValueAsString()) + .orElse(null); + if (isNotBlank(system) && isNotBlank(code)) { + retVal.add(new FhirVersionIndependentConcept(system, code)); + } + } + } + + return retVal; + } + private String determineSystemIfMissing(RuntimeSearchParam theSearchParam, String code, String theSystem) { String retVal = theSystem; if (retVal == null) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java index 560285fd967..6d5f5949bb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java @@ -20,12 +20,12 @@ package ca.uhn.fhir.jpa.term; * #L% */ -import ca.uhn.fhir.i18n.Msg; 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.ValidationSupportContext; import ca.uhn.fhir.context.support.ValueSetExpansionOptions; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IDao; @@ -100,6 +100,7 @@ import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep; import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; import org.hibernate.search.engine.search.query.SearchQuery; 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.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; @@ -157,7 +158,6 @@ import javax.persistence.criteria.Join; import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; -import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -197,6 +197,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { private static final ValueSetExpansionOptions DEFAULT_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); private static final TermCodeSystemVersion NO_CURRENT_VERSION = new TermCodeSystemVersion().setId(-1L); private static Runnable myInvokeOnNextCallForUnitTest; + private static boolean ourForceDisableHibernateSearchForUnitTest; + private final Cache myCodeSystemCurrentVersionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(); @Autowired protected DaoRegistry myDaoRegistry; @@ -230,6 +232,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { @Autowired private PlatformTransactionManager myTxManager; @Autowired + private ITermConceptDao myTermConceptDao; + @Autowired private ITermValueSetConceptViewDao myTermValueSetConceptViewDao; @Autowired private ITermValueSetConceptViewOracleDao myTermValueSetConceptViewOracleDao; @@ -261,18 +265,21 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return cs != null; } - private boolean addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, TermConcept theConcept, boolean theAdd, String theValueSetIncludeVersion) { + private boolean addCodeIfNotAlreadyAdded(@Nullable ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, TermConcept theConcept, boolean theAdd, String theValueSetIncludeVersion) { String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri(); String codeSystemVersion = theConcept.getCodeSystemVersion().getCodeSystemVersionId(); String code = theConcept.getCode(); String display = theConcept.getDisplay(); Long sourceConceptPid = theConcept.getId(); + String directParentPids = ""; - String directParentPids = theConcept - .getParents() - .stream() - .map(t -> t.getParent().getId().toString()) - .collect(Collectors.joining(" ")); + if (theExpansionOptions != null && theExpansionOptions.isIncludeHierarchy()) { + directParentPids = theConcept + .getParents() + .stream() + .map(t -> t.getParent().getId().toString()) + .collect(Collectors.joining(" ")); + } Collection designations = theConcept.getDesignations(); if (StringUtils.isNotEmpty(theValueSetIncludeVersion)) { @@ -282,11 +289,11 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } - private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, boolean theAdd, String theCodeSystem, String theCodeSystemVersion, String theCode, String theDisplay, Long theSourceConceptPid, String theSourceConceptDirectParentPids) { + private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, boolean theAdd, String theCodeSystem, String theCodeSystemVersion, String theCode, String theDisplay, Long theSourceConceptPid, String theSourceConceptDirectParentPids, Collection theDesignations) { if (StringUtils.isNotEmpty(theCodeSystemVersion)) { if (isNoneBlank(theCodeSystem, theCode)) { if (theAdd && theAddedCodes.add(theCodeSystem + "|" + theCode)) { - theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem + "|" + theCodeSystemVersion, theCode, theDisplay, null, theSourceConceptPid, theSourceConceptDirectParentPids, theCodeSystemVersion); + theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem + "|" + theCodeSystemVersion, theCode, theDisplay, theDesignations, theSourceConceptPid, theSourceConceptDirectParentPids, theCodeSystemVersion); } if (!theAdd && theAddedCodes.remove(theCodeSystem + "|" + theCode)) { @@ -295,7 +302,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } else { if (theAdd && theAddedCodes.add(theCodeSystem + "|" + theCode)) { - theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem, theCode, theDisplay, null, theSourceConceptPid, theSourceConceptDirectParentPids, theCodeSystemVersion); + theValueSetCodeAccumulator.includeConceptWithDesignations(theCodeSystem, theCode, theDisplay, theDesignations, theSourceConceptPid, theSourceConceptDirectParentPids, theCodeSystemVersion); } if (!theAdd && theAddedCodes.remove(theCodeSystem + "|" + theCode)) { @@ -560,8 +567,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { String systemVersion = conceptView.getConceptSystemVersion(); //-- this is quick solution, may need to revisit - if (!applyFilter(display, filterDisplayValue)) - continue; + if (!applyFilter(display, filterDisplayValue)) { + continue;} Long conceptPid = conceptView.getConceptPid(); if (!pidToConcept.containsKey(conceptPid)) { @@ -793,7 +800,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(system); if (cs != null) { - return expandValueSetHandleIncludeOrExcludeUsingDatabase(theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theQueryIndex, theExpansionFilter, system, cs); + return expandValueSetHandleIncludeOrExcludeUsingDatabase(theExpansionOptions, theValueSetCodeAccumulator, theAddedCodes, theIncludeOrExclude, theAdd, theQueryIndex, theExpansionFilter, system, cs); } else { @@ -854,11 +861,11 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } private boolean isHibernateSearchEnabled() { - return myFulltextSearchSvc != null; + return myFulltextSearchSvc != null && !ourForceDisableHibernateSearchForUnitTest; } @Nonnull - private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, int theQueryIndex, @Nonnull ExpansionFilter theExpansionFilter, String theSystem, TermCodeSystem theCs) { + private Boolean expandValueSetHandleIncludeOrExcludeUsingDatabase(ValueSetExpansionOptions theExpansionOptions, IValueSetConceptAccumulator theValueSetCodeAccumulator, Set theAddedCodes, ValueSet.ConceptSetComponent theIncludeOrExclude, boolean theAdd, int theQueryIndex, @Nonnull ExpansionFilter theExpansionFilter, String theSystem, TermCodeSystem theCs) { String includeOrExcludeVersion = theIncludeOrExclude.getVersion(); TermCodeSystemVersion csv; if (isEmpty(includeOrExcludeVersion)) { @@ -948,11 +955,20 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { StopWatch swForBatch = new StopWatch(); AtomicInteger countForBatch = new AtomicInteger(0); - SearchQuery termConceptsQuery = searchSession.search(TermConcept.class) - .where(f -> finishedQuery).toQuery(); + SearchQuery termConceptsQuery = searchSession + .search(TermConcept.class) + .selectEntityReference() + .where(f -> finishedQuery) + .toQuery(); ourLog.trace("About to query: {}", termConceptsQuery.queryString()); - List termConcepts = termConceptsQuery.fetchHits(theQueryIndex * maxResultsPerBatch, maxResultsPerBatch); + List termConceptRefs = termConceptsQuery.fetchHits(theQueryIndex * maxResultsPerBatch, maxResultsPerBatch); + List pids = termConceptRefs + .stream() + .map(t -> (Long) t.id()) + .collect(Collectors.toList()); + + List termConcepts = myTermConceptDao.fetchConceptsAndDesignationsByPid(pids); // If the include section had multiple codes, return the codes in the same order if (codes.size() > 1) { @@ -980,7 +996,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { concept.setDisplay(theIncludeConcept.getDisplay()); } } - boolean added = addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, includeOrExcludeVersion); + boolean added = addCodeIfNotAlreadyAdded(theExpansionOptions, theValueSetCodeAccumulator, theAddedCodes, concept, theAdd, includeOrExcludeVersion); if (added) { delta++; } @@ -1259,7 +1275,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } - private void isCodeSystemLoincOrThrowInvalidRequestException(String theSystemIdentifier, String theProperty) { String systemUrl = getUrlFromIdentifier(theSystemIdentifier); if (!isCodeSystemLoinc(systemUrl)) { @@ -1287,7 +1302,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { bool.must(f.phrase().field("myDisplay").matching(nextFilter.getValue())); } - private void addDisplayFilterInexact(SearchPredicateFactory f, BooleanPredicateClausesStep bool, ValueSet.ConceptSetFilterComponent nextFilter) { bool.must(f.phrase() .field("myDisplay").boost(4.0f) @@ -1328,7 +1342,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { } } - private void addLoincFilterDescendantEqual(String theSystem, SearchPredicateFactory f, BooleanPredicateClausesStep b, ValueSet.ConceptSetFilterComponent theFilter) { addLoincFilterDescendantEqual(theSystem, f, b, theFilter.getProperty(), theFilter.getValue()); } @@ -1358,7 +1371,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return theTerms.stream().map(Term::text).map(Long::valueOf).collect(Collectors.toList()); } - private List getDescendantTerms(String theSystem, String theProperty, String theValue) { List retVal = new ArrayList<>(); @@ -1376,7 +1388,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return retVal; } - private void logFilteringValueOnProperty(String theValue, String theProperty) { ourLog.debug(" * Filtering with value={} on property {}", theValue, theProperty); } @@ -1400,8 +1411,10 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { Validate.isTrue(isNotBlank(theSystem), "Can not expand ValueSet without explicit system - Hibernate Search is not enabled on this server."); if (theInclude.getConcept().isEmpty()) { - for (TermConcept next : theVersion.getConcepts()) { - addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay(), next.getId(), next.getParentPidsAsString()); + + Collection concepts = myConceptDao.fetchConceptsAndDesignationsByVersionPid(theVersion.getPid()); + for (TermConcept next : concepts) { + addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay(), next.getId(), next.getParentPidsAsString(), next.getDesignations()); } } @@ -1409,7 +1422,18 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { if (!theSystem.equals(theInclude.getSystem()) && isNotBlank(theSystem)) { continue; } - addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay(), null, null); + Collection designations = next + .getDesignation() + .stream() + .map(t->new TermConceptDesignation() + .setValue(t.getValue()) + .setLanguage(t.getLanguage()) + .setUseCode(t.getUse().getCode()) + .setUseSystem(t.getUse().getSystem()) + .setUseDisplay(t.getUse().getDisplay()) + ) + .collect(Collectors.toList()); + addCodeIfNotAlreadyAdded(theValueSetCodeAccumulator, theAddedCodes, theAdd, theSystem, theInclude.getVersion(), next.getCode(), next.getDisplay(), null, null, designations); } @@ -1442,7 +1466,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "valueSetPreExpansionInvalidated", termValueSet.getUrl(), totalConcepts); } - @Override @Transactional public boolean isValueSetPreExpandedForCodeValidation(ValueSet theValueSet) { @@ -1744,7 +1767,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { mySchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_MINUTE, vsJobDefinition); } - @Override public synchronized void preExpandDeferredValueSetsToTerminologyTables() { if (!myDaoConfig.isEnableTaskPreExpandValueSets()) { @@ -1781,7 +1803,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { assert valueSet != null; ValueSetConceptAccumulator accumulator = new ValueSetConceptAccumulator(valueSetToExpand, myTermValueSetDao, myValueSetConceptDao, myValueSetConceptDesignationDao); - expandValueSet(null, valueSet, accumulator); + ValueSetExpansionOptions options = new ValueSetExpansionOptions(); + options.setIncludeHierarchy(true); + expandValueSet(options, valueSet, accumulator); // We are done with this ValueSet. txTemplate.execute(t -> { @@ -2045,7 +2069,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { }); } - @Nullable private ConceptSubsumptionOutcome testForSubsumption(SearchSession theSearchSession, TermConcept theLeft, TermConcept theRight, ConceptSubsumptionOutcome theOutput) { List fetch = theSearchSession.search(TermConcept.class) @@ -2346,7 +2369,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return codeSystemValidateCode(codeSystemUrl, theVersion, code, display); } - /** * When the search is for unversioned loinc system it uses the forcedId to obtain the current * version, as it is not necessarily the last one anymore. @@ -2356,7 +2378,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { public Optional findCurrentTermValueSet(String theUrl) { if (TermReadSvcUtil.isLoincUnversionedValueSet(theUrl)) { Optional vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl); - if (! vsIdOpt.isPresent()) { + if (!vsIdOpt.isPresent()) { return Optional.empty(); } @@ -2371,7 +2393,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return Optional.of(termValueSetList.get(0)); } - @SuppressWarnings("unchecked") private CodeValidationResult codeSystemValidateCode(String theCodeSystemUrl, String theCodeSystemVersion, String theCode, String theDisplay) { @@ -2436,7 +2457,8 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { "where f.myResourceType = 'CodeSystem' and f.myForcedId = '" + theForcedId + "'").getResultList(); if (resultList.isEmpty()) return Optional.empty(); - if (resultList.size() > 1) throw new NonUniqueResultException(Msg.code(911) + "More than one CodeSystem is pointed by forcedId: " + theForcedId + ". Was constraint " + if (resultList.size() > 1) + throw new NonUniqueResultException(Msg.code(911) + "More than one CodeSystem is pointed by forcedId: " + theForcedId + ". Was constraint " + ForcedId.IDX_FORCEDID_TYPE_FID + " removed?"); IFhirResourceDao csDao = myDaoRegistry.getResourceDao("CodeSystem"); @@ -2444,14 +2466,9 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return Optional.of(cs); } - public static class Job implements HapiJob { - @Autowired - private ITermReadSvc myTerminologySvc; - - @Override - public void execute(JobExecutionContext theContext) { - myTerminologySvc.preExpandDeferredValueSetsToTerminologyTables(); - } + @VisibleForTesting + public static void setForceDisableHibernateSearchForUnitTest(boolean theForceDisableHibernateSearchForUnitTest) { + ourForceDisableHibernateSearchForUnitTest = theForceDisableHibernateSearchForUnitTest; } static boolean isPlaceholder(DomainResource theResource) { @@ -2540,12 +2557,22 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { return termConcept; } - static boolean isDisplayLanguageMatch(String theReqLang, String theStoredLang) { + static boolean isDisplayLanguageMatch(String theReqLang, String theStoredLang) { // NOTE: return the designation when one of then is not specified. if (theReqLang == null || theStoredLang == null) return true; return theReqLang.equalsIgnoreCase(theStoredLang); - } + } + + public static class Job implements HapiJob { + @Autowired + private ITermReadSvc myTerminologySvc; + + @Override + public void execute(JobExecutionContext theContext) { + myTerminologySvc.preExpandDeferredValueSetsToTerminologyTables(); + } + } } 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 422f8fb6d26..af776a697ff 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 @@ -56,6 +56,9 @@ public class JpaValidationSupportChain extends ValidationSupportChain { @Autowired private UnknownCodeSystemWarningValidationSupport myUnknownCodeSystemWarningValidationSupport; + /** + * Constructor + */ public JpaValidationSupportChain(FhirContext theFhirContext) { myFhirContext = theFhirContext; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index d47c2a241b8..d1f67ab4d3f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -20,8 +20,14 @@ import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao; +import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao; +import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermConcept; +import ca.uhn.fhir.jpa.entity.TermConceptDesignation; +import ca.uhn.fhir.jpa.entity.TermConceptProperty; import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSetConcept; import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation; @@ -32,7 +38,6 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; -import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc; import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc; @@ -62,11 +67,7 @@ import org.apache.commons.io.IOUtils; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; -import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings; -import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings; -import org.hibernate.search.engine.cfg.BackendSettings; import org.hibernate.search.mapper.orm.Search; -import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; @@ -105,10 +106,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -179,6 +178,18 @@ public abstract class BaseJpaTest extends BaseTest { protected IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao; @Autowired protected IResourceIndexedComboTokensNonUniqueDao myResourceIndexedComboTokensNonUniqueDao; + @Autowired(required = false) + protected IFulltextSearchSvc myFulltestSearchSvc; + @Autowired(required = false) + protected BatchJobHelper myBatchJobHelper; + @Autowired + protected ITermConceptDao myTermConceptDao; + @Autowired + protected ITermValueSetConceptDao myTermValueSetConceptDao; + @Autowired + protected ITermConceptDesignationDao myTermConceptDesignationDao; + @Autowired + protected ITermConceptPropertyDao myTermConceptPropertyDao; @Autowired private IdHelperService myIdHelperService; @Autowired @@ -197,10 +208,6 @@ public abstract class BaseJpaTest extends BaseTest { @Autowired private IForcedIdDao myForcedIdDao; @Autowired(required = false) - protected IFulltextSearchSvc myFulltestSearchSvc; - @Autowired(required = false) - protected BatchJobHelper myBatchJobHelper; - @Autowired(required = false) private JobExecutionDao myMapJobExecutionDao; @Autowired(required = false) private JobInstanceDao myMapJobInstanceDao; @@ -308,33 +315,6 @@ public abstract class BaseJpaTest extends BaseTest { }); } - @SuppressWarnings("BusyWait") - public static void waitForSize(int theTarget, List theList) { - StopWatch sw = new StopWatch(); - while (theList.size() != theTarget && sw.getMillis() <= 16000) { - try { - Thread.sleep(50); - } catch (InterruptedException theE) { - throw new Error(theE); - } - } - if (sw.getMillis() >= 16000 || theList.size() > theTarget) { - String describeResults = theList - .stream() - .map(t -> { - if (t == null) { - return "null"; - } - if (t instanceof IBaseResource) { - return ((IBaseResource) t).getIdElement().getValue(); - } - return t.toString(); - }) - .collect(Collectors.joining(", ")); - fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + describeResults); - } - } - protected int logAllResources() { return runInTransaction(() -> { List resources = myResourceTableDao.findAll(); @@ -343,6 +323,38 @@ public abstract class BaseJpaTest extends BaseTest { }); } + protected int logAllConceptDesignations() { + return runInTransaction(() -> { + List resources = myTermConceptDesignationDao.findAll(); + ourLog.info("Concept Designations:\n * {}", resources.stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + return resources.size(); + }); + } + + protected int logAllConceptProperties() { + return runInTransaction(() -> { + List resources = myTermConceptPropertyDao.findAll(); + ourLog.info("Concept Designations:\n * {}", resources.stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + return resources.size(); + }); + } + + protected int logAllConcepts() { + return runInTransaction(() -> { + List resources = myTermConceptDao.findAll(); + ourLog.info("Concepts:\n * {}", resources.stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + return resources.size(); + }); + } + + protected int logAllValueSetConcepts() { + return runInTransaction(() -> { + List resources = myTermValueSetConceptDao.findAll(); + ourLog.info("Concepts:\n * {}", resources.stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + return resources.size(); + }); + } + protected int logAllForcedIds() { return runInTransaction(() -> { List forcedIds = myForcedIdDao.findAll(); @@ -375,7 +387,7 @@ public abstract class BaseJpaTest extends BaseTest { String message = myResourceIndexedSearchParamStringDao .findAll() .stream() - .filter(t->theParamNames.length == 0 ? true : Arrays.asList(theParamNames).contains(t.getParamName())) + .filter(t -> theParamNames.length == 0 ? true : Arrays.asList(theParamNames).contains(t.getParamName())) .map(t -> t.toString()) .collect(Collectors.joining("\n * ")); ourLog.info("String indexes{}:\n * {}", messageSuffix, message); @@ -649,6 +661,62 @@ public abstract class BaseJpaTest extends BaseTest { } } + protected TermValueSetConceptDesignation assertTermConceptContainsDesignation(TermValueSetConcept theConcept, String theLanguage, String theUseSystem, String theUseCode, String theUseDisplay, String theDesignationValue) { + Stream stream = theConcept.getDesignations().stream(); + if (theLanguage != null) { + stream = stream.filter(designation -> theLanguage.equalsIgnoreCase(designation.getLanguage())); + } + if (theUseSystem != null) { + stream = stream.filter(designation -> theUseSystem.equalsIgnoreCase(designation.getUseSystem())); + } + if (theUseCode != null) { + stream = stream.filter(designation -> theUseCode.equalsIgnoreCase(designation.getUseCode())); + } + if (theUseDisplay != null) { + stream = stream.filter(designation -> theUseDisplay.equalsIgnoreCase(designation.getUseDisplay())); + } + if (theDesignationValue != null) { + stream = stream.filter(designation -> theDesignationValue.equalsIgnoreCase(designation.getValue())); + } + + Optional first = stream.findFirst(); + if (!first.isPresent()) { + String failureMessage = String.format("Concept %s did not contain designation [%s|%s|%s|%s|%s] ", theConcept, theLanguage, theUseSystem, theUseCode, theUseDisplay, theDesignationValue); + fail(failureMessage); + return null; + } else { + return first.get(); + } + + } + + @SuppressWarnings("BusyWait") + public static void waitForSize(int theTarget, List theList) { + StopWatch sw = new StopWatch(); + while (theList.size() != theTarget && sw.getMillis() <= 16000) { + try { + Thread.sleep(50); + } catch (InterruptedException theE) { + throw new Error(theE); + } + } + if (sw.getMillis() >= 16000 || theList.size() > theTarget) { + String describeResults = theList + .stream() + .map(t -> { + if (t == null) { + return "null"; + } + if (t instanceof IBaseResource) { + return ((IBaseResource) t).getIdElement().getValue(); + } + return t.toString(); + }) + .collect(Collectors.joining(", ")); + fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + describeResults); + } + } + @BeforeAll public static void beforeClassRandomizeLocale() { doRandomizeLocaleAndTimezone(); @@ -723,35 +791,6 @@ public abstract class BaseJpaTest extends BaseTest { return retVal; } - protected TermValueSetConceptDesignation assertTermConceptContainsDesignation(TermValueSetConcept theConcept, String theLanguage, String theUseSystem, String theUseCode, String theUseDisplay, String theDesignationValue) { - Stream stream = theConcept.getDesignations().stream(); - if (theLanguage != null) { - stream = stream.filter(designation -> theLanguage.equalsIgnoreCase(designation.getLanguage())); - } - if (theUseSystem != null) { - stream = stream.filter(designation -> theUseSystem.equalsIgnoreCase(designation.getUseSystem())); - } - if (theUseCode != null) { - stream = stream.filter(designation -> theUseCode.equalsIgnoreCase(designation.getUseCode())); - } - if (theUseDisplay != null) { - stream = stream.filter(designation -> theUseDisplay.equalsIgnoreCase(designation.getUseDisplay())); - } - if (theDesignationValue != null) { - stream = stream.filter(designation -> theDesignationValue.equalsIgnoreCase(designation.getValue())); - } - - Optional first = stream.findFirst(); - if (!first.isPresent()) { - String failureMessage = String.format("Concept %s did not contain designation [%s|%s|%s|%s|%s] ", theConcept, theLanguage, theUseSystem, theUseCode, theUseDisplay, theDesignationValue); - fail(failureMessage); - return null; - } else { - return first.get(); - } - - } - public static void waitForSize(int theTarget, Callable theCallable, Callable theFailureMessage) throws Exception { waitForSize(theTarget, 10000, theCallable, theFailureMessage); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index 9cabd9fdb35..8da2f74ef13 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -60,6 +60,8 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory; import org.apache.commons.io.IOUtils; +import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -218,6 +220,8 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { protected SubscriptionLoader mySubscriptionLoader; @Autowired private IBulkDataExportSvc myBulkDataExportSvc; + @Autowired + private ValidationSupportChain myJpaValidationSupportChain; @BeforeEach public void beforeFlushFT() { @@ -271,5 +275,11 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { return retVal; } + @AfterEach + public void afterEachClearCaches() { + myValueSetDao.purgeCaches(); + myJpaValidationSupportChain.invalidateCaches(); + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 38d10588ec4..44a26b70366 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -426,14 +426,10 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { return retVal; } - @AfterAll - public static void afterClassClearContextBaseJpaDstu3Test() { - if (ourValueSetDao != null) { - ourValueSetDao.purgeCaches(); - } - if (ourJpaValidationSupportChainDstu3 != null) { - ourJpaValidationSupportChainDstu3.invalidateCaches(); - } + @AfterEach + public void afterEachClearCaches() { + myValueSetDao.purgeCaches(); + myJpaValidationSupportChainDstu3.invalidateCaches(); } /** diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java index 247ed81f223..a5c8088855a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3TerminologyTest.java @@ -806,7 +806,7 @@ public class FhirResourceDaoDstu3TerminologyTest extends BaseJpaDstu3Test { assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); } catch (ResourceNotFoundException e) { //noinspection SpellCheckingInspection - assertEquals(Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fexample.com%2Fmy_value_set", e.getMessage()); + assertEquals(Msg.code(2024) + "Unknown ValueSet: http%3A%2F%2Fexample.com%2Fmy_value_set", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 4b718b5184e..90e7fe64406 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -87,9 +87,7 @@ import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.server.BasePagingProvider; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory; -import ca.uhn.fhir.test.utilities.BatchJobHelper; import ca.uhn.fhir.test.utilities.ITestDataBuilder; -import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.validation.FhirValidator; @@ -153,6 +151,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.RiskAssessment; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.ServiceRequest; +import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.Subscription; import org.hl7.fhir.r4.model.Substance; @@ -173,10 +172,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.event.Level; -import org.springframework.batch.core.repository.dao.JobExecutionDao; -import org.springframework.batch.core.repository.dao.JobInstanceDao; -import org.springframework.batch.core.repository.dao.MapJobExecutionDao; -import org.springframework.batch.core.repository.dao.MapJobInstanceDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; @@ -205,8 +200,7 @@ import static org.mockito.Mockito.mock; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {TestR4Config.class}) public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuilder { - private static IValidationSupport ourJpaValidationSupportChainR4; - private static IFhirResourceDaoValueSet ourValueSetDao; + public static final String MY_VALUE_SET = "my-value-set"; @Autowired protected IPackageInstallerSvc myPackageInstallerSvc; @@ -541,12 +535,6 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil termDeferredStorageSvc.clearDeferred(); } - @AfterEach() - public void afterGrabCaches() { - ourValueSetDao = myValueSetDao; - ourJpaValidationSupportChainR4 = myJpaValidationSupportChainR4; - } - @BeforeEach public void beforeCreateInterceptor() { myInterceptor = mock(IServerInterceptor.class); @@ -721,6 +709,38 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil }); } + protected void createLocalCsAndVs() { + //@formatter:off + CodeSystem codeSystem = new CodeSystem(); + codeSystem.setUrl(FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM); + codeSystem.setContent(CodeSystem.CodeSystemContentMode.COMPLETE); + codeSystem + .addConcept().setCode("A").setDisplay("Code A").addDesignation( + new CodeSystem.ConceptDefinitionDesignationComponent().setLanguage("en").setValue("CodeADesignation")).addProperty( + new CodeSystem.ConceptPropertyComponent().setCode("CodeAProperty").setValue(new StringType("CodeAPropertyValue")) + ) + .addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("AA").setDisplay("Code AA") + .addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("AAA").setDisplay("Code AAA")) + ) + .addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("AB").setDisplay("Code AB")); + codeSystem + .addConcept().setCode("B").setDisplay("Code B") + .addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("BA").setDisplay("Code BA")) + .addConcept(new CodeSystem.ConceptDefinitionComponent().setCode("BB").setDisplay("Code BB")); + //@formatter:on + myCodeSystemDao.create(codeSystem, mySrd); + + createLocalVs(codeSystem); + } + + protected void createLocalVs(CodeSystem codeSystem) { + ValueSet valueSet = new ValueSet(); + valueSet.setId(MY_VALUE_SET); + valueSet.setUrl(FhirResourceDaoR4TerminologyTest.URL_MY_VALUE_SET); + valueSet.getCompose().addInclude().setSystem(codeSystem.getUrl()); + myValueSetDao.update(valueSet, mySrd); + } + private static void flattenExpansionHierarchy(List theFlattenedHierarchy, List theCodes, String thePrefix) { theCodes.sort((o1, o2) -> { int s1 = o1.getSequence() != null ? o1.getSequence() : o1.getCode().hashCode(); @@ -738,10 +758,10 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil } } - @AfterAll - public static void afterClassClearContextBaseJpaR4Test() { - ourValueSetDao.purgeCaches(); - ourJpaValidationSupportChainR4.invalidateCaches(); + @AfterEach + public void afterEachClearCaches() { + myValueSetDao.purgeCaches(); + myJpaValidationSupportChainR4.invalidateCaches(); } /** diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java index 3d033bb37e0..e11d7561c0a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ComboUniqueParamTest.java @@ -57,6 +57,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.jupiter.api.Assertions.assertEquals; 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 FhirResourceDaoR4ComboUniqueParamTest extends BaseComboParamsR4Test { @@ -796,6 +797,41 @@ public class FhirResourceDaoR4ComboUniqueParamTest extends BaseComboParamsR4Test } + @Test + public void testUpdateConditionalOverExistingUnique() { + createUniqueIndexPatientIdentifierCount1(); + + Patient pt = new Patient(); + pt.addIdentifier().setSystem("urn").setValue("111"); + IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless(); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(@Nonnull TransactionStatus status) { + List all = myResourceIndexedCompositeStringUniqueDao.findAll(); + assertEquals(1, all.size()); + } + }); + + pt = new Patient(); + pt.addIdentifier().setSystem("urn").setValue("111"); + pt.setActive(true); + String version = myPatientDao.update(pt, "Patient?first-identifier=urn|111").getId().getVersionIdPart(); + assertEquals("2", version); + + new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(@Nonnull TransactionStatus status) { + List all = myResourceIndexedCompositeStringUniqueDao.findAll(); + assertEquals(1, all.size()); + } + }); + + pt = myPatientDao.read(id); + assertTrue(pt.getActive()); + + } + @Test public void testIndexTransactionWithMatchUrl() { Patient pt2 = new Patient(); @@ -1255,7 +1291,7 @@ public class FhirResourceDaoR4ComboUniqueParamTest extends BaseComboParamsR4Test pt1.setManagingOrganization(new Reference("Organization/ORG")); myPatientDao.update(pt1, "Patient?name=FAMILY1&organization.name=ORG").getId().toUnqualifiedVersionless(); - runInTransaction(()->{ + runInTransaction(() -> { List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(1, uniques.size()); assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 2d262954e9b..be4c6f76ec3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -1,7 +1,11 @@ package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum; +import ca.uhn.fhir.jpa.entity.TermValueSet; +import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceTable; @@ -9,6 +13,7 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl; import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.SortSpec; @@ -39,11 +44,14 @@ import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.ServiceRequest; import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; import javax.annotation.Nonnull; import java.io.IOException; @@ -53,6 +61,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.stream.Collectors; +import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -81,6 +90,8 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test myDaoConfig.setAutoCreatePlaceholderReferenceTargets(new DaoConfig().isAutoCreatePlaceholderReferenceTargets()); myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(new DaoConfig().isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets()); myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); + + BaseTermReadSvcImpl.setForceDisableHibernateSearchForUnitTest(false); } @Override @@ -2276,6 +2287,128 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test } + @Test + public void testValueSetExpand_NotPreExpanded_UseHibernateSearch() { + createLocalCsAndVs(); + + logAllConcepts(); + logAllConceptDesignations(); + logAllConceptProperties(); + + ValueSet valueSet = myValueSetDao.read(new IdType(MY_VALUE_SET), mySrd); + + myCaptureQueriesListener.clear(); + ValueSet expansion = (ValueSet) myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet).getValueSet(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); + assertEquals(7, expansion.getExpansion().getContains().size()); + assertEquals(1, expansion.getExpansion().getContains().stream().filter(t->t.getCode().equals("A")).findFirst().orElseThrow(()->new IllegalArgumentException()).getDesignation().size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(5, myCaptureQueriesListener.countSelectQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countInsertQueries()); + assertEquals(1, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); + + // Second time - Should reuse cache + myCaptureQueriesListener.clear(); + expansion = (ValueSet) myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet).getValueSet(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); + assertEquals(7, expansion.getExpansion().getContains().size()); + assertEquals(1, expansion.getExpansion().getContains().stream().filter(t->t.getCode().equals("A")).findFirst().orElseThrow(()->new IllegalArgumentException()).getDesignation().size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.countSelectQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countInsertQueries()); + assertEquals(0, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); + } + + @Test + public void testValueSetExpand_NotPreExpanded_DontUseHibernateSearch() { + BaseTermReadSvcImpl.setForceDisableHibernateSearchForUnitTest(true); + + createLocalCsAndVs(); + + logAllConcepts(); + logAllConceptDesignations(); + logAllConceptProperties(); + + ValueSet valueSet = myValueSetDao.read(new IdType(MY_VALUE_SET), mySrd); + + myCaptureQueriesListener.clear(); + ValueSet expansion = (ValueSet) myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet).getValueSet(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); + assertEquals(7, expansion.getExpansion().getContains().size()); + assertEquals(1, expansion.getExpansion().getContains().stream().filter(t->t.getCode().equals("A")).findFirst().orElseThrow(()->new IllegalArgumentException()).getDesignation().size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(5, myCaptureQueriesListener.countSelectQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countInsertQueries()); + assertEquals(1, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); + + // Second time - Should reuse cache + myCaptureQueriesListener.clear(); + expansion = (ValueSet) myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet).getValueSet(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); + assertEquals(7, expansion.getExpansion().getContains().size()); + assertEquals(1, expansion.getExpansion().getContains().stream().filter(t->t.getCode().equals("A")).findFirst().orElseThrow(()->new IllegalArgumentException()).getDesignation().size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.countSelectQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countInsertQueries()); + assertEquals(0, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); + } + + @Test + public void testValueSetExpand_PreExpanded_UseHibernateSearch() { + createLocalCsAndVs(); + + myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); + runInTransaction(()->{ + Slice page = myTermValueSetDao.findByExpansionStatus(PageRequest.of(0, 10), TermValueSetPreExpansionStatusEnum.EXPANDED); + assertEquals(1, page.getContent().size()); + }); + + logAllConcepts(); + logAllConceptDesignations(); + logAllConceptProperties(); + + ValueSet valueSet = myValueSetDao.read(new IdType(MY_VALUE_SET), mySrd); + + myCaptureQueriesListener.clear(); + ValueSet expansion = (ValueSet) myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet).getValueSet(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); + assertEquals(7, expansion.getExpansion().getContains().size()); + assertEquals(1, expansion.getExpansion().getContains().stream().filter(t->t.getCode().equals("A")).findFirst().orElseThrow(()->new IllegalArgumentException()).getDesignation().size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(3, myCaptureQueriesListener.countSelectQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countInsertQueries()); + assertEquals(1, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); + + // Second time - Should reuse cache + myCaptureQueriesListener.clear(); + expansion = (ValueSet) myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet).getValueSet(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); + assertEquals(7, expansion.getExpansion().getContains().size()); + assertEquals(1, expansion.getExpansion().getContains().stream().filter(t->t.getCode().equals("A")).findFirst().orElseThrow(()->new IllegalArgumentException()).getDesignation().size()); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + assertEquals(0, myCaptureQueriesListener.countSelectQueries()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); + assertEquals(0, myCaptureQueriesListener.countUpdateQueries()); + assertEquals(0, myCaptureQueriesListener.countInsertQueries()); + assertEquals(0, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); + } + @Test public void testMassIngestionMode_TransactionWithChanges() { myDaoConfig.setDeleteEnabled(false); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java index ccf7d8ef108..f064888489f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyTest.java @@ -168,7 +168,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); - ResourceTable table = runInTransaction(()->myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new)); + ResourceTable table = runInTransaction(() -> myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new)); TermCodeSystemVersion cs = new TermCodeSystemVersion(); cs.setResource(table); @@ -194,34 +194,6 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { myTerminologyDeferredStorageSvc.saveAllDeferred(); } - private void createLocalCsAndVs() { - //@formatter:off - CodeSystem codeSystem = new CodeSystem(); - codeSystem.setUrl(URL_MY_CODE_SYSTEM); - codeSystem.setContent(CodeSystemContentMode.COMPLETE); - codeSystem - .addConcept().setCode("A").setDisplay("Code A") - .addConcept(new ConceptDefinitionComponent().setCode("AA").setDisplay("Code AA") - .addConcept(new ConceptDefinitionComponent().setCode("AAA").setDisplay("Code AAA")) - ) - .addConcept(new ConceptDefinitionComponent().setCode("AB").setDisplay("Code AB")); - codeSystem - .addConcept().setCode("B").setDisplay("Code B") - .addConcept(new ConceptDefinitionComponent().setCode("BA").setDisplay("Code BA")) - .addConcept(new ConceptDefinitionComponent().setCode("BB").setDisplay("Code BB")); - //@formatter:on - myCodeSystemDao.create(codeSystem, mySrd); - - createLocalVs(codeSystem); - } - - private void createLocalVs(CodeSystem codeSystem) { - ValueSet valueSet = new ValueSet(); - valueSet.setUrl(URL_MY_VALUE_SET); - valueSet.getCompose().addInclude().setSystem(codeSystem.getUrl()); - myValueSetDao.create(valueSet, mySrd); - } - private void logAndValidateValueSet(ValueSet theResult) { IParser parser = myFhirCtx.newXmlParser().setPrettyPrint(true); String encoded = parser.encodeResourceToString(theResult); @@ -531,7 +503,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); - ResourceTable table = runInTransaction(()->myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new)); + ResourceTable table = runInTransaction(() -> myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new)); TermCodeSystemVersion cs = new TermCodeSystemVersion(); cs.setResource(table); @@ -857,7 +829,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { codeSystem.setContent(CodeSystemContentMode.NOTPRESENT); IIdType id = myCodeSystemDao.create(codeSystem, mySrd).getId().toUnqualified(); - ResourceTable table = runInTransaction(()->myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new)); + ResourceTable table = runInTransaction(() -> myResourceTableDao.findById(id.getIdPartAsLong()).orElseThrow(IllegalStateException::new)); TermCodeSystemVersion cs = new TermCodeSystemVersion(); cs.setResource(table); @@ -1250,15 +1222,44 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { obsCA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("CA"); myObservationDao.create(obsCA, mySrd).getId().toUnqualifiedVersionless(); - SearchParameterMap params = new SearchParameterMap(); + SearchParameterMap params = SearchParameterMap.newSynchronous(); params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.IN)); assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), containsInAnyOrder(idAA.getValue(), idBA.getValue())); + myCaptureQueriesListener.clear(); + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), containsInAnyOrder(idAA.getValue(), idBA.getValue())); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + + } + + @Test + public void testSearchCodeNotInLocalCodesystem() { + createLocalCsAndVs(); + + Observation obsAA = new Observation(); + obsAA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("AA"); + IIdType idAA = myObservationDao.create(obsAA, mySrd).getId().toUnqualifiedVersionless(); + + Observation obsBA = new Observation(); + obsBA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("BA"); + IIdType idBA = myObservationDao.create(obsBA, mySrd).getId().toUnqualifiedVersionless(); + + Observation obsCA = new Observation(); + obsCA.getCode().addCoding().setSystem(URL_MY_CODE_SYSTEM).setCode("CA"); + IIdType idCA = myObservationDao.create(obsCA, mySrd).getId().toUnqualifiedVersionless(); + + SearchParameterMap params = SearchParameterMap.newSynchronous(); + params.add(Observation.SP_CODE, new TokenParam(null, URL_MY_VALUE_SET).setModifier(TokenParamModifier.NOT_IN)); + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), containsInAnyOrder(idCA.getValue())); + + myCaptureQueriesListener.clear(); + assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), containsInAnyOrder(idCA.getValue())); + myCaptureQueriesListener.logSelectQueriesForCurrentThread(); + } @Test public void testSearchCodeInUnknownCodeSystem() { - SearchParameterMap params = new SearchParameterMap(); try { @@ -1266,7 +1267,7 @@ public class FhirResourceDaoR4TerminologyTest extends BaseJpaR4Test { assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params)), empty()); } catch (ResourceNotFoundException e) { //noinspection SpellCheckingInspection - assertEquals(Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fexample.com%2Fmy_value_set", e.getMessage()); + assertEquals(Msg.code(2024) + "Unknown ValueSet: http%3A%2F%2Fexample.com%2Fmy_value_set", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetMultiVersionTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetMultiVersionTest.java index c6b33820e58..a1b28d52556 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetMultiVersionTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValueSetMultiVersionTest.java @@ -12,6 +12,7 @@ import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; +import org.springframework.test.annotation.DirtiesContext; import java.util.HashMap; import java.util.HashSet; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index 65c54426062..318d6806332 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -1319,13 +1319,12 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { @Test public void testSearch_IdParamSecond_ForcedId_SpecificPartition() { - // FIXME: move down IIdType patientId1 = createPatient(withPartition(1), withId("PT-1"), withActiveTrue()); - logAllTokenIndexes(); - IIdType patientIdNull = createPatient(withPartition(null), withId("PT-NULL"), withActiveTrue()); IIdType patientId2 = createPatient(withPartition(2), withId("PT-2"), withActiveTrue()); + logAllTokenIndexes(); + /* ******************************* * _id param is second parameter * *******************************/ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java index d65b62cf94b..eb73e9ebfc6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java @@ -566,10 +566,10 @@ public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuil }); } - @AfterAll - public static void afterClassClearContextBaseJpaR5Test() { - ourValueSetDao.purgeCaches(); - ourJpaValidationSupportChainR5.invalidateCaches(); + @AfterEach + public void afterEachClearCaches() { + myValueSetDao.purgeCaches(); + myJpaValidationSupportChain.invalidateCaches(); } /** diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java index 8f5fbe73dd1..e6d4631efcc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetTest.java @@ -441,7 +441,7 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } @@ -484,7 +484,7 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3 .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java index 8039c0b8342..b443e931128 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3ValueSetVersionedTest.java @@ -576,7 +576,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); } } @@ -657,7 +657,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java index 61ce00a5c1c..02bfd356f6b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorJpaR4Test.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider; +import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; @@ -61,6 +62,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; @@ -68,6 +70,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Test { @@ -678,6 +681,34 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes } + @Test + public void testSearchCodeIn() { + createLocalCsAndVs(); + + createObservation(withId("allowed"), withObservationCode(FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM, "A")); + createObservation(withId("disallowed"), withObservationCode(FhirResourceDaoR4TerminologyTest.URL_MY_CODE_SYSTEM, "foo")); + + ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().read().resourcesOfType("Observation").withCodeInValueSet("code", FhirResourceDaoR4TerminologyTest.URL_MY_VALUE_SET).andThen() + .build(); + } + }.setValidationSupport(myValidationSupport)); + + // Should be ok + myClient.read().resource(Observation.class).withId("Observation/allowed").execute(); + + try { + myClient.read().resource(Observation.class).withId("Observation/disallowed").execute(); + fail(); + } catch (ForbiddenOperationException e) { + // good + } + + } + /** * See #751 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java index 94738c712ec..ae93c93111f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetNoVerCSNoVerTest.java @@ -159,7 +159,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv private void createExternalCsAndLocalVs() { runInTransaction(() -> { CodeSystem codeSystem = createExternalCs(); - createLocalVs(codeSystem); + createLocalVsForCodeSystem(codeSystem); }); } @@ -197,7 +197,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv myLocalValueSetId = myValueSetDao.create(myLocalVs, mySrd).getId().toUnqualifiedVersionless(); } - private void createLocalVs(CodeSystem codeSystem) { + private void createLocalVsForCodeSystem(CodeSystem codeSystem) { myLocalVs = new ValueSet(); myLocalVs.setUrl(URL_MY_VALUE_SET); ConceptSetComponent include = myLocalVs.getCompose().addInclude(); @@ -445,7 +445,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } @@ -493,7 +493,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } @@ -1075,7 +1075,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .execute(); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); assertThat(toDirectCodes(expansion.getExpansion().getContains()), containsInAnyOrder("A", "AA", "AB", "AAA")); - assertEquals(19, myCaptureQueriesListener.getSelectQueries().size()); + assertEquals(11, myCaptureQueriesListener.getSelectQueries().size()); assertEquals("ValueSet \"ValueSet.url[http://example.com/my_value_set]\" has not yet been pre-expanded. Performing in-memory expansion without parameters. Current status: NOT_EXPANDED | The ValueSet is waiting to be picked up and pre-expanded by a scheduled task.", expansion.getMeta().getExtensionString(EXT_VALUESET_EXPANSION_MESSAGE)); // Hierarchical @@ -1092,7 +1092,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv assertThat(toDirectCodes(expansion.getExpansion().getContains()), containsInAnyOrder("A")); assertThat(toDirectCodes(expansion.getExpansion().getContains().get(0).getContains()), containsInAnyOrder("AA", "AB")); assertThat(toDirectCodes(expansion.getExpansion().getContains().get(0).getContains().stream().filter(t -> t.getCode().equals("AA")).findFirst().orElseThrow(() -> new IllegalArgumentException()).getContains()), containsInAnyOrder("AAA")); - assertEquals(16, myCaptureQueriesListener.getSelectQueries().size()); + assertEquals(12, myCaptureQueriesListener.getSelectQueries().size()); } @@ -1115,7 +1115,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .execute(); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); assertThat(toDirectCodes(expansion.getExpansion().getContains()), containsInAnyOrder("A", "AA", "AB", "AAA")); - assertEquals(15, myCaptureQueriesListener.getSelectQueries().size()); + assertEquals(7, myCaptureQueriesListener.getSelectQueries().size()); assertEquals("ValueSet with URL \"Unidentified ValueSet\" was expanded using an in-memory expansion", expansion.getMeta().getExtensionString(EXT_VALUESET_EXPANSION_MESSAGE)); // Hierarchical @@ -1132,7 +1132,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv assertThat(toDirectCodes(expansion.getExpansion().getContains()), containsInAnyOrder("A")); assertThat(toDirectCodes(expansion.getExpansion().getContains().get(0).getContains()), containsInAnyOrder("AA", "AB")); assertThat(toDirectCodes(expansion.getExpansion().getContains().get(0).getContains().stream().filter(t -> t.getCode().equals("AA")).findFirst().orElseThrow(() -> new IllegalArgumentException()).getContains()), containsInAnyOrder("AAA")); - assertEquals(14, myCaptureQueriesListener.getSelectQueries().size()); + assertEquals(10, myCaptureQueriesListener.getSelectQueries().size()); } @@ -1147,6 +1147,8 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv myTermSvc.preExpandDeferredValueSetsToTerminologyTables(); + logAllValueSetConcepts(); + // Do a warm-up pass to precache anything that can be pre-cached myClient .operation() @@ -1156,7 +1158,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .returnResourceType(ValueSet.class) .execute(); - // Non-hierarchical + // Non-hierarchical (Should reuse cache) myCaptureQueriesListener.clear(); expansion = myClient .operation() @@ -1167,10 +1169,10 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv .execute(); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(expansion)); assertThat(toDirectCodes(expansion.getExpansion().getContains()), containsInAnyOrder("A", "AA", "AB", "AAA")); - assertEquals(3, myCaptureQueriesListener.getSelectQueries().size()); + assertEquals(0, myCaptureQueriesListener.getSelectQueries().size()); assertThat(expansion.getMeta().getExtensionString(EXT_VALUESET_EXPANSION_MESSAGE), containsString("ValueSet was expanded using an expansion that was pre-calculated")); - // Hierarchical + // Hierarchical (shouldn't reuse cache) myCaptureQueriesListener.clear(); expansion = myClient .operation() diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java index 9181542e872..c2e1178e3cf 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSNoVerTest.java @@ -152,7 +152,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid private void createExternalCsAndLocalVs() { runInTransaction(()-> { CodeSystem codeSystem = createExternalCs(); - createLocalVs(codeSystem); + createLocalVsForCodeSystem(codeSystem); }); } @@ -163,7 +163,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid }); } - private void createLocalVs(CodeSystem codeSystem) { + private void createLocalVsForCodeSystem(CodeSystem codeSystem) { myLocalVs = new ValueSet(); myLocalVs.setUrl(URL_MY_VALUE_SET); ConceptSetComponent include = myLocalVs.getCompose().addInclude(); @@ -357,7 +357,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } @@ -405,7 +405,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java index c1fa367cf35..92a2f776ab4 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4ValueSetVerCSVerTest.java @@ -526,7 +526,7 @@ public class ResourceProviderR4ValueSetVerCSVerTest extends BaseResourceProvider .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); } } @@ -610,7 +610,7 @@ public class ResourceProviderR4ValueSetVerCSVerTest extends BaseResourceProvider .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java index 3209f081fa5..3e6ca22bd12 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetTest.java @@ -485,7 +485,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } @@ -622,7 +622,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test { .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java index 7543e40bd70..973f80451df 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r5/ResourceProviderR5ValueSetVersionedTest.java @@ -558,7 +558,7 @@ public class ResourceProviderR5ValueSetVersionedTest extends BaseResourceProvide .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); } } @@ -640,7 +640,7 @@ public class ResourceProviderR5ValueSetVersionedTest extends BaseResourceProvide .execute(); } catch (ResourceNotFoundException e) { assertEquals(404, e.getStatusCode()); - assertEquals("HTTP 404 Not Found: " + Msg.code(886) + "Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); + assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-cql/pom.xml b/hapi-fhir-jpaserver-cql/pom.xml index 07989e27b2c..d401b369888 100644 --- a/hapi-fhir-jpaserver-cql/pom.xml +++ b/hapi-fhir-jpaserver-cql/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 1c928e769e1..07b2f843538 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index bd317e7d59a..2678b113eab 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 11a3f8d0c86..8b42efd6b8a 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index ce193566620..91c879b2cc0 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 407f9ba2e6b..73ce2dbaa01 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index fb24397d89c..06651ae1025 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 368e0f49984..64de30d51fd 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index 6b184ce8a83..06330c2e62f 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 060fd3befc8..572bf89e269 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AllowedCodeInValueSet.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AllowedCodeInValueSet.java new file mode 100644 index 00000000000..9ce93b38803 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AllowedCodeInValueSet.java @@ -0,0 +1,59 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import javax.annotation.Nonnull; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +class AllowedCodeInValueSet { + private final String myResourceName; + private final String mySearchParameterName; + private final String myValueSetUrl; + private final boolean myNegate; + + public AllowedCodeInValueSet(@Nonnull String theResourceName, @Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl, boolean theNegate) { + assert isNotBlank(theResourceName); + assert isNotBlank(theSearchParameterName); + assert isNotBlank(theValueSetUrl); + + myResourceName = theResourceName; + mySearchParameterName = theSearchParameterName; + myValueSetUrl = theValueSetUrl; + myNegate = theNegate; + } + + public String getResourceName() { + return myResourceName; + } + + public String getSearchParameterName() { + return mySearchParameterName; + } + + public String getValueSetUrl() { + return myValueSetUrl; + } + + public boolean isNegate() { + return myNegate; + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 033934471de..38f41b92ce5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -20,8 +20,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * #L% */ -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.IValidationSupport; +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; @@ -43,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -78,12 +80,15 @@ public class AuthorizationInterceptor implements IRuleApplier { private final String myRequestRuleListKey = AuthorizationInterceptor.class.getName() + "_" + myInstanceIndex + "_RULELIST"; private PolicyEnum myDefaultPolicy = PolicyEnum.DENY; private Set myFlags = Collections.emptySet(); + private IValidationSupport myValidationSupport; + private Logger myTroubleshootingLog; /** * Constructor */ public AuthorizationInterceptor() { super(); + setTroubleshootingLog(ourLog); } /** @@ -96,6 +101,17 @@ public class AuthorizationInterceptor implements IRuleApplier { setDefaultPolicy(theDefaultPolicy); } + @Nonnull + @Override + public Logger getTroubleshootingLog() { + return myTroubleshootingLog; + } + + public void setTroubleshootingLog(@Nonnull Logger theTroubleshootingLog) { + Validate.notNull(theTroubleshootingLog, "theTroubleshootingLog must not be null"); + myTroubleshootingLog = theTroubleshootingLog; + } + private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Pointcut thePointcut) { Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, thePointcut); @@ -139,6 +155,28 @@ public class AuthorizationInterceptor implements IRuleApplier { return verdict; } + /** + * @since 6.0.0 + */ + @Nullable + @Override + public IValidationSupport getValidationSupport() { + return myValidationSupport; + } + + /** + * Sets a validation support module that will be used for terminology-based rules + * + * @param theValidationSupport The validation support. Null is also acceptable (this is the default), + * in which case the validation support module associated with the {@link FhirContext} + * will be used. + * @since 6.0.0 + */ + public AuthorizationInterceptor setValidationSupport(IValidationSupport theValidationSupport) { + myValidationSupport = theValidationSupport; + return this; + } + /** * Subclasses should override this method to supply the set of rules to be applied to * this individual request. @@ -363,7 +401,6 @@ public class AuthorizationInterceptor implements IRuleApplier { applyRulesAndFailIfDeny(restOperationType, theRequestDetails, null, null, null, thePointcut); } - private void checkPointcutAndFailIfDeny(RequestDetails theRequestDetails, Pointcut thePointcut, @Nonnull IBaseResource theInputResource) { applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, theInputResource, theInputResource.getIdElement(), null, thePointcut); } @@ -442,6 +479,34 @@ public class AuthorizationInterceptor implements IRuleApplier { OUT, } + static List toListOfResourcesAndExcludeContainer(IBaseResource theResponseObject, FhirContext fhirContext) { + if (theResponseObject == null) { + return Collections.emptyList(); + } + + List retVal; + + boolean isContainer = false; + if (theResponseObject instanceof IBaseBundle) { + isContainer = true; + } else if (theResponseObject instanceof IBaseParameters) { + isContainer = true; + } + + if (!isContainer) { + return Collections.singletonList(theResponseObject); + } + + retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); + + // Exclude the container + if (retVal.size() > 0 && retVal.get(0) == theResponseObject) { + retVal = retVal.subList(1, retVal.size()); + } + + return retVal; + } + public static class Verdict { private final IAuthRule myDecidingRule; @@ -478,32 +543,4 @@ public class AuthorizationInterceptor implements IRuleApplier { } - static List toListOfResourcesAndExcludeContainer(IBaseResource theResponseObject, FhirContext fhirContext) { - if (theResponseObject == null) { - return Collections.emptyList(); - } - - List retVal; - - boolean isContainer = false; - if (theResponseObject instanceof IBaseBundle) { - isContainer = true; - } else if (theResponseObject instanceof IBaseParameters) { - isContainer = true; - } - - if (!isContainer) { - return Collections.singletonList(theResponseObject); - } - - retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class); - - // Exclude the container - if (retVal.size() > 0 && retVal.get(0) == theResponseObject) { - retVal = retVal.subList(1, retVal.size()); - } - - return retVal; - } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java index c39ef0d1904..cf8c64fc9d6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java @@ -23,6 +23,8 @@ package ca.uhn.fhir.rest.server.interceptor.auth; import ca.uhn.fhir.rest.api.server.RequestDetails; import org.apache.commons.lang3.Validate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -33,11 +35,19 @@ public class AuthorizedList { private List myAllowedCompartments; private List myAllowedInstances; + private List myAllowedCodeInValueSets; + @Nullable List getAllowedCompartments() { return myAllowedCompartments; } + @Nullable + List getAllowedCodeInValueSets() { + return myAllowedCodeInValueSets; + } + + @Nullable List getAllowedInstances() { return myAllowedInstances; } @@ -101,4 +111,51 @@ public class AuthorizedList { } return this; } + + /** + * If specified, any search for theResourceName will automatically include a parameter indicating that + * the token search parameter theSearchParameterName must have a value in the ValueSet with URL theValueSetUrl. + * + * @param theResourceName The resource name, e.g. Observation + * @param theSearchParameterName The search parameter name, e.g. code + * @param theValueSetUrl The valueset URL, e.g. http://my-value-set + * @return Returns a reference to this for easy chaining + * @see AuthorizationInterceptor If search narrowing by code is being used for security reasons, consider also using AuthorizationInterceptor as a failsafe to ensure that no inapproproiate resources are returned + * @since 6.0.0 + */ + public AuthorizedList addCodeInValueSet(@Nonnull String theResourceName, @Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl) { + Validate.notBlank(theResourceName, "theResourceName must not be missing or null"); + Validate.notBlank(theSearchParameterName, "theSearchParameterName must not be missing or null"); + Validate.notBlank(theValueSetUrl, "theResourceUrl must not be missing or null"); + + return doAddCodeInValueSet(theResourceName, theSearchParameterName, theValueSetUrl, false); + } + + /** + * If specified, any search for theResourceName will automatically include a parameter indicating that + * the token search parameter theSearchParameterName must have a value not in the ValueSet with URL theValueSetUrl. + * + * @param theResourceName The resource name, e.g. Observation + * @param theSearchParameterName The search parameter name, e.g. code + * @param theValueSetUrl The valueset URL, e.g. http://my-value-set + * @return Returns a reference to this for easy chaining + * @see AuthorizationInterceptor If search narrowing by code is being used for security reasons, consider also using AuthorizationInterceptor as a failsafe to ensure that no inapproproiate resources are returned + * @since 6.0.0 + */ + public AuthorizedList addCodeNotInValueSet(@Nonnull String theResourceName, @Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl) { + Validate.notBlank(theResourceName, "theResourceName must not be missing or null"); + Validate.notBlank(theSearchParameterName, "theSearchParameterName must not be missing or null"); + Validate.notBlank(theValueSetUrl, "theResourceUrl must not be missing or null"); + + return doAddCodeInValueSet(theResourceName, theSearchParameterName, theValueSetUrl, true); + } + + private AuthorizedList doAddCodeInValueSet(String theResourceName, String theSearchParameterName, String theValueSetUrl, boolean negate) { + if (myAllowedCodeInValueSets == null) { + myAllowedCodeInValueSets = new ArrayList<>(); + } + myAllowedCodeInValueSets.add(new AllowedCodeInValueSet(theResourceName, theSearchParameterName, theValueSetUrl, negate)); + + return this; + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java index 4099f2b259e..6c3861d4752 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifier.java @@ -25,6 +25,8 @@ import java.util.List; import org.hl7.fhir.instance.model.api.IIdType; +import javax.annotation.Nonnull; + public interface IAuthRuleBuilderRuleOpClassifier { /** @@ -116,4 +118,20 @@ public interface IAuthRuleBuilderRuleOpClassifier { *

*/ IAuthRuleBuilderRuleOpClassifierFinished withAnyId(); + + /** + * Rule applies to resources where the given search parameter would be satisfied by a code in the given ValueSet + * @param theSearchParameterName The search parameter name, e.g. "code" + * @param theValueSetUrl The valueset URL, e.g. "http://my-value-set" + * @since 6.0.0 + */ + IAuthRuleBuilderRuleOpClassifierFinished withCodeInValueSet(@Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl); + + /** + * Rule applies to resources where the given search parameter would be satisfied by a code not in the given ValueSet + * @param theSearchParameterName The search parameter name, e.g. "code" + * @param theValueSetUrl The valueset URL, e.g. "http://my-value-set" + * @since 6.0.0 + */ + IAuthRuleFinished withCodeNotInValueSet(@Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java index 2951ddac34f..2441219071d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * #L% */ +import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.Pointcut; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -27,9 +28,18 @@ import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict; +import org.slf4j.Logger; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; public interface IRuleApplier { + @Nonnull + Logger getTroubleshootingLog(); + Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Pointcut thePointcut); + @Nullable + IValidationSupport getValidationSupport(); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 14b08ea4ea7..4a8edc5d5ae 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -472,22 +472,26 @@ public class RuleBuilder implements IAuthRuleBuilder { } private RuleBuilderFinished finished() { - Validate.isTrue(myRule == null, "Can not call finished() twice"); - myRule = new RuleImplOp(myRuleName); - myRule.setMode(myRuleMode); - myRule.setOp(myRuleOp); - myRule.setAppliesTo(myAppliesTo); - myRule.setAppliesToTypes(myAppliesToTypes); - myRule.setAppliesToInstances(myAppliesToInstances); - myRule.setClassifierType(myClassifierType); - myRule.setClassifierCompartmentName(myInCompartmentName); - myRule.setClassifierCompartmentOwners(myInCompartmentOwners); - myRule.setAppliesToDeleteCascade(myOnCascade); - myRule.setAppliesToDeleteExpunge(myOnExpunge); - myRule.setAdditionalSearchParamsForCompartmentTypes(myAdditionalSearchParamsForCompartmentTypes); - myRules.add(myRule); + return finished(new RuleImplOp(myRuleName)); + } - return new RuleBuilderFinished(myRule); + private RuleBuilderFinished finished(RuleImplOp theRule) { + Validate.isTrue(myRule == null, "Can not call finished() twice"); + myRule = theRule; + theRule.setMode(myRuleMode); + theRule.setOp(myRuleOp); + theRule.setAppliesTo(myAppliesTo); + theRule.setAppliesToTypes(myAppliesToTypes); + theRule.setAppliesToInstances(myAppliesToInstances); + theRule.setClassifierType(myClassifierType); + theRule.setClassifierCompartmentName(myInCompartmentName); + theRule.setClassifierCompartmentOwners(myInCompartmentOwners); + theRule.setAppliesToDeleteCascade(myOnCascade); + theRule.setAppliesToDeleteExpunge(myOnExpunge); + theRule.setAdditionalSearchParamsForCompartmentTypes(myAdditionalSearchParamsForCompartmentTypes); + myRules.add(theRule); + + return new RuleBuilderFinished(theRule); } @Override @@ -554,6 +558,24 @@ public class RuleBuilder implements IAuthRuleBuilder { return finished(); } + @Override + public IAuthRuleBuilderRuleOpClassifierFinished withCodeInValueSet(@Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl) { + SearchParameterAndValueSetRuleImpl rule = new SearchParameterAndValueSetRuleImpl(myRuleName); + rule.setSearchParameterName(theSearchParameterName); + rule.setValueSetUrl(theValueSetUrl); + rule.setWantCode(true); + return finished(rule); + } + + @Override + public IAuthRuleFinished withCodeNotInValueSet(@Nonnull String theSearchParameterName, @Nonnull String theValueSetUrl) { + SearchParameterAndValueSetRuleImpl rule = new SearchParameterAndValueSetRuleImpl(myRuleName); + rule.setSearchParameterName(theSearchParameterName); + rule.setValueSetUrl(theValueSetUrl); + rule.setWantCode(false); + return finished(rule); + } + RuleBuilderFinished addInstances(Collection theInstances) { myAppliesToInstances.addAll(theInstances); return new RuleBuilderFinished(myRule); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index e4adeb96f1c..324cb9caef8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -298,11 +298,21 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { throw new IllegalStateException(Msg.code(336) + "Unable to apply security to event of applies to type " + myAppliesTo); } + return applyRuleLogic(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, ctx, target, theRuleApplier); + } + + /** + * Apply any special processing logic specific to this rule. + * This is intended to be overridden. + * + * TODO: At this point {@link RuleImplOp} handles "any ID" and "in compartment" logic - It would be nice to split these into separate classes. + */ + protected Verdict applyRuleLogic(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Set theFlags, FhirContext theFhirContext, RuleTarget theRuleTarget, IRuleApplier theRuleApplier) { switch (myClassifierType) { case ANY_ID: break; case IN_COMPARTMENT: - return applyRuleToCompartment(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, ctx, target); + return applyRuleToCompartment(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, theFlags, theFhirContext, theRuleTarget); default: throw new IllegalStateException(Msg.code(337) + "Unable to apply security to event of applies to type " + myAppliesTo); } @@ -704,4 +714,5 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { public void setAdditionalSearchParamsForCompartmentTypes(AdditionalCompartmentSearchParameters theAdditionalParameters) { myAdditionalCompartmentSearchParamMap = theAdditionalParameters; } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java index 271f34a50de..409ea2e2c89 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.rest.api.Constants; @@ -31,23 +32,31 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails; import ca.uhn.fhir.rest.server.util.ServletRequestUtil; import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.bundle.ModifiableBundleEntry; import com.google.common.collect.ArrayListMultimap; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -75,8 +84,6 @@ import java.util.stream.Collectors; * @see AuthorizationInterceptor */ public class SearchNarrowingInterceptor { - private static final Logger ourLog = LoggerFactory.getLogger(SearchNarrowingInterceptor.class); - /** * Subclasses should override this method to supply the set of compartments that @@ -106,7 +113,6 @@ public class SearchNarrowingInterceptor { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName()); - HashMap> parameterToOrValues = new HashMap<>(); AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails); if (authorizedList == null) { return true; @@ -118,19 +124,27 @@ public class SearchNarrowingInterceptor { */ Collection compartments = authorizedList.getAllowedCompartments(); if (compartments != null) { - processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, compartments, true); + Map> parameterToOrValues = processResourcesOrCompartments(theRequestDetails, resDef, compartments, true); + applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true); } Collection resources = authorizedList.getAllowedInstances(); if (resources != null) { - processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, resources, false); + Map> parameterToOrValues = processResourcesOrCompartments(theRequestDetails, resDef, resources, false); + applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true); + } + List allowedCodeInValueSet = authorizedList.getAllowedCodeInValueSets(); + if (allowedCodeInValueSet != null) { + Map> parameterToOrValues = processAllowedCodes(resDef, allowedCodeInValueSet); + applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, false); } - /* - * Add any param values to the actual request - */ - if (parameterToOrValues.size() > 0) { + return true; + } + + private void applyParametersToRequestDetails(RequestDetails theRequestDetails, @Nullable Map> theParameterToOrValues, boolean thePatientIdMode) { + if (theParameterToOrValues != null) { Map newParameters = new HashMap<>(theRequestDetails.getParameters()); - for (Map.Entry> nextEntry : parameterToOrValues.entrySet()) { + for (Map.Entry> nextEntry : theParameterToOrValues.entrySet()) { String nextParamName = nextEntry.getKey(); List nextAllowedValues = nextEntry.getValue(); @@ -151,43 +165,54 @@ public class SearchNarrowingInterceptor { * requested, and the values that the user is allowed to see */ String[] existingValues = newParameters.get(nextParamName); - List nextAllowedValueIds = nextAllowedValues - .stream() - .map(t -> t.lastIndexOf("/") > -1 ? t.substring(t.lastIndexOf("/") + 1) : t) - .collect(Collectors.toList()); - boolean restrictedExistingList = false; - for (int i = 0; i < existingValues.length; i++) { - String nextExistingValue = existingValues[i]; - List nextRequestedValues = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue); - List nextPermittedValues = ListUtils.union( - ListUtils.intersection(nextRequestedValues, nextAllowedValues), - ListUtils.intersection(nextRequestedValues, nextAllowedValueIds) - ); - if (nextPermittedValues.size() > 0) { - restrictedExistingList = true; - existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues); + if (thePatientIdMode) { + List nextAllowedValueIds = nextAllowedValues + .stream() + .map(t -> t.lastIndexOf("/") > -1 ? t.substring(t.lastIndexOf("/") + 1) : t) + .collect(Collectors.toList()); + boolean restrictedExistingList = false; + for (int i = 0; i < existingValues.length; i++) { + + String nextExistingValue = existingValues[i]; + List nextRequestedValues = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue); + List nextPermittedValues = ListUtils.union( + ListUtils.intersection(nextRequestedValues, nextAllowedValues), + ListUtils.intersection(nextRequestedValues, nextAllowedValueIds) + ); + if (nextPermittedValues.size() > 0) { + restrictedExistingList = true; + existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues); + } + } + /* + * If none of the values that were requested by the client overlap at all + * with the values that the user is allowed to see, the client shouldn't + * get *any* results back. We return an error code indicating that the + * caller is forbidden from accessing the resources they requested. + */ + if (!restrictedExistingList) { + throw new ForbiddenOperationException(Msg.code(2026) + "Value not permitted for parameter " + UrlUtil.escapeUrlParam(nextParamName)); + } + + } else { + + int existingValuesCount = existingValues.length; + String[] newValues = Arrays.copyOf(existingValues, existingValuesCount + nextAllowedValues.size()); + for (int i = 0; i < nextAllowedValues.size(); i++) { + newValues[existingValuesCount + i] = nextAllowedValues.get(i); + } + newParameters.put(nextParamName, newValues); + } - /* - * If none of the values that were requested by the client overlap at all - * with the values that the user is allowed to see, the client shouldn't - * get *any* results back. We return an error code indicating that the - * caller is forbidden from accessing the resources they requested. - */ - if (!restrictedExistingList) { - theResponse.setStatus(Constants.STATUS_HTTP_403_FORBIDDEN); - return false; - } } } theRequestDetails.setParameters(newParameters); } - - return true; } @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED) @@ -202,37 +227,10 @@ public class SearchNarrowingInterceptor { BundleUtil.processEntries(ctx, bundle, processor); } - private class BundleEntryUrlProcessor implements Consumer { - private final FhirContext myFhirContext; - private final ServletRequestDetails myRequestDetails; - private final HttpServletRequest myRequest; - private final HttpServletResponse myResponse; + @Nullable + private Map> processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, Collection theResourcesOrCompartments, boolean theAreCompartments) { + Map> retVal = null; - public BundleEntryUrlProcessor(FhirContext theFhirContext, ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) { - myFhirContext = theFhirContext; - myRequestDetails = theRequestDetails; - myRequest = theRequest; - myResponse = theResponse; - } - - @Override - public void accept(ModifiableBundleEntry theModifiableBundleEntry) { - ArrayListMultimap paramValues = ArrayListMultimap.create(); - - String url = theModifiableBundleEntry.getRequestUrl(); - - ServletSubRequestDetails subServletRequestDetails = ServletRequestUtil.getServletSubRequestDetails(myRequestDetails, url, paramValues); - BaseMethodBinding method = subServletRequestDetails.getServer().determineResourceMethod(subServletRequestDetails, url); - RestOperationTypeEnum restOperationType = method.getRestOperationType(); - subServletRequestDetails.setRestOperationType(restOperationType); - - incomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse); - - theModifiableBundleEntry.setRequestUrl(myFhirContext, ServletRequestUtil.extractUrl(subServletRequestDetails)); - } - } - - private void processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, HashMap> theParameterToOrValues, Collection theResourcesOrCompartments, boolean theAreCompartments) { String lastCompartmentName = null; String lastSearchParamName = null; for (String nextCompartment : theResourcesOrCompartments) { @@ -262,12 +260,44 @@ public class SearchNarrowingInterceptor { } if (searchParamName != null) { - List orValues = theParameterToOrValues.computeIfAbsent(searchParamName, t -> new ArrayList<>()); + if (retVal == null) { + retVal = new HashMap<>(); + } + List orValues = retVal.computeIfAbsent(searchParamName, t -> new ArrayList<>()); orValues.add(nextCompartment); } } + + return retVal; } + @Nullable + private Map> processAllowedCodes(RuntimeResourceDefinition theResDef, List theAllowedCodeInValueSet) { + Map> retVal = null; + + for (AllowedCodeInValueSet next : theAllowedCodeInValueSet) { + if (!next.getResourceName().equals(theResDef.getName())) { + continue; + } + + String paramName; + if (next.isNegate()) { + paramName = next.getSearchParameterName() + Constants.PARAMQUALIFIER_TOKEN_NOT_IN; + } else { + paramName = next.getSearchParameterName() + Constants.PARAMQUALIFIER_TOKEN_IN; + } + + if (retVal == null) { + retVal = new HashMap<>(); + } + retVal.computeIfAbsent(paramName, k->new ArrayList<>()).add(next.getValueSetUrl()); + } + + return retVal; + } + + + private String selectBestSearchParameterForCompartment(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, String compartmentName) { String searchParamName = null; @@ -333,4 +363,34 @@ public class SearchNarrowingInterceptor { } } + private class BundleEntryUrlProcessor implements Consumer { + private final FhirContext myFhirContext; + private final ServletRequestDetails myRequestDetails; + private final HttpServletRequest myRequest; + private final HttpServletResponse myResponse; + + public BundleEntryUrlProcessor(FhirContext theFhirContext, ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) { + myFhirContext = theFhirContext; + myRequestDetails = theRequestDetails; + myRequest = theRequest; + myResponse = theResponse; + } + + @Override + public void accept(ModifiableBundleEntry theModifiableBundleEntry) { + ArrayListMultimap paramValues = ArrayListMultimap.create(); + + String url = theModifiableBundleEntry.getRequestUrl(); + + ServletSubRequestDetails subServletRequestDetails = ServletRequestUtil.getServletSubRequestDetails(myRequestDetails, url, paramValues); + BaseMethodBinding method = subServletRequestDetails.getServer().determineResourceMethod(subServletRequestDetails, url); + RestOperationTypeEnum restOperationType = method.getRestOperationType(); + subServletRequestDetails.setRestOperationType(restOperationType); + + incomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse); + + theModifiableBundleEntry.setRequestUrl(myFhirContext, ServletRequestUtil.extractUrl(subServletRequestDetails)); + } + } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchParameterAndValueSetRuleImpl.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchParameterAndValueSetRuleImpl.java new file mode 100644 index 00000000000..553584315a2 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchParameterAndValueSetRuleImpl.java @@ -0,0 +1,163 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2022 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.context.support.ConceptValidationOptions; +import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.FhirTerser; +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.ICompositeType; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +class SearchParameterAndValueSetRuleImpl extends RuleImplOp { + + private String mySearchParameterName; + private String myValueSetUrl; + private boolean myWantCode; + + /** + * Constructor + * + * @param theRuleName The rule name + */ + SearchParameterAndValueSetRuleImpl(String theRuleName) { + super(theRuleName); + } + + void setWantCode(boolean theWantCode) { + myWantCode = theWantCode; + } + + public void setSearchParameterName(String theSearchParameterName) { + mySearchParameterName = theSearchParameterName; + } + + public void setValueSetUrl(String theValueSetUrl) { + myValueSetUrl = theValueSetUrl; + } + + + @Override + protected AuthorizationInterceptor.Verdict applyRuleLogic(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, Set theFlags, FhirContext theFhirContext, RuleTarget theRuleTarget, IRuleApplier theRuleApplier) { + // Sanity check + Validate.isTrue(theInputResource == null || theOutputResource == null); + + if (theInputResource != null) { + return applyRuleLogic(theFhirContext, theRequestDetails, theInputResource, theOperation, theInputResource, theInputResourceId, theOutputResource, theRuleApplier); + } + if (theOutputResource != null) { + return applyRuleLogic(theFhirContext, theRequestDetails, theOutputResource, theOperation, theInputResource, theInputResourceId, theOutputResource, theRuleApplier); + } + + // No resource present + if (theOperation == RestOperationTypeEnum.READ || theOperation == RestOperationTypeEnum.SEARCH_TYPE) { + return new AuthorizationInterceptor.Verdict(PolicyEnum.ALLOW, this); + } + + return null; + } + + private AuthorizationInterceptor.Verdict applyRuleLogic(FhirContext theFhirContext, RequestDetails theRequestDetails, IBaseResource theResource, RestOperationTypeEnum theOperation, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier) { + IValidationSupport validationSupport = theRuleApplier.getValidationSupport(); + if (validationSupport == null) { + validationSupport = theFhirContext.getValidationSupport(); + } + + FhirTerser terser = theFhirContext.newTerser(); + ConceptValidationOptions conceptValidationOptions = new ConceptValidationOptions(); + ValidationSupportContext validationSupportContext = new ValidationSupportContext(validationSupport); + + RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource); + RuntimeSearchParam searchParameter = resourceDefinition.getSearchParam(mySearchParameterName); + if (searchParameter == null) { + throw new InternalErrorException(Msg.code(2025) + "Unknown SearchParameter for resource " + resourceDefinition.getName() + ": " + mySearchParameterName); + } + + theRuleApplier + .getTroubleshootingLog() + .debug("Applying {}:{} rule for valueSet: {}", mySearchParameterName, myWantCode ? "in" : "not-in", myValueSetUrl); + + List paths = searchParameter.getPathsSplitForResourceType(resourceDefinition.getName()); + + for (String nextPath : paths) { + List foundCodeableConcepts = theFhirContext.newFhirPath().evaluate(theResource, nextPath, ICompositeType.class); + int codeCount = 0; + int matchCount = 0; + for (ICompositeType nextCodeableConcept : foundCodeableConcepts) { + for (IBase nextCoding : terser.getValues(nextCodeableConcept, "coding")) { + String system = terser.getSinglePrimitiveValueOrNull(nextCoding, "system"); + String code = terser.getSinglePrimitiveValueOrNull(nextCoding, "code"); + if (isNotBlank(system) && isNotBlank(code)) { + codeCount++; + IValidationSupport.CodeValidationResult validateCodeResult = validationSupport.validateCode(validationSupportContext, conceptValidationOptions, system, code, null, myValueSetUrl); + if (validateCodeResult != null) { + if (validateCodeResult.isOk()) { + if (myWantCode) { + AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); + theRuleApplier + .getTroubleshootingLog() + .debug("Code {}:{} was found in VS - Verdict: {}", system, code, verdict); + return verdict; + } else { + matchCount++; + break; + } + } else { + theRuleApplier + .getTroubleshootingLog() + .debug("Code {}:{} was not found in VS", system, code); + } + } + } + } + } + + if (!myWantCode) { + if ((getMode() == PolicyEnum.ALLOW && matchCount == 0) || + (getMode() == PolicyEnum.DENY && matchCount < codeCount)) { + AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); + theRuleApplier + .getTroubleshootingLog() + .debug("Code was found in VS - Verdict: {}", verdict); + return verdict; + } + } + + } + + return null; + } +} 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 c26ba1dfb9c..50f10597922 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 d348f8e7312..5f3cd6127bc 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 dec787e158c..693997f1ffd 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index f0ca866b3fd..211aea0f8fd 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index ac6b8013a32..2e5872c9ef2 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT hapi-fhir-spring-boot-samples diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index f45e5420e0c..cdd2978703a 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index cf352b6bc1f..83bf2e8fab0 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index 380bcdf5932..e38d63cad64 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index 4290c6f6201..881d45e64ba 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 247bab6f17e..dc168caf90e 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 270d8685b0a..e71e4a560e5 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 2daf0ad3561..9b40427a8c5 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 b963aac7771..8d0640f630b 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 71a77b6f67a..09dbde43929 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java index e3b76672911..9c1413a3ad8 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java @@ -11,16 +11,22 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.gclient.ICriterion; +import ca.uhn.fhir.rest.gclient.TokenClientParam; import ca.uhn.fhir.rest.param.BaseAndListParam; import ca.uhn.fhir.rest.param.ReferenceAndListParam; import ca.uhn.fhir.rest.param.StringAndListParam; import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.test.utilities.JettyUtil; import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.UrlUtil; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -40,9 +46,14 @@ import org.slf4j.LoggerFactory; import java.net.URLEncoder; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import static ca.uhn.fhir.util.UrlUtil.escapeUrlParam; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -62,7 +73,7 @@ public class SearchNarrowingInterceptorTest { private static List ourReturn; private static Server ourServer; private static IGenericClient ourClient; - private static AuthorizedList ourNextCompartmentList; + private static AuthorizedList ourNextAuthorizedList; private static Bundle.BundleEntryRequestComponent ourLastBundleRequest; @@ -76,13 +87,13 @@ public class SearchNarrowingInterceptorTest { ourLastPatientParam = null; ourLastPerformerParam = null; ourLastCodeParam = null; - ourNextCompartmentList = null; + ourNextAuthorizedList = null; } @Test public void testReturnNull() { - ourNextCompartmentList = null; + ourNextAuthorizedList = null; ourClient .search() @@ -98,9 +109,122 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedNoParams() { + public void testNarrowCode_NotInSelected_ClientRequestedNoParams() { + ourNextAuthorizedList = new AuthorizedList() + .addCodeNotInValueSet("Observation", "code", "http://myvs"); - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourClient + .search() + .forResource("Observation") + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertEquals(1, ourLastCodeParam.size()); + assertEquals(1, ourLastCodeParam.getValuesAsQueryTokens().get(0).size()); + assertEquals(TokenParamModifier.NOT_IN, ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getModifier()); + assertEquals("http://myvs", ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(null, ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + assertNull(ourLastIdParam); + } + + + @Test + public void testNarrowCode_InSelected_ClientRequestedNoParams() { + ourNextAuthorizedList = new AuthorizedList() + .addCodeInValueSet("Observation", "code", "http://myvs"); + + ourClient + .search() + .forResource("Observation") + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertEquals(1, ourLastCodeParam.size()); + assertEquals(1, ourLastCodeParam.getValuesAsQueryTokens().get(0).size()); + assertEquals(TokenParamModifier.IN, ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getModifier()); + assertEquals("http://myvs", ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(null, ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + assertNull(ourLastIdParam); + } + + @Test + public void testNarrowCode_InSelected_ClientRequestedBundleWithNoParams() { + ourNextAuthorizedList = new AuthorizedList() + .addCodeInValueSet("Observation", "code", "http://myvs"); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Observation?subject=Patient/123"); + ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); + + ourClient + .transaction() + .withBundle(bundle) + .execute(); + + assertEquals("transaction", ourLastHitMethod); + String expectedUrl = "Observation?" + + escapeUrlParam("code:in") + + "=" + + escapeUrlParam("http://myvs") + + "&subject=" + + escapeUrlParam("Patient/123"); + assertEquals(expectedUrl, ourLastBundleRequest.getUrl()); + + } + + @Test + public void testNarrowCode_InSelected_ClientRequestedOtherInParam() { + ourNextAuthorizedList = new AuthorizedList() + .addCodeInValueSet("Observation", "code", "http://myvs"); + + ourClient.registerInterceptor(new LoggingInterceptor(false)); + ourClient + .search() + .forResource("Observation") + .where(singletonMap("code", singletonList(new TokenParam("http://othervs").setModifier(TokenParamModifier.IN)))) + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertEquals(2, ourLastCodeParam.size()); + assertEquals(1, ourLastCodeParam.getValuesAsQueryTokens().get(0).size()); + assertEquals(TokenParamModifier.IN, ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getModifier()); + assertEquals("http://othervs", ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(null, ourLastCodeParam.getValuesAsQueryTokens().get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals(1, ourLastCodeParam.getValuesAsQueryTokens().get(1).size()); + assertEquals(TokenParamModifier.IN, ourLastCodeParam.getValuesAsQueryTokens().get(1).getValuesAsQueryTokens().get(0).getModifier()); + assertEquals("http://myvs", ourLastCodeParam.getValuesAsQueryTokens().get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(null, ourLastCodeParam.getValuesAsQueryTokens().get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + assertNull(ourLastIdParam); + } + + @Test + public void testNarrowCode_InSelected_DifferentResource() { + ourNextAuthorizedList = new AuthorizedList() + .addCodeInValueSet("Procedure", "code", "http://myvs"); + + ourClient + .search() + .forResource("Observation") + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertEquals(null, ourLastCodeParam); + } + + @Test + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedNoParams() { + ourNextAuthorizedList = new AuthorizedList() + .addCompartments("Patient/123", "Patient/456"); ourClient .search() @@ -116,9 +240,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedBundleNoParams() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedBundleNoParams() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); Bundle bundle = new Bundle(); bundle.setType(Bundle.BundleType.TRANSACTION); @@ -134,49 +258,10 @@ public class SearchNarrowingInterceptorTest { assertEquals("Patient?_id=" + URLEncoder.encode("Patient/123,Patient/456"), ourLastBundleRequest.getUrl()); } - /** - * Should not make any changes - */ @Test - public void testNarrowObservationsByPatientResources_ClientRequestedNoParams() { + public void testNarrowCompartment_PatientByPatientContext_ClientRequestedNoParams() { - ourNextCompartmentList = new AuthorizedList().addResources("Patient/123", "Patient/456"); - - ourClient - .search() - .forResource("Observation") - .execute(); - - assertEquals("Observation.search", ourLastHitMethod); - assertNull(ourLastIdParam); - assertNull(ourLastCodeParam); - assertNull(ourLastSubjectParam); - assertNull(ourLastPerformerParam); - assertNull(ourLastPatientParam); - } - - @Test - public void testNarrowPatientByPatientResources_ClientRequestedNoParams() { - - ourNextCompartmentList = new AuthorizedList().addResources("Patient/123", "Patient/456"); - - ourClient - .search() - .forResource("Patient") - .execute(); - - assertEquals("Patient.search", ourLastHitMethod); - assertNull(ourLastCodeParam); - assertNull(ourLastSubjectParam); - assertNull(ourLastPerformerParam); - assertNull(ourLastPatientParam); - assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456")); - } - - @Test - public void testNarrowPatientByPatientContext_ClientRequestedNoParams() { - - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); ourClient .search() @@ -189,9 +274,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowPatientByPatientContext_ClientRequestedSomeOverlap() { + public void testNarrowCompartment_PatientByPatientContext_ClientRequestedSomeOverlap() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); ourClient .search() @@ -205,9 +290,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedSomeOverlap() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedSomeOverlap() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); ourClient .search() @@ -225,9 +310,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedSomeOverlap_ShortIds() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedSomeOverlap_ShortIds() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); ourClient .search() @@ -245,9 +330,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedSomeOverlap_UseSynonym() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedSomeOverlap_UseSynonym() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); ourClient .search() @@ -265,9 +350,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedNoOverlap() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedNoOverlap() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); try { ourClient @@ -286,9 +371,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedNoOverlap_UseSynonym() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedNoOverlap_UseSynonym() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); try { ourClient @@ -307,9 +392,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedBadParameter() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedBadParameter() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); try { ourClient @@ -327,9 +412,9 @@ public class SearchNarrowingInterceptorTest { } @Test - public void testNarrowObservationsByPatientContext_ClientRequestedBadPermission() { + public void testNarrowCompartment_ObservationsByPatientContext_ClientRequestedBadPermission() { - ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/"); + ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/"); try { ourClient @@ -346,6 +431,45 @@ public class SearchNarrowingInterceptorTest { assertNull(ourLastHitMethod); } + /** + * Should not make any changes + */ + @Test + public void testNarrowResources_ObservationsByPatientResources_ClientRequestedNoParams() { + ourNextAuthorizedList = new AuthorizedList() + .addResources("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Observation") + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertNull(ourLastIdParam); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + } + + @Test + public void testNarrowResources_PatientByPatientResources_ClientRequestedNoParams() { + ourNextAuthorizedList = new AuthorizedList() + .addResources("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Patient") + .execute(); + + assertEquals("Patient.search", ourLastHitMethod); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456")); + } + private List toStrings(BaseAndListParam> theParams) { List> valuesAsQueryTokens = theParams.getValuesAsQueryTokens(); @@ -393,7 +517,7 @@ public class SearchNarrowingInterceptorTest { @OptionalParam(name = Observation.SP_SUBJECT) ReferenceAndListParam theSubjectParam, @OptionalParam(name = Observation.SP_PATIENT) ReferenceAndListParam thePatientParam, @OptionalParam(name = Observation.SP_PERFORMER) ReferenceAndListParam thePerformerParam, - @OptionalParam(name = "code") TokenAndListParam theCodeParam + @OptionalParam(name = Observation.SP_CODE) TokenAndListParam theCodeParam ) { ourLastHitMethod = "Observation.search"; ourLastIdParam = theIdParam; @@ -418,10 +542,10 @@ public class SearchNarrowingInterceptorTest { private static class MySearchNarrowingInterceptor extends SearchNarrowingInterceptor { @Override protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) { - if (ourNextCompartmentList == null) { + if (ourNextAuthorizedList == null) { return null; } - return ourNextCompartmentList; + return ourNextAuthorizedList; } } diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index f472b3dedbe..d6ea85eec2d 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 9e1f2a9e2e4..4e1cafe5e45 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 0c9eeeccc56..703e7f5f7e4 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index 5ba61cb176e..9408b7603a8 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 35305bc5a1a..59d09106547 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 69f6e9dd6c8..ee9a1a63155 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 df9025e2f9d..261ef4de0e9 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 c82895326db..084d2c8299d 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 4e3f7570262..a0229aaa4f7 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../hapi-deployable-pom/pom.xml 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 c7ab4c296b7..f56d748575c 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 @@ -5,6 +5,7 @@ import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.TranslateConceptResults; import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.context.support.ValueSetExpansionOptions; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.apache.commons.lang3.concurrent.BasicThreadFactory; @@ -22,11 +23,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; 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; @@ -35,6 +36,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; 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; @@ -42,6 +44,7 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple private final Cache myLookupCodeCache; private final ThreadPoolExecutor myBackgroundExecutor; private final Map myNonExpiringCache; + private final Cache myExpandValueSetCache; /** * Constuctor with default timeouts @@ -60,6 +63,11 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple */ public CachingValidationSupport(IValidationSupport theWrap, CacheTimeouts theCacheTimeouts) { super(theWrap.getFhirContext(), theWrap); + myExpandValueSetCache = Caffeine + .newBuilder() + .expireAfterWrite(theCacheTimeouts.getExpandValueSetMillis(), TimeUnit.MILLISECONDS) + .maximumSize(100) + .build(); myValidateCodeCache = Caffeine .newBuilder() .expireAfterWrite(theCacheTimeouts.getValidateCodeMillis(), TimeUnit.MILLISECONDS) @@ -146,6 +154,22 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple 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(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultIfBlank(theValueSetUrl, "NO_VS"); @@ -215,7 +239,7 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple retVal = (T) myNonExpiringCache.get(theKey); if (retVal != null) { - Runnable loaderTask = ()->{ + Runnable loaderTask = () -> { T loadedItem = loadFromCache(theCache, theKey, theLoader); myNonExpiringCache.put(theKey, loadedItem); }; @@ -233,6 +257,7 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple @Override public void invalidateCaches() { + myExpandValueSetCache.invalidateAll(); myLookupCodeCache.invalidateAll(); myCache.invalidateAll(); myValidateCodeCache.invalidateAll(); @@ -248,6 +273,16 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple private long myLookupCodeMillis; private long myValidateCodeMillis; private long myMiscMillis; + private long myExpandValueSetMillis; + + public long getExpandValueSetMillis() { + return myExpandValueSetMillis; + } + + public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) { + myExpandValueSetMillis = theExpandValueSetMillis; + return this; + } public long getTranslateCodeMillis() { return myTranslateCodeMillis; @@ -288,6 +323,7 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple public static CacheTimeouts defaultValues() { return new CacheTimeouts() .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) + .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE) .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE) .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java similarity index 91% rename from hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java rename to hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java index 7e0bb9c224e..4a7397bc0da 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java @@ -112,6 +112,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; public class AuthorizationInterceptorR4Test { @@ -162,7 +163,7 @@ public class AuthorizationInterceptorR4Test { return new StringEntity(out, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")); } - private Resource createObservation(Integer theId, String theSubjectId) { + private Observation createObservation(Integer theId, String theSubjectId) { Observation retVal = new Observation(); if (theId != null) { retVal.setId(new IdType("Observation", (long) theId)); @@ -791,6 +792,431 @@ public class AuthorizationInterceptorR4Test { assertEquals(403, status.getStatusLine().getStatusCode()); } + @Test + public void testCodeIn_Search_BanList() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .deny("Rule 1").read().resourcesOfType("Observation").withCodeInValueSet("code", "http://hl7.org/fhir/ValueSet/administrative-gender").andThen() + .allowAll() + .build(); + } + }); + + HttpGet httpGet; + String response; + Observation observation; + CloseableHttpResponse status; + + // Banned code present + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by rule: Rule 1")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Acceptable code present + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Both Unacceptable and Acceptable code present + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by rule: Rule 1")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + + @Test + public void testCodeIn_Search_AllowList() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().resourcesOfType("Observation").withCodeInValueSet("code", "http://hl7.org/fhir/ValueSet/administrative-gender").andThen() + .build(); + } + }); + + HttpGet httpGet; + String response; + Observation observation; + CloseableHttpResponse status; + + // Allowed code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // No acceptable code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Both Unacceptable and Acceptable code present + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Allowed code present - Search + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // No acceptable code present - Search + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + + @Test + public void testCodeNotIn_AllowSearch() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().resourcesOfType("Observation").withCodeNotInValueSet("code", "http://hl7.org/fhir/ValueSet/administrative-gender").andThen() + .build(); + } + }); + + HttpGet httpGet; + String response; + Observation observation; + CloseableHttpResponse status; + + // Allowed code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // No acceptable code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Both Unacceptable and Acceptable code present - Should not pass since one of the codes is in the VS + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + @Test + public void testCodeNotIn_DenySearch() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .deny("Rule 1").read().resourcesOfType("Observation").withCodeNotInValueSet("code", "http://hl7.org/fhir/ValueSet/administrative-gender").andThen() + .allowAll() + .build(); + } + }); + + HttpGet httpGet; + String response; + Observation observation; + CloseableHttpResponse status; + + // Allowed code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // No acceptable code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by rule: Rule 1")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Both Unacceptable and Acceptable code present - Should not pass since one of the codes is in the VS + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("foo"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + + @Test + public void testCodeIn_TransactionCreate() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow().transaction().withAnyOperation().andApplyNormalRules().andThen() + .deny("Rule 1").write().resourcesOfType("Observation").withCodeInValueSet("code", "http://hl7.org/fhir/ValueSet/administrative-gender").andThen() + .allowAll() + .build(); + } + }); + + HttpPost httpPost; + String response; + Observation observation; + CloseableHttpResponse status; + + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + + Bundle input = new Bundle(); + input.setType(Bundle.BundleType.TRANSACTION); + input + .addEntry() + .setResource(observation) + .getRequest() + .setUrl("/Observation") + .setMethod(Bundle.HTTPVerb.POST); + + Bundle output = new Bundle(); + output.setType(Bundle.BundleType.TRANSACTIONRESPONSE); + output.addEntry().setResource(createPatient(1)); + + // Transaction with resource containing banned code + ourReturn = Collections.singletonList(output); + ourHitMethod = false; + httpPost = new HttpPost("http://localhost:" + ourPort + "/"); + httpPost.setEntity(createFhirResourceEntity(input)); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by rule: Rule 1")); + assertEquals(403, status.getStatusLine().getStatusCode()); + + // Transaction with resource containing acceptable code + observation.getCode().getCoding().clear(); + observation.getCode().addCoding().setSystem("http://foo").setCode("bar"); + ourReturn = Collections.singletonList(output); + ourHitMethod = false; + httpPost = new HttpPost("http://localhost:" + ourPort + "/"); + httpPost.setEntity(createFhirResourceEntity(input)); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + + } + + @Test + public void testCodeIn_InvalidSearchParam() throws IOException { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().resourcesOfType("Observation").withCodeInValueSet("blah", "http://hl7.org/fhir/ValueSet/administrative-gender").andThen() + .build(); + } + }); + + HttpGet httpGet; + String response; + Observation observation; + CloseableHttpResponse status; + + // Allowed code present - Read + ourHitMethod = false; + observation = createObservation(10, "Patient/2"); + observation + .getCode() + .addCoding() + .setSystem("http://hl7.org/fhir/administrative-gender") + .setCode("male"); + ourReturn = Collections.singletonList(observation); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(500, status.getStatusLine().getStatusCode()); + assertThat(response, containsString("HAPI-2025: Unknown SearchParameter for resource Observation: blah")); + assertTrue(ourHitMethod); + } + + @Test public void testBatchWhenTransactionWrongBundleType() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 6d1979afda2..0e50f90e848 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml @@ -58,37 +58,37 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r5 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT org.apache.velocity diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index ac1d80be78b..15c667c9414 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index e6b0aca0ff1..ac0fbcfa8b2 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io @@ -2010,7 +2010,7 @@ ca.uhn.hapi.fhir hapi-fhir-checkstyle - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index c5b727e41c3..550d6f26cd3 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 193082dfaaf..85e369a4607 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-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 bead7e3e3fc..6f7dd96ae9d 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.0.0-PRE1-SNAPSHOT + 6.0.0-PRE2-SNAPSHOT ../../pom.xml