Apply SearchNarrowingInterceptor to conditional URLs (#5712)
* Fix #5110 - Failure in tx processing * Test fix * Work on narrowins * Add changelog * Docs cleanup * Fix compile error * Rollback incompatible change * Test fix * Test fix * Force update * Test fixes * Build fixes * Bump HTMLUnit * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5712-apply-searchnarrowing-to-conditional-urls.yaml Co-authored-by: Ken Stevens <khstevens@gmail.com> * Address review comments * Version bump to 7.1.4-SNAPSHOT * Spotless * Roll back version --------- Co-authored-by: Ken Stevens <khstevens@gmail.com>
This commit is contained in:
parent
c7f413d6ea
commit
ec525f4457
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -627,6 +627,8 @@ public class BundleUtil {
|
|||
//noinspection EnumSwitchStatementWhichMissesCases
|
||||
switch (requestType) {
|
||||
case PUT:
|
||||
case DELETE:
|
||||
case PATCH:
|
||||
conditionalUrl = url != null && url.contains("?") ? url : null;
|
||||
break;
|
||||
case POST:
|
||||
|
|
|
@ -315,6 +315,7 @@ public class UrlUtil {
|
|||
return theCtx.getResourceDefinition(resourceName);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Map<String, String[]> parseQueryString(String theQueryString) {
|
||||
HashMap<String, List<String>> map = new HashMap<>();
|
||||
parseQueryString(theQueryString, map);
|
||||
|
|
|
@ -69,4 +69,12 @@ public class BundleEntryMutator {
|
|||
BaseRuntimeChildDefinition resourceChild = myEntryDefinition.getChildByName("resource");
|
||||
resourceChild.getMutator().setValue(myEntry, theUpdatedResource);
|
||||
}
|
||||
|
||||
public void setRequestIfNoneExist(FhirContext theFhirContext, String theIfNoneExist) {
|
||||
BaseRuntimeChildDefinition requestUrlChildDef = myRequestChildContentsDef.getChildByName("ifNoneExist");
|
||||
IPrimitiveType<?> url = ParametersUtil.createString(theFhirContext, theIfNoneExist);
|
||||
for (IBase nextRequest : myRequestChildDef.getAccessor().getValues(myEntry)) {
|
||||
requestUrlChildDef.getMutator().addValue(nextRequest, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
package ca.uhn.fhir.util.bundle;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
public class ModifiableBundleEntry {
|
||||
|
@ -58,4 +59,16 @@ public class ModifiableBundleEntry {
|
|||
public void setResource(IBaseResource theUpdatedResource) {
|
||||
myBundleEntryMutator.setResource(theUpdatedResource);
|
||||
}
|
||||
|
||||
public RequestTypeEnum getRequestMethod() {
|
||||
return myBundleEntryParts.getRequestType();
|
||||
}
|
||||
|
||||
public String getConditionalUrl() {
|
||||
return myBundleEntryParts.getConditionalUrl();
|
||||
}
|
||||
|
||||
public void setRequestIfNoneExist(FhirContext theFhirContext, String theIfNoneExist) {
|
||||
myBundleEntryMutator.setRequestIfNoneExist(theFhirContext, theIfNoneExist);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-bom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
<name>HAPI FHIR BOM</name>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-cli</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -55,13 +55,13 @@ import java.util.List;
|
|||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
/**
|
||||
* Examples integrated into our documentation.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "WriteOnlyObject", "UnnecessaryLocalVariable"})
|
||||
public class AuthorizationInterceptors {
|
||||
|
||||
public class PatientResourceProvider implements IResourceProvider {
|
||||
public static class PatientResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
|
@ -74,8 +74,8 @@ public class AuthorizationInterceptors {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"ConstantConditions", "InnerClassMayBeStatic"})
|
||||
// START SNIPPET: patientAndAdmin
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public class PatientAndAdminAuthorizationInterceptor extends AuthorizationInterceptor {
|
||||
|
||||
@Override
|
||||
|
@ -265,6 +265,7 @@ public class AuthorizationInterceptors {
|
|||
|
||||
}
|
||||
|
||||
@SuppressWarnings("InnerClassMayBeStatic")
|
||||
// START SNIPPET: narrowing
|
||||
public class MyPatientSearchNarrowingInterceptor extends SearchNarrowingInterceptor {
|
||||
|
||||
|
@ -300,6 +301,13 @@ public class AuthorizationInterceptors {
|
|||
}
|
||||
// END SNIPPET: narrowing
|
||||
|
||||
public void narrowingConditional() {
|
||||
// START SNIPPET: narrowingConditional
|
||||
SearchNarrowingInterceptor interceptor = new SearchNarrowingInterceptor();
|
||||
interceptor.setNarrowConditionalUrls(true);
|
||||
// END SNIPPET: narrowingConditional
|
||||
}
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
public void rsNarrowing() {
|
||||
RestfulServer restfulServer = new RestfulServer();
|
||||
|
@ -330,6 +338,7 @@ public class AuthorizationInterceptors {
|
|||
// END SNIPPET: rsnarrowing
|
||||
}
|
||||
|
||||
@SuppressWarnings("InnerClassMayBeStatic")
|
||||
// START SNIPPET: narrowingByCode
|
||||
public class MyCodeSearchNarrowingInterceptor extends SearchNarrowingInterceptor {
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
type: add
|
||||
jira: SMILE-7971
|
||||
issue: 5712
|
||||
title: "The SearchNarrowingInterceptor can now optionally be configured to also apply
|
||||
URL narrowing to conditional URLs used by conditional create/update/delete/patch
|
||||
operations, both as raw HTTP transactions as well as within FHIR transaction
|
||||
Bundles."
|
|
@ -193,7 +193,7 @@ HAPI FHIR provides an interceptor that can be used to implement consent rules an
|
|||
|
||||
# Security: Search Narrowing
|
||||
|
||||
HAPI FHIR provides an interceptor that can be used to implement consent rules and directives. See [Consent Interceptor](/docs/security/consent_interceptor.html) for more information.
|
||||
HAPI FHIR provides an interceptor that can be used to implement consent rules and directives. See [Search Narrowing Interceptor](/docs/security/search_narrowing_interceptor.html) for more information.
|
||||
|
||||
|
||||
# Security: Rejecting Unsupported HTTP Verbs
|
||||
|
|
|
@ -25,6 +25,24 @@ An example of this interceptor follows:
|
|||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowing}}
|
||||
```
|
||||
|
||||
# Narrowing Conditional URLs
|
||||
|
||||
By default, this interceptor will narrow URLs for FHIR search operations only. The
|
||||
interceptor can also be configured to narrow URLs on conditional operations.
|
||||
|
||||
When this feature is enabled request URLs are also narrowed for the following FHIR operations:
|
||||
|
||||
* Conditional Create (The `If-None-Exist` header is narrowed)
|
||||
* Conditional Update (The request URL is narrowed if it is a conditional URL)
|
||||
* Conditional Delete (The request URL is narrowed if it is a conditional URL)
|
||||
* Conditional Patch (The request URL is narrowed if it is a conditional URL)
|
||||
|
||||
The following example shows how to enable conditional URL narrowing on the interceptor.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowingConditional}}
|
||||
```
|
||||
|
||||
<a name="constraining-by-valueset-membership"/>
|
||||
|
||||
# Constraining by ValueSet Membership
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -117,6 +117,16 @@ public class JaxRsRequest extends RequestDetails {
|
|||
return requestHeader == null ? Collections.<String>emptyList() : requestHeader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHeader(String theName, String theValue) {
|
||||
throw new UnsupportedOperationException(Msg.code(2499) + "Headers can not be modified in JAX-RS");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeaders(String theName, List<String> theValue) {
|
||||
throw new UnsupportedOperationException(Msg.code(2500) + "Headers can not be modified in JAX-RS");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String theAttributeName) {
|
||||
return myAttributes.get(theAttributeName);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -567,12 +567,13 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
|
|||
MyAnonymousInterceptor1 interceptor1 = new MyAnonymousInterceptor1();
|
||||
ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED, interceptor1);
|
||||
MySearchNarrowingInterceptor interceptor2 = new MySearchNarrowingInterceptor();
|
||||
interceptor2.setNarrowConditionalUrls(true);
|
||||
ourRestServer.getInterceptorService().registerInterceptor(interceptor2);
|
||||
try {
|
||||
myClient.transaction().withBundle(input).execute();
|
||||
assertEquals(1, counter0.get());
|
||||
assertEquals(1, counter1.get());
|
||||
assertEquals(5, counter2.get());
|
||||
assertEquals(1, counter2.get());
|
||||
|
||||
} finally {
|
||||
ourRestServer.getInterceptorService().unregisterInterceptor(interceptor1);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -249,6 +249,24 @@ public abstract class RequestDetails {
|
|||
|
||||
public abstract List<String> getHeaders(String name);
|
||||
|
||||
/**
|
||||
* Adds a new header
|
||||
*
|
||||
* @param theName The header name
|
||||
* @param theValue The header value
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public abstract void addHeader(String theName, String theValue);
|
||||
|
||||
/**
|
||||
* Replaces any existing header(s) with the given name using a List of new header values
|
||||
*
|
||||
* @param theName The header name
|
||||
* @param theValue The header value
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public abstract void setHeaders(String theName, List<String> theValue);
|
||||
|
||||
public IIdType getId() {
|
||||
return myId;
|
||||
}
|
||||
|
|
|
@ -33,9 +33,9 @@ import ca.uhn.fhir.rest.server.IPagingProvider;
|
|||
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -124,13 +124,27 @@ public class SystemRequestDetails extends RequestDetails {
|
|||
return headers.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHeader(String theName, String theValue) {
|
||||
if (myHeaders == null) {
|
||||
myHeaders = ArrayListMultimap.create();
|
||||
}
|
||||
initHeaderMap();
|
||||
myHeaders.put(theName, theValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeaders(String theName, List<String> theValues) {
|
||||
initHeaderMap();
|
||||
myHeaders.putAll(theName, theValues);
|
||||
}
|
||||
|
||||
private void initHeaderMap() {
|
||||
if (myHeaders == null) {
|
||||
// Make sure we are case-insensitive on keys
|
||||
myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
|
||||
.arrayListValues()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String theAttributeName) {
|
||||
return null;
|
||||
|
@ -145,7 +159,7 @@ public class SystemRequestDetails extends RequestDetails {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Reader getReader() throws IOException {
|
||||
public Reader getReader() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Server Framework
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
/**
|
||||
* Request object for {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_CONDITIONAL_URL_PREPROCESS}
|
||||
*/
|
||||
public class ConditionalUrlRequest {}
|
|
@ -0,0 +1,25 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Server Framework
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
/**
|
||||
* Request object for {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_CONDITIONAL_URL_PREPROCESS}
|
||||
*/
|
||||
public class ConditionalUrlResponse {}
|
|
@ -30,21 +30,21 @@ import ca.uhn.fhir.interceptor.api.Hook;
|
|||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||
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.FhirTerser;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import ca.uhn.fhir.util.ValidateUtil;
|
||||
import ca.uhn.fhir.util.bundle.ModifiableBundleEntry;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
@ -65,6 +65,8 @@ import java.util.Set;
|
|||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/**
|
||||
* This interceptor can be used to automatically narrow the scope of searches in order to
|
||||
* automatically restrict the searches to specific compartments.
|
||||
|
@ -88,12 +90,26 @@ import java.util.stream.Collectors;
|
|||
*
|
||||
* @see AuthorizationInterceptor
|
||||
*/
|
||||
@SuppressWarnings("JavadocLinkAsPlainText")
|
||||
public class SearchNarrowingInterceptor {
|
||||
|
||||
public static final String POST_FILTERING_LIST_ATTRIBUTE_NAME =
|
||||
SearchNarrowingInterceptor.class.getName() + "_POST_FILTERING_LIST";
|
||||
private IValidationSupport myValidationSupport;
|
||||
private int myPostFilterLargeValueSetThreshold = 500;
|
||||
private boolean myNarrowConditionalUrls;
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (default is {@literal false}), conditional URLs such
|
||||
* as the If-None-Exist header used for Conditional Create operations will
|
||||
* also be narrowed.
|
||||
*
|
||||
* @param theNarrowConditionalUrls Should we narrow conditional URLs in requests
|
||||
* @since 7.2.0
|
||||
*/
|
||||
public void setNarrowConditionalUrls(boolean theNarrowConditionalUrls) {
|
||||
myNarrowConditionalUrls = theNarrowConditionalUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies a threshold over which any ValueSet-based rules will be applied by
|
||||
|
@ -126,6 +142,68 @@ public class SearchNarrowingInterceptor {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles narrowing for FHIR search/create/update/patch operations.
|
||||
*
|
||||
* @see #hookIncomingRequestPreHandled(ServletRequestDetails, HttpServletRequest, HttpServletResponse) This method narrows FHIR transaction bundles
|
||||
*/
|
||||
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
|
||||
@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
|
||||
public void hookIncomingRequestPostProcessed(
|
||||
RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
|
||||
throws AuthenticationException {
|
||||
|
||||
// We don't support this operation type yet
|
||||
RestOperationTypeEnum restOperationType = theRequestDetails.getRestOperationType();
|
||||
Validate.isTrue(restOperationType != RestOperationTypeEnum.SEARCH_SYSTEM);
|
||||
|
||||
switch (restOperationType) {
|
||||
case EXTENDED_OPERATION_INSTANCE:
|
||||
case EXTENDED_OPERATION_TYPE: {
|
||||
if ("$everything".equals(theRequestDetails.getOperation())) {
|
||||
narrowEverythingOperation(theRequestDetails);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SEARCH_TYPE:
|
||||
narrowTypeSearch(theRequestDetails);
|
||||
break;
|
||||
case CREATE:
|
||||
narrowIfNoneExistHeader(theRequestDetails);
|
||||
break;
|
||||
case DELETE:
|
||||
case UPDATE:
|
||||
case PATCH:
|
||||
narrowRequestUrl(theRequestDetails, restOperationType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method narrows FHIR transaction operations (because this pointcut
|
||||
* is called after the request body is parsed).
|
||||
*
|
||||
* @see #hookIncomingRequestPostProcessed(RequestDetails, HttpServletRequest, HttpServletResponse) This method narrows FHIR search/create/update/etc operations
|
||||
*/
|
||||
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
|
||||
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
|
||||
public void hookIncomingRequestPreHandled(
|
||||
ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
|
||||
throws AuthenticationException {
|
||||
|
||||
if (theRequestDetails.getRestOperationType() != null) {
|
||||
switch (theRequestDetails.getRestOperationType()) {
|
||||
case TRANSACTION:
|
||||
case BATCH:
|
||||
IBaseBundle bundle = (IBaseBundle) theRequestDetails.getResource();
|
||||
FhirContext ctx = theRequestDetails.getFhirContext();
|
||||
BundleEntryUrlProcessor processor = new BundleEntryUrlProcessor(ctx, theRequestDetails);
|
||||
BundleUtil.processEntries(ctx, bundle, processor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses should override this method to supply the set of compartments that
|
||||
* the user making the request should actually have access to.
|
||||
|
@ -143,54 +221,214 @@ public class SearchNarrowingInterceptor {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
|
||||
public boolean hookIncomingRequestPostProcessed(
|
||||
RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
|
||||
throws AuthenticationException {
|
||||
// We don't support this operation type yet
|
||||
Validate.isTrue(theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_SYSTEM);
|
||||
/**
|
||||
* For the $everything operation, we only do code narrowing, and in this case
|
||||
* we're not actually even making any changes to the request. All we do here is
|
||||
* ensure that an attribute is added to the request, which is picked up later
|
||||
* by {@link SearchNarrowingConsentService}.
|
||||
*/
|
||||
private void narrowEverythingOperation(RequestDetails theRequestDetails) {
|
||||
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
|
||||
if (authorizedList != null) {
|
||||
buildParameterListForAuthorizedCodes(
|
||||
theRequestDetails, theRequestDetails.getResourceName(), authorizedList);
|
||||
}
|
||||
}
|
||||
|
||||
private void narrowIfNoneExistHeader(RequestDetails theRequestDetails) {
|
||||
if (myNarrowConditionalUrls) {
|
||||
String ifNoneExist = theRequestDetails.getHeader(Constants.HEADER_IF_NONE_EXIST);
|
||||
if (isNotBlank(ifNoneExist)) {
|
||||
String newConditionalUrl = narrowConditionalUrlForCompartmentOnly(
|
||||
theRequestDetails, ifNoneExist, true, theRequestDetails.getResourceName());
|
||||
if (newConditionalUrl != null) {
|
||||
theRequestDetails.setHeaders(Constants.HEADER_IF_NONE_EXIST, List.of(newConditionalUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void narrowRequestUrl(RequestDetails theRequestDetails, RestOperationTypeEnum theRestOperationType) {
|
||||
if (myNarrowConditionalUrls) {
|
||||
String conditionalUrl = theRequestDetails.getConditionalUrl(theRestOperationType);
|
||||
if (isNotBlank(conditionalUrl)) {
|
||||
String newConditionalUrl = narrowConditionalUrlForCompartmentOnly(
|
||||
theRequestDetails, conditionalUrl, false, theRequestDetails.getResourceName());
|
||||
if (newConditionalUrl != null) {
|
||||
String newCompleteUrl = theRequestDetails
|
||||
.getCompleteUrl()
|
||||
.substring(
|
||||
0,
|
||||
theRequestDetails.getCompleteUrl().indexOf('?') + 1)
|
||||
+ newConditionalUrl;
|
||||
theRequestDetails.setCompleteUrl(newCompleteUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not narrow codes
|
||||
*/
|
||||
@Nullable
|
||||
private String narrowConditionalUrlForCompartmentOnly(
|
||||
RequestDetails theRequestDetails,
|
||||
@Nonnull String theConditionalUrl,
|
||||
boolean theIncludeUpToQuestionMarkInResponse,
|
||||
String theResourceName) {
|
||||
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
|
||||
return narrowConditionalUrl(
|
||||
theRequestDetails,
|
||||
theConditionalUrl,
|
||||
theIncludeUpToQuestionMarkInResponse,
|
||||
theResourceName,
|
||||
false,
|
||||
authorizedList);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String narrowConditionalUrl(
|
||||
RequestDetails theRequestDetails,
|
||||
@Nonnull String theConditionalUrl,
|
||||
boolean theIncludeUpToQuestionMarkInResponse,
|
||||
String theResourceName,
|
||||
boolean theNarrowCodes,
|
||||
AuthorizedList theAuthorizedList) {
|
||||
if (theAuthorizedList == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ListMultimap<String, String> parametersToAdd =
|
||||
buildParameterListForAuthorizedCompartment(theRequestDetails, theResourceName, theAuthorizedList);
|
||||
|
||||
if (theNarrowCodes) {
|
||||
ListMultimap<String, String> parametersToAddForCodes =
|
||||
buildParameterListForAuthorizedCodes(theRequestDetails, theResourceName, theAuthorizedList);
|
||||
if (parametersToAdd == null) {
|
||||
parametersToAdd = parametersToAddForCodes;
|
||||
} else if (parametersToAddForCodes != null) {
|
||||
parametersToAdd.putAll(parametersToAddForCodes);
|
||||
}
|
||||
}
|
||||
|
||||
String newConditionalUrl = null;
|
||||
if (parametersToAdd != null) {
|
||||
|
||||
String query = theConditionalUrl;
|
||||
int qMarkIndex = theConditionalUrl.indexOf('?');
|
||||
if (qMarkIndex != -1) {
|
||||
query = theConditionalUrl.substring(qMarkIndex + 1);
|
||||
}
|
||||
|
||||
Map<String, String[]> inputParams = UrlUtil.parseQueryString(query);
|
||||
Map<String, String[]> newParameters = applyCompartmentParameters(parametersToAdd, true, inputParams);
|
||||
|
||||
StringBuilder newUrl = new StringBuilder();
|
||||
if (theIncludeUpToQuestionMarkInResponse) {
|
||||
newUrl.append(qMarkIndex != -1 ? theConditionalUrl.substring(0, qMarkIndex + 1) : "?");
|
||||
}
|
||||
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String[]> nextEntry : newParameters.entrySet()) {
|
||||
for (String nextValue : nextEntry.getValue()) {
|
||||
if (isNotBlank(nextValue)) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
newUrl.append("&");
|
||||
}
|
||||
newUrl.append(UrlUtil.escapeUrlParam(nextEntry.getKey()));
|
||||
newUrl.append("=");
|
||||
newUrl.append(UrlUtil.escapeUrlParam(nextValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newConditionalUrl = newUrl.toString();
|
||||
}
|
||||
return newConditionalUrl;
|
||||
}
|
||||
|
||||
private void narrowTypeSearch(RequestDetails theRequestDetails) {
|
||||
|
||||
// N.B do not add code above this for filtering, this should only ever occur on search.
|
||||
if (shouldSkipNarrowing(theRequestDetails)) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
|
||||
if (authorizedList == null) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add rules to request so that the SearchNarrowingConsentService can pick them up
|
||||
String resourceName = theRequestDetails.getResourceName();
|
||||
|
||||
// Narrow request URL for compartments
|
||||
ListMultimap<String, String> parametersToAdd =
|
||||
buildParameterListForAuthorizedCompartment(theRequestDetails, resourceName, authorizedList);
|
||||
if (parametersToAdd != null) {
|
||||
applyParametersToRequestDetails(theRequestDetails, parametersToAdd, true);
|
||||
}
|
||||
|
||||
// Narrow request URL for codes - Add rules to request so that the SearchNarrowingConsentService can pick them
|
||||
// up
|
||||
ListMultimap<String, String> parameterToOrValues =
|
||||
buildParameterListForAuthorizedCodes(theRequestDetails, resourceName, authorizedList);
|
||||
if (parameterToOrValues != null) {
|
||||
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ListMultimap<String, String> buildParameterListForAuthorizedCodes(
|
||||
RequestDetails theRequestDetails, String resourceName, AuthorizedList authorizedList) {
|
||||
List<AllowedCodeInValueSet> postFilteringList = getPostFilteringList(theRequestDetails);
|
||||
if (authorizedList.getAllowedCodeInValueSets() != null) {
|
||||
postFilteringList.addAll(authorizedList.getAllowedCodeInValueSets());
|
||||
}
|
||||
|
||||
List<AllowedCodeInValueSet> allowedCodeInValueSet = authorizedList.getAllowedCodeInValueSets();
|
||||
ListMultimap<String, String> parameterToOrValues = null;
|
||||
if (allowedCodeInValueSet != null) {
|
||||
FhirContext context = theRequestDetails.getServer().getFhirContext();
|
||||
RuntimeResourceDefinition resourceDef = context.getResourceDefinition(resourceName);
|
||||
parameterToOrValues = processAllowedCodes(resourceDef, allowedCodeInValueSet);
|
||||
}
|
||||
return parameterToOrValues;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ListMultimap<String, String> buildParameterListForAuthorizedCompartment(
|
||||
RequestDetails theRequestDetails, String theResourceName, @Nullable AuthorizedList theAuthorizedList) {
|
||||
if (theAuthorizedList == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
|
||||
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName());
|
||||
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theResourceName);
|
||||
|
||||
/*
|
||||
* Create a map of search parameter values that need to be added to the
|
||||
* given request
|
||||
*/
|
||||
Collection<String> compartments = authorizedList.getAllowedCompartments();
|
||||
Collection<String> compartments = theAuthorizedList.getAllowedCompartments();
|
||||
ListMultimap<String, String> parametersToAdd = null;
|
||||
if (compartments != null) {
|
||||
Map<String, List<String>> parameterToOrValues =
|
||||
processResourcesOrCompartments(theRequestDetails, resDef, compartments, true);
|
||||
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true);
|
||||
}
|
||||
Collection<String> resources = authorizedList.getAllowedInstances();
|
||||
if (resources != null) {
|
||||
Map<String, List<String>> parameterToOrValues =
|
||||
processResourcesOrCompartments(theRequestDetails, resDef, resources, false);
|
||||
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true);
|
||||
}
|
||||
List<AllowedCodeInValueSet> allowedCodeInValueSet = authorizedList.getAllowedCodeInValueSets();
|
||||
if (allowedCodeInValueSet != null) {
|
||||
Map<String, List<String>> parameterToOrValues = processAllowedCodes(resDef, allowedCodeInValueSet);
|
||||
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, false);
|
||||
parametersToAdd =
|
||||
processResourcesOrCompartments(theRequestDetails, resDef, compartments, true, theResourceName);
|
||||
}
|
||||
|
||||
return true;
|
||||
Collection<String> resources = theAuthorizedList.getAllowedInstances();
|
||||
if (resources != null) {
|
||||
ListMultimap<String, String> parameterToOrValues =
|
||||
processResourcesOrCompartments(theRequestDetails, resDef, resources, false, theResourceName);
|
||||
if (parametersToAdd == null) {
|
||||
parametersToAdd = parameterToOrValues;
|
||||
} else if (parameterToOrValues != null) {
|
||||
parametersToAdd.putAll(parameterToOrValues);
|
||||
}
|
||||
}
|
||||
return parametersToAdd;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -201,102 +439,26 @@ public class SearchNarrowingInterceptor {
|
|||
&& !"$everything".equalsIgnoreCase(theRequestDetails.getOperation());
|
||||
}
|
||||
|
||||
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
|
||||
public void hookIncomingRequestPreHandled(
|
||||
ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
|
||||
throws AuthenticationException {
|
||||
if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.TRANSACTION) {
|
||||
return;
|
||||
}
|
||||
|
||||
IBaseBundle bundle = (IBaseBundle) theRequestDetails.getResource();
|
||||
FhirContext ctx = theRequestDetails.getFhirContext();
|
||||
BundleEntryUrlProcessor processor =
|
||||
new BundleEntryUrlProcessor(ctx, theRequestDetails, theRequest, theResponse);
|
||||
BundleUtil.processEntries(ctx, bundle, processor);
|
||||
}
|
||||
|
||||
private void applyParametersToRequestDetails(
|
||||
RequestDetails theRequestDetails,
|
||||
@Nullable Map<String, List<String>> theParameterToOrValues,
|
||||
@Nullable ListMultimap<String, String> theParameterToOrValues,
|
||||
boolean thePatientIdMode) {
|
||||
Map<String, String[]> inputParameters = theRequestDetails.getParameters();
|
||||
if (theParameterToOrValues != null) {
|
||||
Map<String, String[]> newParameters = new HashMap<>(theRequestDetails.getParameters());
|
||||
for (Map.Entry<String, List<String>> nextEntry : theParameterToOrValues.entrySet()) {
|
||||
String nextParamName = nextEntry.getKey();
|
||||
List<String> nextAllowedValues = nextEntry.getValue();
|
||||
|
||||
if (!newParameters.containsKey(nextParamName)) {
|
||||
|
||||
/*
|
||||
* If we don't already have a parameter of the given type, add one
|
||||
*/
|
||||
String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
|
||||
String[] paramValues = {nextValuesJoined};
|
||||
newParameters.put(nextParamName, paramValues);
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* If the client explicitly requested the given parameter already, we'll
|
||||
* just update the request to have the intersection of the values that the client
|
||||
* requested, and the values that the user is allowed to see
|
||||
*/
|
||||
String[] existingValues = newParameters.get(nextParamName);
|
||||
|
||||
if (thePatientIdMode) {
|
||||
List<String> 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<String> nextRequestedValues =
|
||||
QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue);
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, String[]> newParameters =
|
||||
applyCompartmentParameters(theParameterToOrValues, thePatientIdMode, inputParameters);
|
||||
theRequestDetails.setParameters(newParameters);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, List<String>> processResourcesOrCompartments(
|
||||
private ListMultimap<String, String> processResourcesOrCompartments(
|
||||
RequestDetails theRequestDetails,
|
||||
RuntimeResourceDefinition theResDef,
|
||||
Collection<String> theResourcesOrCompartments,
|
||||
boolean theAreCompartments) {
|
||||
Map<String, List<String>> retVal = null;
|
||||
boolean theAreCompartments,
|
||||
String theResourceName) {
|
||||
ListMultimap<String, String> retVal = null;
|
||||
|
||||
String lastCompartmentName = null;
|
||||
String lastSearchParamName = null;
|
||||
|
@ -315,7 +477,7 @@ public class SearchNarrowingInterceptor {
|
|||
|
||||
} else {
|
||||
|
||||
if (compartmentName.equalsIgnoreCase(theRequestDetails.getResourceName())) {
|
||||
if (compartmentName.equalsIgnoreCase(theResourceName)) {
|
||||
|
||||
searchParamName = "_id";
|
||||
|
||||
|
@ -331,10 +493,9 @@ public class SearchNarrowingInterceptor {
|
|||
|
||||
if (searchParamName != null) {
|
||||
if (retVal == null) {
|
||||
retVal = new HashMap<>();
|
||||
retVal = MultimapBuilder.hashKeys().arrayListValues().build();
|
||||
}
|
||||
List<String> orValues = retVal.computeIfAbsent(searchParamName, t -> new ArrayList<>());
|
||||
orValues.add(nextCompartment);
|
||||
retVal.put(searchParamName, nextCompartment);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,9 +503,9 @@ public class SearchNarrowingInterceptor {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private Map<String, List<String>> processAllowedCodes(
|
||||
private ListMultimap<String, String> processAllowedCodes(
|
||||
RuntimeResourceDefinition theResDef, List<AllowedCodeInValueSet> theAllowedCodeInValueSet) {
|
||||
Map<String, List<String>> retVal = null;
|
||||
ListMultimap<String, String> retVal = null;
|
||||
|
||||
for (AllowedCodeInValueSet next : theAllowedCodeInValueSet) {
|
||||
String resourceName = next.getResourceName();
|
||||
|
@ -371,9 +532,9 @@ public class SearchNarrowingInterceptor {
|
|||
}
|
||||
|
||||
if (retVal == null) {
|
||||
retVal = new HashMap<>();
|
||||
retVal = MultimapBuilder.hashKeys().arrayListValues().build();
|
||||
}
|
||||
retVal.computeIfAbsent(paramName, k -> new ArrayList<>()).add(valueSetUrl);
|
||||
retVal.put(paramName, valueSetUrl);
|
||||
}
|
||||
|
||||
return retVal;
|
||||
|
@ -408,7 +569,7 @@ public class SearchNarrowingInterceptor {
|
|||
Set<String> queryParameters = theRequestDetails.getParameters().keySet();
|
||||
|
||||
List<RuntimeSearchParam> searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName);
|
||||
if (searchParams.size() > 0) {
|
||||
if (!searchParams.isEmpty()) {
|
||||
|
||||
// Resources like Observation have several fields that add the resource to
|
||||
// the compartment. In the case of Observation, it's subject, patient and performer.
|
||||
|
@ -467,41 +628,75 @@ public class SearchNarrowingInterceptor {
|
|||
}
|
||||
}
|
||||
|
||||
private class BundleEntryUrlProcessor implements Consumer<ModifiableBundleEntry> {
|
||||
private final FhirContext myFhirContext;
|
||||
private final ServletRequestDetails myRequestDetails;
|
||||
private final HttpServletRequest myRequest;
|
||||
private final HttpServletResponse myResponse;
|
||||
@Nonnull
|
||||
private static Map<String, String[]> applyCompartmentParameters(
|
||||
@Nonnull ListMultimap<String, String> theParameterToOrValues,
|
||||
boolean thePatientIdMode,
|
||||
Map<String, String[]> theInputParameters) {
|
||||
Map<String, String[]> newParameters = new HashMap<>(theInputParameters);
|
||||
for (String nextParamName : theParameterToOrValues.keySet()) {
|
||||
List<String> nextAllowedValues = theParameterToOrValues.get(nextParamName);
|
||||
|
||||
public BundleEntryUrlProcessor(
|
||||
FhirContext theFhirContext,
|
||||
ServletRequestDetails theRequestDetails,
|
||||
HttpServletRequest theRequest,
|
||||
HttpServletResponse theResponse) {
|
||||
myFhirContext = theFhirContext;
|
||||
myRequestDetails = theRequestDetails;
|
||||
myRequest = theRequest;
|
||||
myResponse = theResponse;
|
||||
if (!newParameters.containsKey(nextParamName)) {
|
||||
|
||||
/*
|
||||
* If we don't already have a parameter of the given type, add one
|
||||
*/
|
||||
String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
|
||||
String[] paramValues = {nextValuesJoined};
|
||||
newParameters.put(nextParamName, paramValues);
|
||||
|
||||
} else {
|
||||
|
||||
/*
|
||||
* If the client explicitly requested the given parameter already, we'll
|
||||
* just update the request to have the intersection of the values that the client
|
||||
* requested, and the values that the user is allowed to see
|
||||
*/
|
||||
String[] existingValues = newParameters.get(nextParamName);
|
||||
|
||||
if (thePatientIdMode) {
|
||||
List<String> 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<String> nextRequestedValues =
|
||||
QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue);
|
||||
List<String> nextPermittedValues = ListUtils.union(
|
||||
ListUtils.intersection(nextRequestedValues, nextAllowedValues),
|
||||
ListUtils.intersection(nextRequestedValues, nextAllowedValueIds));
|
||||
if (!nextPermittedValues.isEmpty()) {
|
||||
restrictedExistingList = true;
|
||||
existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(ModifiableBundleEntry theModifiableBundleEntry) {
|
||||
ArrayListMultimap<String, String> 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);
|
||||
|
||||
hookIncomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse);
|
||||
|
||||
theModifiableBundleEntry.setRequestUrl(
|
||||
myFhirContext, ServletRequestUtil.extractUrl(subServletRequestDetails));
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newParameters;
|
||||
}
|
||||
|
||||
static List<AllowedCodeInValueSet> getPostFilteringList(RequestDetails theRequestDetails) {
|
||||
|
@ -517,4 +712,82 @@ public class SearchNarrowingInterceptor {
|
|||
static List<AllowedCodeInValueSet> getPostFilteringListOrNull(RequestDetails theRequestDetails) {
|
||||
return (List<AllowedCodeInValueSet>) theRequestDetails.getAttribute(POST_FILTERING_LIST_ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
private class BundleEntryUrlProcessor implements Consumer<ModifiableBundleEntry> {
|
||||
private final FhirContext myFhirContext;
|
||||
private final ServletRequestDetails myRequestDetails;
|
||||
private final AuthorizedList myAuthorizedList;
|
||||
|
||||
public BundleEntryUrlProcessor(FhirContext theFhirContext, ServletRequestDetails theRequestDetails) {
|
||||
myFhirContext = theFhirContext;
|
||||
myRequestDetails = theRequestDetails;
|
||||
myAuthorizedList = buildAuthorizedList(theRequestDetails);
|
||||
}
|
||||
|
||||
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
|
||||
@Override
|
||||
public void accept(ModifiableBundleEntry theModifiableBundleEntry) {
|
||||
if (myAuthorizedList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RequestTypeEnum method = theModifiableBundleEntry.getRequestMethod();
|
||||
String requestUrl = theModifiableBundleEntry.getRequestUrl();
|
||||
if (method != null && isNotBlank(requestUrl)) {
|
||||
|
||||
String resourceType = UrlUtil.parseUrl(requestUrl).getResourceType();
|
||||
|
||||
switch (method) {
|
||||
case GET: {
|
||||
String existingRequestUrl = theModifiableBundleEntry.getRequestUrl();
|
||||
String newConditionalUrl = narrowConditionalUrl(
|
||||
myRequestDetails, existingRequestUrl, false, resourceType, true, myAuthorizedList);
|
||||
if (isNotBlank(newConditionalUrl)) {
|
||||
newConditionalUrl = resourceType + "?" + newConditionalUrl;
|
||||
theModifiableBundleEntry.setRequestUrl(myFhirContext, newConditionalUrl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case POST: {
|
||||
if (myNarrowConditionalUrls) {
|
||||
String existingConditionalUrl = theModifiableBundleEntry.getConditionalUrl();
|
||||
if (isNotBlank(existingConditionalUrl)) {
|
||||
String newConditionalUrl = narrowConditionalUrl(
|
||||
myRequestDetails,
|
||||
existingConditionalUrl,
|
||||
true,
|
||||
resourceType,
|
||||
false,
|
||||
myAuthorizedList);
|
||||
if (isNotBlank(newConditionalUrl)) {
|
||||
theModifiableBundleEntry.setRequestIfNoneExist(myFhirContext, newConditionalUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PUT:
|
||||
case DELETE:
|
||||
case PATCH: {
|
||||
if (myNarrowConditionalUrls) {
|
||||
String existingConditionalUrl = theModifiableBundleEntry.getConditionalUrl();
|
||||
if (isNotBlank(existingConditionalUrl)) {
|
||||
String newConditionalUrl = narrowConditionalUrl(
|
||||
myRequestDetails,
|
||||
existingConditionalUrl,
|
||||
true,
|
||||
resourceType,
|
||||
false,
|
||||
myAuthorizedList);
|
||||
if (isNotBlank(newConditionalUrl)) {
|
||||
theModifiableBundleEntry.setRequestUrl(myFhirContext, newConditionalUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
|
|||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
@ -59,6 +61,7 @@ public class ServletRequestDetails extends RequestDetails {
|
|||
private RestfulServer myServer;
|
||||
private HttpServletRequest myServletRequest;
|
||||
private HttpServletResponse myServletResponse;
|
||||
private ListMultimap<String, String> myHeaders;
|
||||
|
||||
/**
|
||||
* Constructor for testing only
|
||||
|
@ -129,17 +132,63 @@ public class ServletRequestDetails extends RequestDetails {
|
|||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
// For efficiency, we only make a copy of the request headers if we need to
|
||||
// modify them
|
||||
if (myHeaders != null) {
|
||||
List<String> values = myHeaders.get(name);
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return values.get(0);
|
||||
}
|
||||
}
|
||||
return getServletRequest().getHeader(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHeaders(String name) {
|
||||
// For efficiency, we only make a copy of the request headers if we need to
|
||||
// modify them
|
||||
if (myHeaders != null) {
|
||||
return myHeaders.get(name);
|
||||
}
|
||||
Enumeration<String> headers = getServletRequest().getHeaders(name);
|
||||
return headers == null
|
||||
? Collections.emptyList()
|
||||
: Collections.list(getServletRequest().getHeaders(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHeader(String theName, String theValue) {
|
||||
initHeaders();
|
||||
myHeaders.put(theName, theValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHeaders(String theName, List<String> theValue) {
|
||||
initHeaders();
|
||||
myHeaders.removeAll(theName);
|
||||
myHeaders.putAll(theName, theValue);
|
||||
}
|
||||
|
||||
private void initHeaders() {
|
||||
if (myHeaders == null) {
|
||||
// Make sure we are case-insensitive for header names
|
||||
myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
|
||||
.arrayListValues()
|
||||
.build();
|
||||
|
||||
Enumeration<String> headerNames = getServletRequest().getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String nextName = headerNames.nextElement();
|
||||
Enumeration<String> values = getServletRequest().getHeaders(nextName);
|
||||
while (values.hasMoreElements()) {
|
||||
myHeaders.put(nextName, values.nextElement());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String theAttributeName) {
|
||||
Validate.notBlank(theAttributeName, "theAttributeName must not be null or blank");
|
||||
|
|
|
@ -19,34 +19,38 @@
|
|||
*/
|
||||
package ca.uhn.fhir.rest.server.servlet;
|
||||
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ServletSubRequestDetails extends ServletRequestDetails {
|
||||
|
||||
private final ServletRequestDetails myWrap;
|
||||
private Map<String, List<String>> myHeaders = new HashMap<>();
|
||||
/**
|
||||
* Map with case-insensitive keys
|
||||
*/
|
||||
private final ListMultimap<String, String> myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
|
||||
.arrayListValues()
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param theRequestDetails The parent request details
|
||||
*/
|
||||
public ServletSubRequestDetails(ServletRequestDetails theRequestDetails) {
|
||||
public ServletSubRequestDetails(@Nonnull ServletRequestDetails theRequestDetails) {
|
||||
super(theRequestDetails.getInterceptorBroadcaster());
|
||||
|
||||
myWrap = theRequestDetails;
|
||||
|
||||
if (theRequestDetails != null) {
|
||||
Map<String, List<String>> headers = theRequestDetails.getHeaders();
|
||||
for (Map.Entry<String, List<String>> next : headers.entrySet()) {
|
||||
myHeaders.put(next.getKey().toLowerCase(), next.getValue());
|
||||
}
|
||||
myHeaders.putAll(next.getKey(), next.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,16 +64,15 @@ public class ServletSubRequestDetails extends ServletRequestDetails {
|
|||
return myWrap.getServletResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHeader(String theName, String theValue) {
|
||||
String lowerCase = theName.toLowerCase();
|
||||
List<String> list = myHeaders.computeIfAbsent(lowerCase, k -> new ArrayList<>());
|
||||
list.add(theValue);
|
||||
myHeaders.put(theName, theValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String theName) {
|
||||
List<String> list = myHeaders.get(theName.toLowerCase());
|
||||
if (list == null || list.isEmpty()) {
|
||||
List<String> list = myHeaders.get(theName);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return list.get(0);
|
||||
|
@ -78,7 +81,7 @@ public class ServletSubRequestDetails extends ServletRequestDetails {
|
|||
@Override
|
||||
public List<String> getHeaders(String theName) {
|
||||
List<String> list = myHeaders.get(theName.toLowerCase());
|
||||
if (list == null || list.isEmpty()) {
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return list;
|
||||
|
|
|
@ -68,22 +68,4 @@ public class ServletRequestUtil {
|
|||
theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
|
||||
return requestDetails;
|
||||
}
|
||||
|
||||
public static String extractUrl(ServletRequestDetails theRequestDetails) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (Map.Entry<String, String[]> next :
|
||||
theRequestDetails.getParameters().entrySet()) {
|
||||
for (String nextValue : next.getValue()) {
|
||||
if (b.length() == 0) {
|
||||
b.append('?');
|
||||
} else {
|
||||
b.append('&');
|
||||
}
|
||||
b.append(UrlUtil.escapeUrlParam(next.getKey()));
|
||||
b.append('=');
|
||||
b.append(UrlUtil.escapeUrlParam(nextValue));
|
||||
}
|
||||
}
|
||||
return theRequestDetails.getRequestPath() + b.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,32 @@
|
|||
package ca.uhn.fhir.rest.server.servlet;
|
||||
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import org.apache.commons.collections4.iterators.IteratorEnumeration;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ServletRequestDetailsTest {
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest myHttpServletRequest;
|
||||
|
||||
@Test
|
||||
public void testRewriteHistoryHeader() {
|
||||
ServletRequestDetails servletRequestDetails = new ServletRequestDetails();
|
||||
|
@ -41,4 +57,24 @@ class ServletRequestDetailsTest {
|
|||
assertFalse(servletRequestDetails.isRewriteHistory());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddHeader() {
|
||||
ServletRequestDetails srd = new ServletRequestDetails();
|
||||
srd.setServletRequest(myHttpServletRequest);
|
||||
when(myHttpServletRequest.getHeaderNames()).thenReturn(new IteratorEnumeration<>(List.of("Foo").iterator()));
|
||||
when(myHttpServletRequest.getHeaders(eq("Foo"))).thenReturn(new IteratorEnumeration<>(List.of("Bar", "Baz").iterator()));
|
||||
|
||||
srd.addHeader("Name", "Value");
|
||||
srd.addHeader("Name", "Value2");
|
||||
|
||||
// Verify added headers (make sure we're case insensitive)
|
||||
assertEquals("Value", srd.getHeader("NAME"));
|
||||
assertThat(srd.getHeaders("name"), Matchers.contains("Value", "Value2"));
|
||||
|
||||
// Verify original headers (make sure we're case insensitive)
|
||||
assertEquals("Bar", srd.getHeader("FOO"));
|
||||
assertThat(srd.getHeaders("foo"), Matchers.contains("Bar", "Baz"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir-serviceloaders</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir-serviceloaders</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
@ -21,7 +21,7 @@
|
|||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-caching-api</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir-serviceloaders</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-spring-boot</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,12 +4,20 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterOr;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
||||
import ca.uhn.fhir.rest.annotation.Create;
|
||||
import ca.uhn.fhir.rest.annotation.Delete;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.OptionalParam;
|
||||
import ca.uhn.fhir.rest.annotation.Patch;
|
||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.annotation.Transaction;
|
||||
import ca.uhn.fhir.rest.annotation.TransactionParam;
|
||||
import ca.uhn.fhir.rest.annotation.Update;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||
import ca.uhn.fhir.rest.api.PatchTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||
|
@ -23,12 +31,18 @@ import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
|
|||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Parameters;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.hl7.fhir.r4.model.Resource;
|
||||
|
@ -44,16 +58,19 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
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.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
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.fail;
|
||||
|
@ -77,10 +94,13 @@ public class SearchNarrowingInterceptorTest {
|
|||
private static List<Resource> ourReturn;
|
||||
private static AuthorizedList ourNextAuthorizedList;
|
||||
private static Bundle.BundleEntryRequestComponent ourLastBundleRequest;
|
||||
private static String ourLastConditionalUrl;
|
||||
|
||||
private IGenericClient myClient;
|
||||
@Mock
|
||||
private IValidationSupport myValidationSupport;
|
||||
private MySearchNarrowingInterceptor myInterceptor;
|
||||
|
||||
@RegisterExtension
|
||||
private RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx)
|
||||
.registerProvider(new DummyObservationResourceProvider())
|
||||
|
@ -99,8 +119,11 @@ public class SearchNarrowingInterceptorTest {
|
|||
ourLastPerformerParam = null;
|
||||
ourLastCodeParam = null;
|
||||
ourNextAuthorizedList = null;
|
||||
ourLastConditionalUrl = null;
|
||||
|
||||
myInterceptor = new MySearchNarrowingInterceptor();
|
||||
myInterceptor.setNarrowConditionalUrls(true);
|
||||
|
||||
myRestfulServerExtension.registerInterceptor(myInterceptor);
|
||||
|
||||
myClient = myRestfulServerExtension.getFhirClient();
|
||||
|
@ -299,7 +322,7 @@ public class SearchNarrowingInterceptorTest {
|
|||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertEquals("Patient?_id=" + URLEncoder.encode("Patient/123,Patient/456"), ourLastBundleRequest.getUrl());
|
||||
assertEquals("Patient?_id=" + UrlUtil.escapeUrlParam("Patient/123,Patient/456"), ourLastBundleRequest.getUrl());
|
||||
}
|
||||
@Test
|
||||
public void testNarrow_OnlyAppliesToSearches() {
|
||||
|
@ -539,6 +562,319 @@ public class SearchNarrowingInterceptorTest {
|
|||
assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalCreate_Patient() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addResources("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.create()
|
||||
.resource(new Patient().setActive(true))
|
||||
.conditionalByUrl("Patient?active=true")
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.create", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("/Patient?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("active=true"));
|
||||
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalCreate_Patient_Disabled() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addResources("Patient/123", "Patient/456");
|
||||
myInterceptor.setNarrowConditionalUrls(false);
|
||||
|
||||
myClient
|
||||
.create()
|
||||
.resource(new Patient().setActive(true))
|
||||
.conditionalByUrl("Patient?active=true")
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.create", ourLastHitMethod);
|
||||
assertEquals("/Patient?active=true", ourLastConditionalUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalCreate_Patient_ReturnNull() {
|
||||
ourNextAuthorizedList = null;
|
||||
|
||||
myClient
|
||||
.create()
|
||||
.resource(new Patient().setActive(true))
|
||||
.conditionalByUrl("Patient?active=true")
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.create", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, equalTo("/Patient?active=true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalCreate_Observation() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.create()
|
||||
.resource(new Observation().setStatus(Observation.ObservationStatus.FINAL))
|
||||
.conditionalByUrl("Observation?status=final")
|
||||
.execute();
|
||||
|
||||
assertEquals("Observation.create", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("/Observation?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("status=final"));
|
||||
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalCreate_Observation_InTransaction() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionCreateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL)).conditional("Observation?status=final");
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertEquals("Observation", ourLastBundleRequest.getUrl());
|
||||
assertThat(ourLastBundleRequest.getIfNoneExist(), startsWith("Observation?"));
|
||||
assertThat(ourLastBundleRequest.getIfNoneExist(), containsString("status=final"));
|
||||
assertThat(ourLastBundleRequest.getIfNoneExist(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalCreate_Observation_InTransaction_Disabled() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
myInterceptor.setNarrowConditionalUrls(false);
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionCreateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL)).conditional("Observation?status=final");
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertEquals("Observation", ourLastBundleRequest.getUrl());
|
||||
assertEquals("Observation?status=final", ourLastBundleRequest.getIfNoneExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalUpdate_Patient() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addResources("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.update()
|
||||
.resource(new Patient().setActive(true))
|
||||
.conditionalByUrl("Patient?active=true")
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.update", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("Patient?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("active=true"));
|
||||
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalUpdate_Patient_Disabled() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addResources("Patient/123", "Patient/456");
|
||||
myInterceptor.setNarrowConditionalUrls(false);
|
||||
|
||||
myClient
|
||||
.update()
|
||||
.resource(new Patient().setActive(true))
|
||||
.conditionalByUrl("Patient?active=true")
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.update", ourLastHitMethod);
|
||||
assertEquals("Patient?active=true", ourLastConditionalUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalUpdate_Observation() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.update()
|
||||
.resource(new Observation().setStatus(Observation.ObservationStatus.FINAL))
|
||||
.conditionalByUrl("Observation?status=final")
|
||||
.execute();
|
||||
|
||||
assertEquals("Observation.update", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("Observation?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("status=final"));
|
||||
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalUpdate_Observation_InTransaction() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionUpdateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL)).conditional("Observation?status=final");
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertThat(ourLastBundleRequest.getUrl(), startsWith("Observation?"));
|
||||
assertThat(ourLastBundleRequest.getUrl(), containsString("status=final"));
|
||||
assertThat(ourLastBundleRequest.getUrl(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalUpdate_Observation_InTransaction_NoConditionalUrl() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionUpdateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL).setId("Observation/ABC"));
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertEquals("Observation/ABC", ourLastBundleRequest.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalDelete_Patient() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addResources("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.delete()
|
||||
.resourceConditionalByUrl("Patient?active=true")
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.delete", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("Patient?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("active=true"));
|
||||
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalDelete_Observation() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.delete()
|
||||
.resourceConditionalByUrl("Observation?status=final")
|
||||
.execute();
|
||||
|
||||
assertEquals("Observation.delete", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("Observation?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("status=final"));
|
||||
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalDelete_Observation_InTransaction() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionDeleteConditionalEntry("Observation?status=final");
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertThat(ourLastBundleRequest.getUrl(), startsWith("Observation?"));
|
||||
assertThat(ourLastBundleRequest.getUrl(), containsString("status=final"));
|
||||
assertThat(ourLastBundleRequest.getUrl(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalPatch_Patient() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addResources("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.patch()
|
||||
.withFhirPatch(new Parameters())
|
||||
.conditional(Patient.class)
|
||||
.whereMap(Map.of("active", List.of("true")))
|
||||
.execute();
|
||||
|
||||
assertEquals("Patient.patch", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("Patient?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("active=true"));
|
||||
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalPatch_Observation() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
myClient
|
||||
.patch()
|
||||
.withFhirPatch(new Parameters())
|
||||
.conditional(Observation.class)
|
||||
.whereMap(Map.of("status", List.of("final")))
|
||||
.execute();
|
||||
|
||||
assertEquals("Observation.patch", ourLastHitMethod);
|
||||
assertThat(ourLastConditionalUrl, startsWith("Observation?"));
|
||||
assertThat(ourLastConditionalUrl, containsString("status=final"));
|
||||
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNarrowCompartment_ConditionalPatch_Observation_InTransaction() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionFhirPatchEntry(new Parameters()).conditional("Observation?status=final");
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertThat(ourLastBundleRequest.getUrl(), startsWith("Observation?"));
|
||||
assertThat(ourLastBundleRequest.getUrl(), containsString("status=final"));
|
||||
assertThat(ourLastBundleRequest.getUrl(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransactionWithNonConditionalDeleteNotModified() {
|
||||
ourNextAuthorizedList = new AuthorizedList()
|
||||
.addCompartments("Patient/123", "Patient/456");
|
||||
|
||||
BundleBuilder bb = new BundleBuilder(ourCtx);
|
||||
bb.addTransactionDeleteEntry("Patient", "ABC");
|
||||
|
||||
myClient
|
||||
.transaction()
|
||||
.withBundle(bb.getBundle())
|
||||
.execute();
|
||||
|
||||
assertEquals("transaction", ourLastHitMethod);
|
||||
assertEquals("Patient/ABC", ourLastBundleRequest.getUrl());
|
||||
}
|
||||
|
||||
|
||||
private List<String> toStrings(BaseAndListParam<? extends IQueryParameterOr<?>> theParams) {
|
||||
List<? extends IQueryParameterOr<? extends IQueryParameterType>> valuesAsQueryTokens = theParams.getValuesAsQueryTokens();
|
||||
|
||||
|
@ -572,6 +908,7 @@ public class SearchNarrowingInterceptorTest {
|
|||
TestUtil.randomizeLocaleAndTimezone();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
|
@ -590,8 +927,37 @@ public class SearchNarrowingInterceptorTest {
|
|||
return ourReturn;
|
||||
}
|
||||
|
||||
@Create
|
||||
public MethodOutcome create(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
|
||||
ourLastHitMethod = "Patient.create";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Patient/123"), true);
|
||||
}
|
||||
|
||||
@Update
|
||||
public MethodOutcome update(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
|
||||
ourLastHitMethod = "Patient.update";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Patient/123"), true);
|
||||
}
|
||||
|
||||
@Delete
|
||||
public MethodOutcome delete(@IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl) {
|
||||
ourLastHitMethod = "Patient.delete";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Patient/123"), true);
|
||||
}
|
||||
|
||||
@Patch
|
||||
public MethodOutcome patch(@IdParam IIdType theId, @ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl, PatchTypeEnum thePatchType) {
|
||||
ourLastHitMethod = "Patient.patch";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Patient/123"), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class DummyObservationResourceProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
|
@ -617,6 +983,34 @@ public class SearchNarrowingInterceptorTest {
|
|||
return ourReturn;
|
||||
}
|
||||
|
||||
@Create
|
||||
public MethodOutcome create(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
|
||||
ourLastHitMethod = "Observation.create";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Observation/123"), true);
|
||||
}
|
||||
|
||||
@Update
|
||||
public MethodOutcome update(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
|
||||
ourLastHitMethod = "Observation.update";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Observation/123"), true);
|
||||
}
|
||||
|
||||
@Delete
|
||||
public MethodOutcome delete(@IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl) {
|
||||
ourLastHitMethod = "Observation.delete";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Observation/123"), true);
|
||||
}
|
||||
|
||||
@Patch
|
||||
public MethodOutcome patch(@IdParam IIdType theId, @ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl, PatchTypeEnum thePatchType) {
|
||||
ourLastHitMethod = "Observation.patch";
|
||||
ourLastConditionalUrl = theConditionalUrl;
|
||||
return new MethodOutcome(new IdType("Observation/123"), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class DummySystemProvider {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
3
pom.xml
3
pom.xml
|
@ -9,7 +9,7 @@
|
|||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<name>HAPI-FHIR</name>
|
||||
<description>An open-source implementation of the FHIR specification in Java.</description>
|
||||
|
@ -3241,3 +3241,4 @@
|
|||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir</artifactId>
|
||||
<version>7.1.3-SNAPSHOT</version>
|
||||
<version>7.1.4-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
|
Loading…
Reference in New Issue