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 extends IQueryParameterOr>> theParams) {
List extends IQueryParameterOr extends IQueryParameterType>> 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