Merge remote-tracking branch 'remotes/origin/master' into im_20200728_term_multi_version_support

This commit is contained in:
ianmarshall 2020-08-16 21:38:41 -04:00
commit bd94ff96cc
122 changed files with 1369 additions and 382 deletions

View File

@ -3,7 +3,7 @@ HAPI FHIR
HAPI FHIR - Java API for HL7 FHIR Clients and Servers
[![Build Status](https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master)](https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master)
[![Build Status](https://dev.azure.com/hapifhir/HAPI%20FHIR/_apis/build/status/jamesagnew.hapi-fhir?branchName=master)](https://dev.azure.com/hapifhir/HAPI%20FHIR/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/jamesagnew/hapi-fhir/branch/master/graph/badge.svg)](https://codecov.io/gh/jamesagnew/hapi-fhir)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg)](http://search.maven.org/#search|ga|1|ca.uhn.hapi.fhir)
[![License](https://img.shields.io/badge/license-apache%202.0-60C060.svg)](https://hapifhir.io/hapi-fhir/license.html)

View File

@ -79,7 +79,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
<version>24.1.1</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -27,7 +27,7 @@ import ca.uhn.fhir.rest.param.ParamPrefixEnum;
*/
public class NumberClientParam extends BaseClientParam implements IParam {
private String myParamName;
private final String myParamName;
public NumberClientParam(String theParamName) {
myParamName = theParamName;
@ -37,12 +37,12 @@ public class NumberClientParam extends BaseClientParam implements IParam {
return new IMatches<ICriterion<NumberClientParam>>() {
@Override
public ICriterion<NumberClientParam> number(long theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), Long.toString(theNumber));
return new StringCriterion<>(getParamName(), Long.toString(theNumber));
}
@Override
public ICriterion<NumberClientParam> number(String theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), (theNumber));
return new StringCriterion<>(getParamName(), (theNumber));
}
};
}
@ -56,12 +56,12 @@ public class NumberClientParam extends BaseClientParam implements IParam {
return new IMatches<ICriterion<NumberClientParam>>() {
@Override
public ICriterion<NumberClientParam> number(long theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.GREATERTHAN, Long.toString(theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.GREATERTHAN, Long.toString(theNumber));
}
@Override
public ICriterion<NumberClientParam> number(String theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.GREATERTHAN, (theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.GREATERTHAN, (theNumber));
}
};
}
@ -70,12 +70,12 @@ public class NumberClientParam extends BaseClientParam implements IParam {
return new IMatches<ICriterion<NumberClientParam>>() {
@Override
public ICriterion<NumberClientParam> number(long theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.GREATERTHAN_OR_EQUALS, Long.toString(theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.GREATERTHAN_OR_EQUALS, Long.toString(theNumber));
}
@Override
public ICriterion<NumberClientParam> number(String theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.GREATERTHAN_OR_EQUALS, (theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.GREATERTHAN_OR_EQUALS, (theNumber));
}
};
}
@ -84,12 +84,12 @@ public class NumberClientParam extends BaseClientParam implements IParam {
return new IMatches<ICriterion<NumberClientParam>>() {
@Override
public ICriterion<NumberClientParam> number(long theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.LESSTHAN, Long.toString(theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.LESSTHAN, Long.toString(theNumber));
}
@Override
public ICriterion<NumberClientParam> number(String theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.LESSTHAN, (theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.LESSTHAN, (theNumber));
}
};
}
@ -98,12 +98,12 @@ public class NumberClientParam extends BaseClientParam implements IParam {
return new IMatches<ICriterion<NumberClientParam>>() {
@Override
public ICriterion<NumberClientParam> number(long theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.LESSTHAN_OR_EQUALS, Long.toString(theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.LESSTHAN_OR_EQUALS, Long.toString(theNumber));
}
@Override
public ICriterion<NumberClientParam> number(String theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), ParamPrefixEnum.LESSTHAN_OR_EQUALS, (theNumber));
return new StringCriterion<>(getParamName(), ParamPrefixEnum.LESSTHAN_OR_EQUALS, (theNumber));
}
};
}
@ -112,12 +112,12 @@ public class NumberClientParam extends BaseClientParam implements IParam {
return new IMatches<ICriterion<NumberClientParam>>() {
@Override
public ICriterion<NumberClientParam> number(long theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), thePrefix, Long.toString(theNumber));
return new StringCriterion<>(getParamName(), thePrefix, Long.toString(theNumber));
}
@Override
public ICriterion<NumberClientParam> number(String theNumber) {
return new StringCriterion<NumberClientParam>(getParamName(), thePrefix, (theNumber));
return new StringCriterion<>(getParamName(), thePrefix, (theNumber));
}
};
}

View File

@ -56,7 +56,7 @@ public class NumberParam extends BaseParamWithPrefix<NumberParam> implements IQu
* Constructor
*
* @param theValue
* A string value, e.g. "&gt;5.0"
* A string value, e.g. "gt5.0"
*/
public NumberParam(String theValue) {
setValueAsQueryToken(null, null, null, theValue);

View File

@ -16,10 +16,16 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
/*
* #%L
@ -370,7 +376,7 @@ public class UrlUtil {
/**
* This method specifically HTML-encodes the &quot; and
* &lt; characters in order to prevent injection attacks.
*
* <p>
* The following characters are escaped:
* <ul>
* <li>&apos;</li>
@ -379,7 +385,6 @@ public class UrlUtil {
* <li>&gt;</li>
* <li>\n (newline)</li>
* </ul>
*
*/
public static String sanitizeUrlPart(CharSequence theString) {
if (theString == null) {
@ -432,6 +437,21 @@ public class UrlUtil {
return theString.toString();
}
/**
* Applies the same logic as {@link #sanitizeUrlPart(CharSequence)} but against an array, returning an array with the
* same strings as the input but with sanitization applied
*/
public static String[] sanitizeUrlPart(String[] theParameterValues) {
String[] retVal = null;
if (theParameterValues != null) {
retVal = new String[theParameterValues.length];
for (int i = 0; i < theParameterValues.length; i++) {
retVal[i] = sanitizeUrlPart(theParameterValues[i]);
}
}
return retVal;
}
private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
HashMap<String, String[]> retVal = new HashMap<>();
for (Entry<String, List<String>> nextEntry : map.entrySet()) {

View File

@ -64,7 +64,8 @@ public enum VersionEnum {
V5_0_0,
V5_0_1,
V5_0_2,
V5_1_0;
V5_1_0,
V5_2_0;
public static VersionEnum latestVersion() {
VersionEnum[] values = VersionEnum.values();

View File

@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@ -106,8 +107,9 @@ public class FhirServerConfig extends BaseJavaConfigDstu2 {
return retVal;
}
@Primary
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
public JpaTransactionManager hapiTransactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager retVal = new JpaTransactionManager();
retVal.setEntityManagerFactory(entityManagerFactory);
return retVal;

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.demo;
* #L%
*/
import ca.uhn.fhir.jpa.binstore.DatabaseBlobBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
@ -31,6 +33,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@ -97,10 +100,12 @@ public class FhirServerConfigDstu3 extends BaseJavaConfigDstu3 {
return retVal;
}
@Primary
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
public JpaTransactionManager hapiTransactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager retVal = new JpaTransactionManager();
retVal.setEntityManagerFactory(entityManagerFactory);
return retVal;
}
}

View File

@ -26,6 +26,8 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider;
import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
@ -180,6 +182,8 @@ public class JpaServerDemo extends RestfulServer {
getInterceptorService().registerInterceptor(new ResponseHighlighterInterceptor());
registerProvider(myAppCtx.getBean(BinaryAccessProvider.class));
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -73,13 +73,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-subscription</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
@ -96,7 +96,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-testpage-overlay</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<classifier>classes</classifier>
</dependency>
<dependency>

View File

@ -1,6 +1,6 @@
---
type: fix
issue: 1856
title: "The subscription delivery queue in the JPA server was erroniously keeping both a copy of the serialized and the
title: "The subscription delivery queue in the JPA server was erroneously keeping both a copy of the serialized and the
deserialized payload in memory for each entry in the queue, doubling the memory requirements. This also caused failures
when delivering XML payloads in some configurations. This has been corrected."

View File

@ -0,0 +1,9 @@
---
type: add
issue: 2021
title: "Added [EMPI](https://hapifhir.io/hapi-fhir/docs/server_jpa_empi/empi.html) functionality, including phonetic
indexing, asynchronous rules-based patient and practitioner matching when resources are created and updated. A number of
[EMPI Operations](https://hapifhir.io/hapi-fhir/docs/server_jpa_empi/empi_operations.html) are provided to
maintain EMPI links (e.g. resolving possible matches and possible duplicates). Also batch operations
are provided to identify links in existing patients and practitioners, and to 'wipe clean' all EMPI data and re-run
the batch as the empi matching rules are refined."

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 2022
title: When performing a resource $expunge in the JPA server, in-memory caches caused issues if a
forced ID was reused quickly enough (as can be the case in some testing scenarios). Thanks to
GitHub user @janvdpol for reporting!"

View File

@ -0,0 +1,7 @@
---
type: add
issue: 2023
title: "The JPA server maintains a cache of active SearchParameeter resources that can cause misleading results
if a SearchParameter is changed and other resources that would be indexed by the changed SearchParameter are updated
before the cache refreshes. A new interceptor has been added that should force a refresh sooner, especially on
non-clustered systems."

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2025
title: "A new interceptor has been added for the JPA server that selectively allows resource deletions to proceed even if
there are valid references to the candidate for deletion from other resources that are not being deleted."

View File

@ -0,0 +1,8 @@
---
type: security
issue: 2026
title: "An XSS vulnerability was reported in the HAPI FHIR Testpage Overlay module. Thanks to Will Davison of NCC Group (Manchester UK) for disclosing this vulnerability.
Users of the HAPI FHIR Testpage Overlay can use a specially crafted URL to exploit an XSS vulnerability in this module, allowing arbitrary JavaScript to be executed in the user's browser. The impact of this vulnerability is believed to be low, as this module is intended for testing and not believed to be widely used for any production purposes. Nonetheless, we recommend all users of the affected module upgrade immediately.
A complete audit of the affected codebase has been completed in order to detect and resolve any similar issues."

View File

@ -1,7 +1,7 @@
---
type: change
issue: 237
title: "The R5 structure methods for working with extensions on arbtrary fields, e.g.
title: "The R5 structure methods for working with extensions on arbitrary fields, e.g.
`getExtensionByUrl(String)`, `removeExtension(String)`, `getExtensionsByUrl(String)`
`hasExtension(String)`, and `getExtensionString(String)` have been enhanced so that they
now return modifier extensions as well as the non-modifier extensions they previously

View File

@ -0,0 +1,3 @@
---
release-date: "2020-08-13"
codename: "Manticore"

View File

@ -0,0 +1,8 @@
---
- item:
type: "add"
title: "The version of a few dependencies have been bumped to the latest versions
(dependent HAPI modules listed in brackets):
<ul>
<li>Flyway (JPA): 6.4.1-&gt; 6.5.4</li>
</ul>"

View File

@ -15,7 +15,7 @@ The following is a list of key subprojects you might open in your IDE:
# Getting the Sources
<p style="float:right;">
<a class="externalLink" href="https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master"><img src="https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master" alt="Build Status" class="img-fluid"/></a>
<a class="externalLink" href="https://dev.azure.com/hapifhir/HAPI%20FHIR/_build/latest?definitionId=1&branchName=master"><img src="https://dev.azure.com/hapifhir/HAPI%20FHIR/_apis/build/status/jamesagnew.hapi-fhir?branchName=master" alt="Build Status" class="img-fluid"/></a>
</p>
The best way to grab our sources is with Git. Grab the repository URL from our [GitHub page](https://github.com/jamesagnew/hapi-fhir). We try our best to ensure that the sources are always left in a buildable state. Check Azure Pipelines CI (see the image/link on the right) to see if the sources currently build.

View File

@ -180,8 +180,24 @@ The ResponseSizeCapturingInterceptor can be used to capture the number of charac
# JPA Server: Allow Cascading Deletes
* [CascadingDeleteInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.html)
* [CascadingDeleteInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java)
The CascadingDeleteInterceptor allows clients to request deletes be cascaded to other resources that contain incoming references. See [Cascading Deletes](/docs/server_jpa/configuration.html#cascading-deletes) for more information.
<a name="overridepathbasedreferentialintegrityfordeletesinterceptor"/>
# JPA Server: Disable Referential Integrity for Some Paths
* [OverridePathBasedReferentialIntegrityForDeletesInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.html)
* [OverridePathBasedReferentialIntegrityForDeletesInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java)
The OverridePathBasedReferentialIntegrityForDeletesInterceptor can be registered and configured to allow resources to be deleted even if other resources have outgoing references to the deleted resource. While it is generally a bad idea to allow deletion of resources that are referred to from other resources, there are circumstances where it is desirable. For example, if you have Provenance or AuditEvent resources that refer to a Patient resource that was created in error, you might want to alow the Patient to be deleted while leaving the Provenance and AuditEvent resources intact (including the now-invalid outgoing references to that Patient).
This interceptor uses FHIRPath expressions to indicate the resource paths that should not have referential integrity applied to them. For example, if this interceptor is configured with a path of `AuditEvent.agent.who`, a Patient resource would be allowed to be deleted even if one or more AuditEvents had references in that path to the given Patient (unless other resources also had references to the Patient).
# JPA Server: Retry on Version Conflicts
The UserRequestRetryVersionConflictsInterceptor allows clients to request that the server avoid version conflicts (HTTP 409) when two concurrent client requests attempt to modify the same resource. See [Version Conflicts](/docs/server_jpa/configuration.html#retry-on-version-conflict) for more information.

View File

@ -32,6 +32,8 @@ Creating your own interceptors is easy. Custom interceptor classes do not need t
* The method may have any of the parameters specified for the given [Pointcut](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html).
* The method must be public.
The following example shows a simple request counter interceptor.
```java

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -34,8 +34,9 @@ import java.util.Collection;
* time to time, even within minor point releases.
*/
public interface IDao {
String RESOURCE_PID_KEY = "RESOURCE_PID";
MetadataKeyResourcePid RESOURCE_PID = new MetadataKeyResourcePid("RESOURCE_PID");
MetadataKeyResourcePid RESOURCE_PID = new MetadataKeyResourcePid(RESOURCE_PID_KEY);
MetadataKeyCurrentlyReindexing CURRENTLY_REINDEXING = new MetadataKeyCurrentlyReindexing("CURRENTLY_REINDEXING");

View File

@ -34,6 +34,7 @@ import java.util.function.Predicate;
public class DeleteConflictList implements Iterable<DeleteConflict> {
private final List<DeleteConflict> myList = new ArrayList<>();
private final Set<String> myResourceIdsMarkedForDeletion;
private final Set<String> myResourceIdsToIgnoreConflict;
private int myRemoveModCount;
/**
@ -41,6 +42,7 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
*/
public DeleteConflictList() {
myResourceIdsMarkedForDeletion = new HashSet<>();
myResourceIdsToIgnoreConflict = new HashSet<>();
}
/**
@ -49,6 +51,7 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
*/
public DeleteConflictList(DeleteConflictList theParentList) {
myResourceIdsMarkedForDeletion = theParentList.myResourceIdsMarkedForDeletion;
myResourceIdsToIgnoreConflict = theParentList.myResourceIdsToIgnoreConflict;
}
@ -64,6 +67,18 @@ public class DeleteConflictList implements Iterable<DeleteConflict> {
myResourceIdsMarkedForDeletion.add(theIdType.toUnqualifiedVersionless().getValue());
}
public boolean isResourceIdToIgnoreConflict(IIdType theIdType) {
Validate.notNull(theIdType);
Validate.notBlank(theIdType.toUnqualifiedVersionless().getValue());
return myResourceIdsToIgnoreConflict.contains(theIdType.toUnqualifiedVersionless().getValue());
}
public void setResourceIdToIgnoreConflict(IIdType theIdType) {
Validate.notNull(theIdType);
Validate.notBlank(theIdType.toUnqualifiedVersionless().getValue());
myResourceIdsToIgnoreConflict.add(theIdType.toUnqualifiedVersionless().getValue());
}
public void add(DeleteConflict theDeleteConflict) {
myList.add(theDeleteConflict);
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -24,7 +25,9 @@ import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver;
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager;
import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc;
@ -167,6 +170,12 @@ public abstract class BaseConfig {
return new BatchJobSubmitterImpl();
}
@Lazy
@Bean
public CascadingDeleteInterceptor cascadingDeleteInterceptor(FhirContext theFhirContext, DaoRegistry theDaoRegistry, IInterceptorBroadcaster theInterceptorBroadcaster) {
return new CascadingDeleteInterceptor(theFhirContext, theDaoRegistry, theInterceptorBroadcaster);
}
/**
* This method should be overridden to provide an actual completed
* bean, but it provides a partially completed entity manager
@ -295,6 +304,12 @@ public abstract class BaseConfig {
return new HapiFhirHibernateJpaDialect(fhirContext().getLocalizer());
}
@Bean
@Lazy
public OverridePathBasedReferentialIntegrityForDeletesInterceptor overridePathBasedReferentialIntegrityForDeletesInterceptor() {
return new OverridePathBasedReferentialIntegrityForDeletesInterceptor();
}
@Bean
public IRequestPartitionHelperSvc requestPartitionHelperService() {
return new RequestPartitionHelperSvc();

View File

@ -79,7 +79,6 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@ -114,10 +113,8 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
@ -636,7 +633,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
IDao.RESOURCE_PID.put(res, theEntity.getId());
IDao.RESOURCE_PID.put(res, theEntity.getResourceId());
Collection<? extends BaseTag> tags = theTagList;
if (theEntity.isHasTags()) {
@ -708,7 +705,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
res.setId(res.getIdElement().withVersion(theVersion.toString()));
res.getMeta().setLastUpdated(theEntity.getUpdatedDate());
IDao.RESOURCE_PID.put(res, theEntity.getId());
IDao.RESOURCE_PID.put(res, theEntity.getResourceId());
Collection<? extends BaseTag> tags = theTagList;

View File

@ -994,22 +994,25 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
}
@Override
@Transactional
public T read(IIdType theId) {
return read(theId, null);
}
@Override
@Transactional
public T read(IIdType theId, RequestDetails theRequestDetails) {
return read(theId, theRequestDetails, false);
}
@Override
@Transactional
public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
validateResourceTypeAndThrowInvalidRequestException(theId);
return myTransactionService.execute(theRequest, tx-> doRead(theId, theRequest, theDeletedOk));
}
public T doRead(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
assert TransactionSynchronizationManager.isActualTransactionActive();
// Notify interceptors
if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId);

View File

@ -51,6 +51,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao
protected void postPersist(ResourceTable theEntity, SearchParameter theResource) {
super.postPersist(theEntity, theResource);
markAffectedResources(theResource);
}
@Override

View File

@ -46,6 +46,7 @@ import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@ -60,7 +61,11 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Collections;
import java.util.List;
@ -108,6 +113,8 @@ public class ResourceExpungeService implements IResourceExpungeService {
private ISearchParamPresentDao mySearchParamPresentDao;
@Autowired
private DaoConfig myDaoConfig;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Override
@Transactional
@ -158,6 +165,20 @@ public class ResourceExpungeService implements IResourceExpungeService {
return;
}
}
/*
* Once this transaction is committed, we will invalidate all memory caches
* in order to avoid any caches having references to things that no longer
* exist. This is a pretty brute-force way of addressing this, and could probably
* be optimized, but expunge is hopefully not frequently called on busy servers
* so it shouldn't be too big a deal.
*/
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){
@Override
public void afterCommit() {
myMemoryCacheService.invalidateAllCaches();
}
});
}
private void expungeHistoricalVersion(RequestDetails theRequestDetails, Long theNextVersionId, AtomicInteger theRemainingCount) {

View File

@ -100,6 +100,8 @@ public class IdHelperService {
private IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private FhirContext myFhirCtx;
@Autowired
private MemoryCacheService myMemoryCacheService;
public void delete(ForcedId forcedId) {
myForcedIdDao.deleteByPid(forcedId.getId());
@ -123,9 +125,6 @@ public class IdHelperService {
return matches.iterator().next();
}
@Autowired
private MemoryCacheService myMemoryCacheService;
/**
* Given a resource type and ID, determines the internal persistent ID for the resource.
*
@ -389,7 +388,7 @@ public class IdHelperService {
lookup
.stream()
.map(t -> new ResourceLookup((String) t[0], (Long) t[1], (Date) t[2]))
.forEach(t->{
.forEach(t -> {
theTarget.add(t);
if (!myDaoConfig.isDeleteEnabled()) {
String nextKey = Long.toString(t.getResourceId());
@ -432,19 +431,6 @@ public class IdHelperService {
return retVal;
}
public static boolean isValidPid(IIdType theId) {
if (theId == null) {
return false;
}
String idPart = theId.getIdPart();
return isValidPid(idPart);
}
public static boolean isValidPid(String theIdPart) {
return StringUtils.isNumeric(theIdPart);
}
@Nullable
public Long getPidOrNull(IBaseResource theResource) {
IAnyResource anyResource = (IAnyResource) theResource;
@ -481,11 +467,24 @@ public class IdHelperService {
return theIds.stream().collect(Collectors.toMap(this::getPidOrThrowException, Function.identity()));
}
public IIdType resourceIdFromPidOrThrowException(Long thePid) {
Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid);
if (!optionalResource.isPresent()) {
throw new ResourceNotFoundException("Requested resource not found");
}
return optionalResource.get().getIdDt().toVersionless();
}
public IIdType resourceIdFromPidOrThrowException(Long thePid) {
Optional<ResourceTable> optionalResource = myResourceTableDao.findById(thePid);
if (!optionalResource.isPresent()) {
throw new ResourceNotFoundException("Requested resource not found");
}
return optionalResource.get().getIdDt().toVersionless();
}
public static boolean isValidPid(IIdType theId) {
if (theId == null) {
return false;
}
String idPart = theId.getIdPart();
return isValidPid(idPart);
}
public static boolean isValidPid(String theIdPart) {
return StringUtils.isNumeric(theIdPart);
}
}

View File

@ -44,27 +44,25 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
@Service
public class DeleteConflictService {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
public static final int FIRST_QUERY_RESULT_COUNT = 1;
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
public static int MAX_RETRY_ATTEMPTS = 10;
public static String MAX_RETRY_ATTEMPTS_EXCEEDED_MSG = "Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.";
@Autowired
protected IResourceLinkDao myResourceLinkDao;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
DeleteConflictFinderService myDeleteConflictFinderService;
@Autowired
DaoConfig myDaoConfig;
@Autowired
protected IResourceLinkDao myResourceLinkDao;
@Autowired
private FhirContext myFhirContext;
@Autowired
protected IInterceptorBroadcaster myInterceptorBroadcaster;
public int validateOkToDelete(DeleteConflictList theDeleteConflicts, ResourceTable theEntity, boolean theForValidate, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
@ -87,9 +85,9 @@ public class DeleteConflictService {
++retryCount;
}
theDeleteConflicts.addAll(newConflicts);
if(retryCount >= MAX_RETRY_ATTEMPTS && !theDeleteConflicts.isEmpty()) {
if (retryCount >= MAX_RETRY_ATTEMPTS && !theDeleteConflicts.isEmpty()) {
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(myFhirContext);
OperationOutcomeUtil.addIssue(myFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, MAX_RETRY_ATTEMPTS_EXCEEDED_MSG,null, "processing");
OperationOutcomeUtil.addIssue(myFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, null, "processing");
throw new ResourceVersionConflictException(MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, oo);
}
return retryCount;
@ -123,7 +121,7 @@ public class DeleteConflictService {
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(TransactionDetails.class, theTransactionDetails);
return (DeleteConflictOutcome)JpaInterceptorBroadcaster.doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, hooks);
return (DeleteConflictOutcome) JpaInterceptorBroadcaster.doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, hooks);
}
private void addConflictsToList(DeleteConflictList theDeleteConflicts, ResourceTable theEntity, List<ResourceLink> theResultList) {
@ -142,26 +140,33 @@ public class DeleteConflictService {
}
public static void validateDeleteConflictsEmptyOrThrowException(FhirContext theFhirContext, DeleteConflictList theDeleteConflicts) {
if (theDeleteConflicts.isEmpty()) {
return;
}
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(theFhirContext);
IBaseOperationOutcome oo = null;
String firstMsg = null;
for (DeleteConflict next : theDeleteConflicts) {
if (theDeleteConflicts.isResourceIdToIgnoreConflict(next.getTargetId())) {
continue;
}
String msg = "Unable to delete " +
next.getTargetId().toUnqualifiedVersionless().getValue() +
" because at least one resource has a reference to this resource. First reference found was resource " +
next.getSourceId().toUnqualifiedVersionless().getValue() +
" in path " +
next.getSourcePath();
if (firstMsg == null) {
firstMsg = msg;
oo = OperationOutcomeUtil.newInstance(theFhirContext);
}
OperationOutcomeUtil.addIssue(theFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, msg, null, "processing");
}
if (firstMsg == null) {
return;
}
throw new ResourceVersionConflictException(firstMsg, oo);
}

View File

@ -71,6 +71,13 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
@Interceptor
public class CascadingDeleteInterceptor {
/*
* We keep the orders for the various handlers of {@link Pointcut#STORAGE_PRESTORAGE_DELETE_CONFLICTS} in one place
* so it's easy to compare them
*/
public static final int OVERRIDE_PATH_BASED_REF_INTEGRITY_INTERCEPTOR_ORDER = 0;
public static final int CASCADING_DELETE_INTERCEPTOR_ORDER = 1;
private static final Logger ourLog = LoggerFactory.getLogger(CascadingDeleteInterceptor.class);
private static final String CASCADED_DELETES_KEY = CascadingDeleteInterceptor.class.getName() + "_CASCADED_DELETES_KEY";
private static final String CASCADED_DELETES_FAILED_KEY = CascadingDeleteInterceptor.class.getName() + "_CASCADED_DELETES_FAILED_KEY";
@ -94,7 +101,7 @@ public class CascadingDeleteInterceptor {
myFhirContext = theFhirContext;
}
@Hook(Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS)
@Hook(value = Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, order = CASCADING_DELETE_INTERCEPTOR_ORDER)
public DeleteConflictOutcome handleDeleteConflicts(DeleteConflictList theConflictList, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
ourLog.debug("Have delete conflicts: {}", theConflictList);

View File

@ -0,0 +1,127 @@
package ca.uhn.fhir.jpa.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* This JPA interceptor can be configured with a collection of FHIRPath expressions, and will disable
* referential integrity for target resources at those paths.
* <p>
* For example, suppose this interceptor is configured with a path of <code>AuditEvent.entity.what</code>,
* and an AuditEvent resource exists in the repository that has a reference in that path to resource
* <code>Patient/123</code>. Normally this reference would prevent the Patient resource from being deleted unless
* the AuditEvent was first deleted as well (or a <a href="/hapi-fhir/docs/server_jpa/configuration.html#cascading-deletes">cascading delete</a> was used).
* With this interceptor in place, the Patient resource could be deleted, and the AuditEvent would remain intact.
* </p>
*/
@Interceptor
public class OverridePathBasedReferentialIntegrityForDeletesInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(OverridePathBasedReferentialIntegrityForDeletesInterceptor.class);
private final Set<String> myPaths = new HashSet<>();
@Autowired
private FhirContext myFhirContext;
@Autowired
private DaoRegistry myDaoRegistry;
/**
* Constructor
*/
public OverridePathBasedReferentialIntegrityForDeletesInterceptor() {
super();
}
/**
* Adds a FHIRPath expression indicating a resource path that should be ignored when considering referential
* integrity for deletes.
*
* @param thePath The FHIRPath expression, e.g. <code>AuditEvent.agent.who</code>
*/
public void addPath(String thePath) {
getPaths().add(thePath);
}
/**
* Remove all paths registered to this interceptor
*/
public void clearPaths() {
getPaths().clear();
}
/**
* Returns the paths that will be considered by this interceptor
*
* @see #addPath(String)
*/
public Set<String> getPaths() {
return myPaths;
}
/**
* Interceptor hook method. Do not invoke directly.
*/
@Hook(value = Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, order = CascadingDeleteInterceptor.OVERRIDE_PATH_BASED_REF_INTEGRITY_INTERCEPTOR_ORDER)
public void handleDeleteConflicts(DeleteConflictList theDeleteConflictList) {
for (DeleteConflict nextConflict : theDeleteConflictList) {
ourLog.info("Ignoring referential integrity deleting {} - Referred to from {} at path {}", nextConflict.getTargetId(), nextConflict.getSourceId(), nextConflict.getSourcePath());
IdDt sourceId = nextConflict.getSourceId();
IdDt targetId = nextConflict.getTargetId();
String targetIdValue = targetId.toVersionless().getValue();
IBaseResource sourceResource = myDaoRegistry.getResourceDao(sourceId.getResourceType()).read(sourceId);
IFhirPath fhirPath = myFhirContext.newFhirPath();
for (String nextPath : myPaths) {
List<IBaseReference> selections = fhirPath.evaluate(sourceResource, nextPath, IBaseReference.class);
for (IBaseReference nextSelection : selections) {
String selectionTargetValue = nextSelection.getReferenceElement().toVersionless().getValue();
if (Objects.equals(targetIdValue, selectionTargetValue)) {
theDeleteConflictList.setResourceIdToIgnoreConflict(nextConflict.getTargetId());
break;
}
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.dstu2;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
@ -28,6 +29,8 @@ import ca.uhn.fhir.model.primitive.DecimalDt;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -232,6 +235,47 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
}
/**
* See #2023
*/
@Test
public void testNumberSearchParam() {
SearchParameter numberParameter = new ca.uhn.fhir.model.dstu2.resource.SearchParameter();
numberParameter.setId("future-appointment-count");
numberParameter.setName("Future Appointment Count");
numberParameter.setCode("future-appointment-count");
numberParameter.setDescription("Count of future appointments for the patient");
numberParameter.setUrl("http://integer");
numberParameter.setStatus(ca.uhn.fhir.model.dstu2.valueset.ConformanceResourceStatusEnum.ACTIVE);
numberParameter.setBase(ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum.PATIENT);
numberParameter.setType(ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum.NUMBER);
numberParameter.setXpathUsage(XPathUsageTypeEnum.NORMAL);
numberParameter.setXpath("Patient.extension('http://integer')");
mySearchParameterDao.update(numberParameter);
// This fires every 10 seconds
mySearchParamRegistry.refreshCacheIfNecessary();
Patient patient = new Patient();
patient.setId("future-appointment-count-pt");
patient.setActive(true);
patient.addUndeclaredExtension(false, "http://integer", new IntegerDt(1));
myPatientDao.update(patient);
IBundleProvider search;
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam(1)));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("gt0")));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0")));
assertEquals(0, search.size());
}
@Test
public void testIncludeExtensionReferenceAsRecurse() {
SearchParameter attendingSp = new SearchParameter();

View File

@ -144,7 +144,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test {
} catch (PreconditionFailedException e) {
String ooString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome());
ourLog.info(ooString);
assertThat(ooString, containsString("Profile reference \\\"http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid\\\" could not be resolved, so has not been checked"));
assertThat(ooString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked"));
}
}

View File

@ -340,7 +340,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
OperationOutcome oo = (OperationOutcome) e.getOperationOutcome();
String outputString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo);
ourLog.info(outputString);
assertThat(outputString, containsString("Profile reference \\\"http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid\\\" could not be resolved, so has not been checked"));
assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked"));
}
}

View File

@ -99,7 +99,7 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
@Test
public void testDeleteCircularReferenceInTransaction() throws IOException {
public void testDeleteCircularReferenceInTransaction() {
// Create two resources with a circular reference
Organization org1 = new Organization();
@ -221,4 +221,12 @@ public class FhirResourceDaoR4DeleteTest extends BaseJpaR4Test {
}
@Test
public void testDeleteIgnoreReferentialIntegrityForPaths() {
}
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
@ -9,7 +10,11 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.dstu2.valueset.XPathUsageTypeEnum;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
@ -112,6 +117,46 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
mySearchParamRegistry.forceRefresh();
}
/**
* See #2023
*/
@Test
public void testNumberSearchParam() {
SearchParameter numberParameter = new SearchParameter();
numberParameter.setId("future-appointment-count");
numberParameter.setName("Future Appointment Count");
numberParameter.setCode("future-appointment-count");
numberParameter.setDescription("Count of future appointments for the patient");
numberParameter.setUrl("http://integer");
numberParameter.setStatus(Enumerations.PublicationStatus.ACTIVE);
numberParameter.addBase("Patient");
numberParameter.setType(Enumerations.SearchParamType.NUMBER);
numberParameter.setExpression("Patient.extension('http://integer')");
mySearchParameterDao.update(numberParameter);
// This fires every 10 seconds
mySearchParamRegistry.refreshCacheIfNecessary();
Patient patient = new Patient();
patient.setId("future-appointment-count-pt");
patient.setActive(true);
patient.addExtension( "http://integer", new IntegerType(1));
myPatientDao.update(patient);
IBundleProvider search;
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam(1)));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("gt0")));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0")));
assertEquals(0, search.size());
}
/**
* Draft search parameters should be ok even if they aren't completely valid
*/

View File

@ -314,7 +314,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
outcome = (OperationOutcome) e.getOperationOutcome();
String outcomeStr = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome);
ourLog.info("Validation outcome: {}", outcomeStr);
assertThat(outcomeStr, containsString("The Profile \\\"https://bb/StructureDefinition/BBDemographicAge\\\" definition allows for the type Quantity but found type string"));
assertThat(outcomeStr, containsString("The Profile 'https://bb/StructureDefinition/BBDemographicAge' definition allows for the type Quantity but found type string"));
}
}
@ -466,7 +466,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
obs.setSubject(new Reference("Group/123"));
OperationOutcome oo = validateAndReturnOutcome(obs);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals("Unable to resolve resource \"Group/123\"", oo.getIssueFirstRep().getDiagnostics(), encode(oo));
assertEquals("Unable to resolve resource 'Group/123'", oo.getIssueFirstRep().getDiagnostics(), encode(oo));
// Target of wrong type
obs.setSubject(new Reference("Group/ABC"));
@ -530,7 +530,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
obs.setSubject(new Reference("Group/123"));
OperationOutcome oo = validateAndReturnOutcome(obs);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals("Unable to resolve resource \"Group/123\"", oo.getIssueFirstRep().getDiagnostics(), encode(oo));
assertEquals("Unable to resolve resource 'Group/123'", oo.getIssueFirstRep().getDiagnostics(), encode(oo));
// Target of wrong type
obs.setSubject(new Reference("Group/ABC"));
@ -595,7 +595,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
obs.setSubject(new Reference("Group/123"));
OperationOutcome oo = validateAndReturnOutcome(obs);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals("Unable to resolve resource \"Group/123\"", oo.getIssueFirstRep().getDiagnostics(), encode(oo));
assertEquals("Unable to resolve resource 'Group/123'", oo.getIssueFirstRep().getDiagnostics(), encode(oo));
// Target of wrong type
obs.setSubject(new Reference("Group/ABC"));
@ -625,7 +625,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
" },\n" +
" \"text\": {\n" +
" \"status\": \"generated\",\n" +
" \"div\": \"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\"></div>\"\n" +
" \"div\": \"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\">HELLO</div>\"\n" +
" },\n" +
" \"url\": \"https://foo/bb\",\n" +
" \"name\": \"BBBehaviourType\",\n" +
@ -1134,7 +1134,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
org.hl7.fhir.r4.model.OperationOutcome oo = (org.hl7.fhir.r4.model.OperationOutcome) e.getOperationOutcome();
String outputString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo);
ourLog.info(outputString);
assertThat(outputString, containsString("Profile reference \\\"http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid\\\" could not be resolved, so has not been checked"));
assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked"));
}
}
@ -1170,7 +1170,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
org.hl7.fhir.r4.model.OperationOutcome oo = (org.hl7.fhir.r4.model.OperationOutcome) e.getOperationOutcome();
String outputString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo);
ourLog.info(outputString);
assertThat(outputString, containsString("Profile reference \\\"http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid\\\" could not be resolved, so has not been checked"));
assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked"));
}
}
@ -1453,7 +1453,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
fail("Didn't fail- response was " + encode);
} catch (PreconditionFailedException e) {
OperationOutcome oo = (OperationOutcome) e.getOperationOutcome();
assertEquals("No response answer found for required item \"link0\"", oo.getIssueFirstRep().getDiagnostics());
assertEquals("No response answer found for required item 'link0'", oo.getIssueFirstRep().getDiagnostics());
}
}
@ -1482,7 +1482,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
fail("Didn't fail- response was " + encode);
} catch (PreconditionFailedException e) {
OperationOutcome oo = (OperationOutcome) e.getOperationOutcome();
assertEquals("No response answer found for required item \"link0\"", oo.getIssueFirstRep().getDiagnostics());
assertEquals("No response answer found for required item 'link0'", oo.getIssueFirstRep().getDiagnostics());
}
}
@ -1506,7 +1506,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
try {
MethodOutcome validationOutcome = myQuestionnaireResponseDao.validate(qa, null, null, null, null, null, null);
OperationOutcome oo = (OperationOutcome) validationOutcome.getOperationOutcome();
assertEquals("The questionnaire \"http://foo/Questionnaire/DOES_NOT_EXIST\" could not be resolved, so no validation can be performed against the base questionnaire", oo.getIssueFirstRep().getDiagnostics());
assertEquals("The questionnaire 'http://foo/Questionnaire/DOES_NOT_EXIST' could not be resolved, so no validation can be performed against the base questionnaire", oo.getIssueFirstRep().getDiagnostics());
} catch (PreconditionFailedException e) {
fail(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome()));
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.provider.r4;
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
@ -20,7 +20,6 @@ import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -31,9 +30,9 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test {
public class CascadingDeleteInterceptorTest extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CascadingDeleteInterceptorR4Test.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CascadingDeleteInterceptorTest.class);
private IIdType myDiagnosticReportId;
@Autowired
@ -42,18 +41,13 @@ public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test
private IInterceptorBroadcaster myInterceptorBroadcaster;
private IIdType myPatientId;
@Autowired
private CascadingDeleteInterceptor myDeleteInterceptor;
private IIdType myObservationId;
private IIdType myConditionId;
private IIdType myEncounterId;
@Override
@BeforeEach
public void before() throws Exception {
super.before();
myDeleteInterceptor = new CascadingDeleteInterceptor(myFhirCtx, myDaoRegistry, myInterceptorBroadcaster);
}
@Autowired
private OverridePathBasedReferentialIntegrityForDeletesInterceptor myOverridePathBasedReferentialIntegrityForDeletesInterceptor;
@Override
@AfterEach
@ -161,6 +155,37 @@ public class CascadingDeleteInterceptorR4Test extends BaseResourceProviderR4Test
}
}
@Test
public void testDeleteCascadingWithOverridePathBasedReferentialIntegrityForDeletesInterceptorAlsoRegistered() throws IOException {
ourRestServer.getInterceptorService().registerInterceptor(myOverridePathBasedReferentialIntegrityForDeletesInterceptor);
try {
createResources();
ourRestServer.getInterceptorService().registerInterceptor(myDeleteInterceptor);
HttpDelete delete = new HttpDelete(ourServerBase + "/" + myPatientId.getValue() + "?" + Constants.PARAMETER_CASCADE_DELETE + "=" + Constants.CASCADE_DELETE + "&_pretty=true");
delete.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON_NEW);
try (CloseableHttpResponse response = ourHttpClient.execute(delete)) {
assertEquals(200, response.getStatusLine().getStatusCode());
String deleteResponse = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", deleteResponse);
assertThat(deleteResponse, containsString("Cascaded delete to "));
}
try {
ourLog.info("Reading {}", myPatientId);
myClient.read().resource(Patient.class).withId(myPatientId).execute();
fail();
} catch (ResourceGoneException e) {
// good
}
} finally {
ourRestServer.getInterceptorService().unregisterInterceptor(myOverridePathBasedReferentialIntegrityForDeletesInterceptor);
}
}
@Test
public void testDeleteCascadingWithCircularReference() throws IOException {

View File

@ -0,0 +1,141 @@
package ca.uhn.fhir.jpa.interceptor;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class OverridePathBasedReferentialIntegrityForDeletesInterceptorTest extends BaseJpaR4Test {
@Autowired
private OverridePathBasedReferentialIntegrityForDeletesInterceptor mySvc;
@Autowired
private CascadingDeleteInterceptor myCascadingDeleteInterceptor;
@AfterEach
public void after() {
myInterceptorRegistry.unregisterInterceptor(mySvc);
mySvc.clearPaths();
}
@Test
public void testDeleteBlockedIfNoInterceptorInPlace() {
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
try {
myPatientDao.delete(new IdType("Patient/P"));
fail();
} catch (ResourceVersionConflictException e) {
// good
}
}
@Test
public void testAllowDelete() {
mySvc.addPath("AuditEvent.agent.who");
myInterceptorRegistry.registerInterceptor(mySvc);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
// Delete should proceed
myPatientDao.delete(new IdType("Patient/P"));
// Make sure we're deleted
try {
myPatientDao.read(new IdType("Patient/P"));
fail();
} catch (ResourceGoneException e) {
// good
}
// Search should still work
IBundleProvider searchOutcome = myAuditEventDao.search(SearchParameterMap.newSynchronous(AuditEvent.SP_AGENT, new ReferenceParam("Patient/P")));
assertEquals(1, searchOutcome.size());
}
@Test
public void testWrongPath() {
mySvc.addPath("AuditEvent.identifier");
mySvc.addPath("Patient.agent.who");
myInterceptorRegistry.registerInterceptor(mySvc);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
// Delete should proceed
try {
myPatientDao.delete(new IdType("Patient/P"));
fail();
} catch (ResourceVersionConflictException e) {
// good
}
}
@Test
public void testCombineWithCascadeDeleteInterceptor() {
try {
myInterceptorRegistry.registerInterceptor(myCascadingDeleteInterceptor);
mySvc.addPath("AuditEvent.agent.who");
myInterceptorRegistry.registerInterceptor(mySvc);
Patient patient = new Patient();
patient.setId("P");
patient.setActive(true);
myPatientDao.update(patient);
AuditEvent audit = new AuditEvent();
audit.setId("A");
audit.addAgent().getWho().setReference("Patient/P");
myAuditEventDao.update(audit);
// Delete should proceed
myPatientDao.delete(new IdType("Patient/P"));
} finally {
myInterceptorRegistry.unregisterInterceptor(myCascadingDeleteInterceptor);
}
}
}

View File

@ -35,6 +35,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.dstu2.resource.SearchParameter;
import ca.uhn.fhir.model.dstu2.valueset.XPathUsageTypeEnum;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.gclient.NumberClientParam;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
@ -168,6 +174,52 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
}
}
/**
* See #2023
*/
@Test
public void testCustomNumberSearchParam() {
SearchParameter numberParameter = new SearchParameter();
numberParameter.setId("future-appointment-count");
numberParameter.setName("Future Appointment Count");
numberParameter.setCode("future-appointment-count");
numberParameter.setDescription("Count of future appointments for the patient");
numberParameter.setUrl("http://integer");
numberParameter.setStatus(ca.uhn.fhir.model.dstu2.valueset.ConformanceResourceStatusEnum.ACTIVE);
numberParameter.setBase(ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum.PATIENT);
numberParameter.setType(ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum.NUMBER);
numberParameter.setXpathUsage(XPathUsageTypeEnum.NORMAL);
numberParameter.setXpath("Patient.extension('http://integer')");
ourClient.update().resource(numberParameter).execute();
// This fires every 10 seconds
mySearchParamRegistry.refreshCacheIfNecessary();
Patient patient = new Patient();
patient.setId("future-appointment-count-pt");
patient.setActive(true);
patient.addUndeclaredExtension(false, "http://integer", new IntegerDt(2));
ourClient.update().resource(patient).execute();
Bundle futureAppointmentCountBundle2 = ourClient
.search()
.forResource(Patient.class)
.where(new NumberClientParam("future-appointment-count").greaterThan().number(1))
.returnBundle(Bundle.class)
.execute();
assertEquals(futureAppointmentCountBundle2.getTotal().intValue(), 1);
Bundle futureAppointmentCountBundle3 = ourClient
.search()
.forResource(Patient.class)
.where(new NumberClientParam("future-appointment-count").exactly().number(2))
.returnBundle(Bundle.class)
.execute();
assertEquals(futureAppointmentCountBundle3.getTotal().intValue(), 1);
}
/**
* See #484
*/

View File

@ -413,6 +413,8 @@ public class BinaryAccessProviderR4Test extends BaseResourceProviderR4Test {
}
/**
* Stores a binary large enough that it should live in binary storage
*/
@ -469,6 +471,77 @@ public class BinaryAccessProviderR4Test extends BaseResourceProviderR4Test {
}
@Test
public void testWriteLargeBinaryToDocumentReference() throws IOException {
byte[] bytes = new byte[134696];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (((float)Byte.MAX_VALUE) * Math.random());
}
DocumentReference dr = new DocumentReference();
dr.addContent().getAttachment()
.setContentType("application/pdf")
.setSize(12345)
.setTitle("hello")
.setCreationElement(new DateTimeType("2002"));
IIdType id = myClient.create().resource(dr).execute().getId().toUnqualifiedVersionless();
IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESHOW_RESOURCES, interceptor);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, interceptor);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, interceptor);
// Write using the operation
String path = ourServerBase +
"/DocumentReference/" + id.getIdPart() + "/" +
JpaConstants.OPERATION_BINARY_ACCESS_WRITE +
"?path=DocumentReference.content.attachment";
HttpPost post = new HttpPost(path);
post.setEntity(new ByteArrayEntity(bytes, ContentType.IMAGE_JPEG));
post.addHeader("Accept", "application/fhir+json; _pretty=true");
String attachmentId;
try (CloseableHttpResponse resp = ourHttpClient.execute(post)) {
assertEquals(200, resp.getStatusLine().getStatusCode());
assertThat(resp.getEntity().getContentType().getValue(), containsString("application/fhir+json"));
String response = IOUtils.toString(resp.getEntity().getContent(), Constants.CHARSET_UTF8);
ourLog.info("Response: {}", response);
DocumentReference target = myFhirCtx.newJsonParser().parseResource(DocumentReference.class, response);
assertEquals(null, target.getContentFirstRep().getAttachment().getData());
assertEquals("2", target.getMeta().getVersionId());
attachmentId = target.getContentFirstRep().getAttachment().getDataElement().getExtensionString(HapiExtensions.EXT_EXTERNALIZED_BINARY_ID);
assertThat(attachmentId, matchesPattern("[a-zA-Z0-9]{100}"));
}
verify(interceptor, timeout(5000).times(1)).invoke(eq(Pointcut.STORAGE_PRESHOW_RESOURCES), any());
verify(interceptor, timeout(5000).times(1)).invoke(eq(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED), any());
verifyNoMoreInteractions(interceptor);
// Read it back using the operation
path = ourServerBase +
"/DocumentReference/" + id.getIdPart() + "/" +
JpaConstants.OPERATION_BINARY_ACCESS_READ +
"?path=DocumentReference.content.attachment";
HttpGet get = new HttpGet(path);
try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
assertEquals(200, resp.getStatusLine().getStatusCode());
assertEquals("image/jpeg", resp.getEntity().getContentType().getValue());
assertEquals(bytes.length, resp.getEntity().getContentLength());
byte[] actualBytes = IOUtils.toByteArray(resp.getEntity().getContent());
assertArrayEquals(bytes, actualBytes);
}
}
private IIdType createDocumentReference(boolean theSetData) {
DocumentReference documentReference = new DocumentReference();
Attachment attachment = documentReference

View File

@ -8,11 +8,12 @@ import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.search.PersistedJpaSearchFirstPageBundleProvider;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.TestUtil;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BooleanType;
@ -23,7 +24,6 @@ import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
@ -36,7 +36,9 @@ import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class ExpungeR4Test extends BaseResourceProviderR4Test {
@ -410,16 +412,16 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test {
public void testExpungeEverythingWhereResourceInSearchResults() {
createStandardPatients();
await().until(()-> runInTransaction(() -> mySearchEntityDao.count() == 0));
await().until(()-> runInTransaction(() -> mySearchResultDao.count() == 0));
await().until(() -> runInTransaction(() -> mySearchEntityDao.count() == 0));
await().until(() -> runInTransaction(() -> mySearchResultDao.count() == 0));
PersistedJpaSearchFirstPageBundleProvider search = (PersistedJpaSearchFirstPageBundleProvider) myPatientDao.search(new SearchParameterMap());
assertEquals(PersistedJpaSearchFirstPageBundleProvider.class, search.getClass());
assertEquals(2, search.size().intValue());
assertEquals(2, search.getResources(0, 2).size());
await().until(()-> runInTransaction(() -> mySearchEntityDao.count() == 1));
await().until(()-> runInTransaction(() -> mySearchResultDao.count() == 2));
await().until(() -> runInTransaction(() -> mySearchEntityDao.count() == 1));
await().until(() -> runInTransaction(() -> mySearchResultDao.count() == 2));
mySystemDao.expunge(new ExpungeOptions()
.setExpungeEverything(true), null);
@ -465,4 +467,87 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test {
}
@Test
public void testExpungeForcedIdAndThenReuseIt() {
// Create with forced ID, and an Observation that links to it
Patient p = new Patient();
p.setId("TEST");
p.setActive(true);
p.addName().setFamily("FOO");
myPatientDao.update(p);
Observation obs = new Observation();
obs.setId("OBS");
obs.getSubject().setReference("Patient/TEST");
myObservationDao.update(obs);
// Make sure read works
p = myPatientDao.read(new IdType("Patient/TEST"));
assertTrue(p.getActive());
// Make sure search by ID works
IBundleProvider outcome = myPatientDao.search(SearchParameterMap.newSynchronous("_id", new TokenParam("Patient/TEST")));
p = (Patient) outcome.getResources(0, 1).get(0);
assertTrue(p.getActive());
// Make sure search by Reference works
outcome = myObservationDao.search(SearchParameterMap.newSynchronous(Observation.SP_SUBJECT, new ReferenceParam("Patient/TEST")));
obs = (Observation) outcome.getResources(0, 1).get(0);
assertEquals("OBS", obs.getIdElement().getIdPart());
// Delete and expunge
myObservationDao.delete(new IdType("Observation/OBS"));
myPatientDao.delete(new IdType("Patient/TEST"));
myPatientDao.expunge(new ExpungeOptions()
.setExpungeDeletedResources(true)
.setExpungeOldVersions(true), null);
myObservationDao.expunge(new ExpungeOptions()
.setExpungeDeletedResources(true)
.setExpungeOldVersions(true), null);
runInTransaction(() -> assertThat(myResourceTableDao.findAll(), empty()));
runInTransaction(() -> assertThat(myResourceHistoryTableDao.findAll(), empty()));
runInTransaction(() -> assertThat(myForcedIdDao.findAll(), empty()));
// Create again with the same forced ID
p = new Patient();
p.setId("TEST");
p.setActive(true);
p.addName().setFamily("FOO");
myPatientDao.update(p);
obs = new Observation();
obs.setId("OBS");
obs.getSubject().setReference("Patient/TEST");
myObservationDao.update(obs);
// Make sure read works
p = myPatientDao.read(new IdType("Patient/TEST"));
assertTrue(p.getActive());
// Make sure search works
outcome = myPatientDao.search(SearchParameterMap.newSynchronous("_id", new TokenParam("Patient/TEST")));
p = (Patient) outcome.getResources(0, 1).get(0);
assertTrue(p.getActive());
// Make sure search by Reference works
outcome = myObservationDao.search(SearchParameterMap.newSynchronous(Observation.SP_SUBJECT, new ReferenceParam("Patient/TEST")));
obs = (Observation) outcome.getResources(0, 1).get(0);
assertEquals("OBS", obs.getIdElement().getIdPart());
// Delete and expunge
myObservationDao.delete(new IdType("Observation/OBS"));
myPatientDao.delete(new IdType("Patient/TEST"));
myPatientDao.expunge(new ExpungeOptions()
.setExpungeDeletedResources(true)
.setExpungeOldVersions(true), null);
myObservationDao.expunge(new ExpungeOptions()
.setExpungeDeletedResources(true)
.setExpungeOldVersions(true), null);
runInTransaction(() -> assertThat(myResourceTableDao.findAll(), empty()));
runInTransaction(() -> assertThat(myResourceHistoryTableDao.findAll(), empty()));
runInTransaction(() -> assertThat(myForcedIdDao.findAll(), empty()));
}
}

View File

@ -1,13 +1,25 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.IDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.rest.api.MethodOutcome;
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.BooleanType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -15,16 +27,24 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HookInterceptorR4Test extends BaseResourceProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(HookInterceptorR4Test.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HookInterceptorR4Test.class);
@Autowired
IdHelperService myIdHelperService;
// @Override
// @AfterEach
// public void after( ) throws Exception {
// super.after();
//
// myInterceptorRegistry.unregisterAllInterceptors();
// }
@BeforeEach
public void before() throws Exception {
super.before();
myDaoConfig.setExpungeEnabled(true);
}
@AfterEach
public void after() throws Exception {
myDaoConfig.setExpungeEnabled(new DaoConfig().isExpungeEnabled());
super.after();
}
@Test
public void testOP_PRESTORAGE_RESOURCE_CREATED_ModifyResource() {
@ -69,7 +89,7 @@ public class HookInterceptorR4Test extends BaseResourceProviderR4Test {
AtomicLong pid = new AtomicLong();
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, (thePointcut, t) -> {
IAnyResource resource = (IAnyResource) t.get(IBaseResource.class, 0);
Long resourcePid = (Long) resource.getUserData("RESOURCE_PID");
Long resourcePid = (Long) resource.getUserData(IDao.RESOURCE_PID_KEY);
assertNotNull(resourcePid, "Expecting RESOURCE_PID to be set on resource user data.");
pid.set(resourcePid);
});
@ -77,6 +97,72 @@ public class HookInterceptorR4Test extends BaseResourceProviderR4Test {
assertTrue(pid.get() > 0);
}
@Test
public void testSTORAGE_PRECOMMIT_RESOURCE_CREATED_hasCorrectPid() {
AtomicLong pid = new AtomicLong();
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, (thePointcut, t) -> {
IAnyResource resource = (IAnyResource) t.get(IBaseResource.class, 0);
Long resourcePid = (Long) resource.getUserData(IDao.RESOURCE_PID_KEY);
assertNotNull(resourcePid, "Expecting RESOURCE_PID to be set on resource user data.");
pid.set(resourcePid);
});
IIdType savedPatientId = myClient.create().resource(new Patient()).execute().getId();
Long savedPatientPid = myIdHelperService.resolveResourcePersistentIdsWithCache(null, Collections.singletonList(savedPatientId)).get(0).getIdAsLong();
assertEquals(savedPatientPid.longValue(), pid.get());
}
@Test
public void testSTORAGE_PRESTORAGE_EXPUNGE_RESOURCE_hasCorrectPid() {
AtomicLong pid = new AtomicLong();
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, (thePointcut, t) -> {
IAnyResource resource = (IAnyResource) t.get(IBaseResource.class, 0);
Long resourcePid = (Long) resource.getUserData(IDao.RESOURCE_PID_KEY);
assertNotNull(resourcePid, "Expecting RESOURCE_PID to be set on resource user data.");
pid.set(resourcePid);
});
IIdType savedPatientId = myClient.create().resource(new Patient()).execute().getId();
Long savedPatientPid = myIdHelperService.resolveResourcePersistentIdsWithCache(null, Collections.singletonList(savedPatientId)).get(0).getIdAsLong();
myClient.delete().resourceById(savedPatientId).execute();
Parameters parameters = new Parameters();
parameters.addParameter().setName(JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES).setValue(new BooleanType(true));
myClient
.operation()
.onInstance(savedPatientId)
.named(JpaConstants.OPERATION_EXPUNGE)
.withParameters(parameters)
.execute();
assertEquals(savedPatientPid.longValue(), pid.get());
}
@Test
public void testSTORAGE_PRECOMMIT_RESOURCE_UPDATED_hasCorrectPid() {
AtomicLong pidOld = new AtomicLong();
AtomicLong pidNew = new AtomicLong();
Patient patient = new Patient();
IIdType savedPatientId = myClient.create().resource(patient).execute().getId();
patient.setId(savedPatientId);
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, (thePointcut, t) -> {
IAnyResource resourceOld = (IAnyResource) t.get(IBaseResource.class, 0);
IAnyResource resourceNew = (IAnyResource) t.get(IBaseResource.class, 1);
Long resourceOldPid = (Long) resourceOld.getUserData(IDao.RESOURCE_PID_KEY);
Long resourceNewPid = (Long) resourceNew.getUserData(IDao.RESOURCE_PID_KEY);
assertNotNull(resourceOldPid, "Expecting RESOURCE_PID to be set on resource user data.");
assertNotNull(resourceNewPid, "Expecting RESOURCE_PID to be set on resource user data.");
pidOld.set(resourceOldPid);
pidNew.set(resourceNewPid);
});
patient.setActive(true);
myClient.update().resource(patient).execute();
Long savedPatientPid = myIdHelperService.resolveResourcePersistentIdsWithCache(null, Collections.singletonList(savedPatientId)).get(0).getIdAsLong();
assertEquals(savedPatientPid.longValue(), pidOld.get());
assertEquals(savedPatientPid.longValue(), pidNew.get());
}
@Test
public void testSTORAGE_PRECOMMIT_RESOURCE_UPDATED_hasPid() {
AtomicLong oldPid = new AtomicLong();
@ -84,12 +170,12 @@ public class HookInterceptorR4Test extends BaseResourceProviderR4Test {
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, (thePointcut, t) -> {
IAnyResource oldResource = (IAnyResource) t.get(IBaseResource.class, 0);
Long oldResourcePid = (Long) oldResource.getUserData("RESOURCE_PID");
Long oldResourcePid = (Long) oldResource.getUserData(IDao.RESOURCE_PID_KEY);
assertNotNull(oldResourcePid, "Expecting RESOURCE_PID to be set on resource user data.");
oldPid.set(oldResourcePid);
IAnyResource newResource = (IAnyResource) t.get(IBaseResource.class, 1);
Long newResourcePid = (Long) newResource.getUserData("RESOURCE_PID");
Long newResourcePid = (Long) newResource.getUserData(IDao.RESOURCE_PID_KEY);
assertNotNull(newResourcePid, "Expecting RESOURCE_PID to be set on resource user data.");
newPid.set(newResourcePid);
});

View File

@ -2359,7 +2359,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(respString);
assertEquals(412, resp.getStatusLine().getStatusCode());
assertThat(respString, containsString("Profile reference \\\"http://foo/structuredefinition/myprofile\\\" could not be resolved, so has not been checked"));
assertThat(respString, containsString("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked"));
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -55,13 +55,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -24,8 +24,10 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
@ -38,8 +40,6 @@ import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.DatatypeUtil;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.SearchParameterUtil;
import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.lang3.StringUtils;
@ -51,6 +51,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -89,7 +90,8 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
private volatile long myLastRefresh;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
private IInterceptorService myInterceptorBroadcaster;
private RefreshSearchParameterCacheOnUpdate myInterceptor;
@Override
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
@ -236,8 +238,16 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
}
@PostConstruct
public void postConstruct() {
public void start() {
myBuiltInSearchParams = createBuiltInSearchParamMap(myFhirContext);
myInterceptor = new RefreshSearchParameterCacheOnUpdate();
myInterceptorBroadcaster.registerInterceptor(myInterceptor);
}
@PreDestroy
public void stop() {
myInterceptorBroadcaster.unregisterInterceptor(myInterceptor);
}
public int doRefresh(long theRefreshInterval) {
@ -376,16 +386,6 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
mySchedulerService.scheduleLocalJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail);
}
public static class Job implements HapiJob {
@Autowired
private ISearchParamRegistry myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.refreshCacheIfNecessary();
}
}
@Override
public boolean refreshCacheIfNecessary() {
if (myActiveSearchParams == null || System.currentTimeMillis() - REFRESH_INTERVAL > myLastRefresh) {
@ -402,30 +402,12 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
return Collections.unmodifiableMap(myActiveSearchParams);
}
public static Map<String, Map<String, RuntimeSearchParam>> createBuiltInSearchParamMap(FhirContext theFhirContext) {
Map<String, Map<String, RuntimeSearchParam>> resourceNameToSearchParams = new HashMap<>();
Set<String> resourceNames = theFhirContext.getResourceTypes();
for (String resourceName : resourceNames) {
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
String nextResourceName = nextResDef.getName();
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
resourceNameToSearchParams.put(nextResourceName, nameToParam);
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
nameToParam.put(nextSp.getName(), nextSp);
}
}
return Collections.unmodifiableMap(resourceNameToSearchParams);
}
/**
* All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder.
*
* @since 5.1.0
*/
@Override
public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
myPhoneticEncoder = thePhoneticEncoder;
@ -446,4 +428,58 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
searchParam.setPhoneticEncoder(myPhoneticEncoder);
}
}
@Interceptor
public class RefreshSearchParameterCacheOnUpdate {
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
public void created(IBaseResource theResource) {
handle(theResource);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
public void deleted(IBaseResource theResource) {
handle(theResource);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
public void updated(IBaseResource theResource) {
handle(theResource);
}
private void handle(IBaseResource theResource) {
if (theResource != null && myFhirContext.getResourceType(theResource).equals("SearchParameter")) {
requestRefresh();
}
}
}
public static class Job implements HapiJob {
@Autowired
private ISearchParamRegistry myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.refreshCacheIfNecessary();
}
}
public static Map<String, Map<String, RuntimeSearchParam>> createBuiltInSearchParamMap(FhirContext theFhirContext) {
Map<String, Map<String, RuntimeSearchParam>> resourceNameToSearchParams = new HashMap<>();
Set<String> resourceNames = theFhirContext.getResourceTypes();
for (String resourceName : resourceNames) {
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
String nextResourceName = nextResDef.getName();
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
resourceNameToSearchParams.put(nextResourceName, nameToParam);
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
nameToParam.put(nextSp.getName(), nextSp);
}
}
return Collections.unmodifiableMap(resourceNameToSearchParams);
}
}

View File

@ -96,31 +96,33 @@ public class SearchParameterCanonicalizer {
String path = theNextSp.getXpath();
RestSearchParameterTypeEnum paramType = null;
RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
switch (theNextSp.getTypeElement().getValueAsEnum()) {
case COMPOSITE:
paramType = RestSearchParameterTypeEnum.COMPOSITE;
break;
case DATE_DATETIME:
paramType = RestSearchParameterTypeEnum.DATE;
break;
case NUMBER:
paramType = RestSearchParameterTypeEnum.NUMBER;
break;
case QUANTITY:
paramType = RestSearchParameterTypeEnum.QUANTITY;
break;
case REFERENCE:
paramType = RestSearchParameterTypeEnum.REFERENCE;
break;
case STRING:
paramType = RestSearchParameterTypeEnum.STRING;
break;
case TOKEN:
paramType = RestSearchParameterTypeEnum.TOKEN;
break;
case URI:
paramType = RestSearchParameterTypeEnum.URI;
break;
if (theNextSp.getTypeElement().getValueAsEnum() != null) {
switch (theNextSp.getTypeElement().getValueAsEnum()) {
case COMPOSITE:
paramType = RestSearchParameterTypeEnum.COMPOSITE;
break;
case DATE_DATETIME:
paramType = RestSearchParameterTypeEnum.DATE;
break;
case NUMBER:
paramType = RestSearchParameterTypeEnum.NUMBER;
break;
case QUANTITY:
paramType = RestSearchParameterTypeEnum.QUANTITY;
break;
case REFERENCE:
paramType = RestSearchParameterTypeEnum.REFERENCE;
break;
case STRING:
paramType = RestSearchParameterTypeEnum.STRING;
break;
case TOKEN:
paramType = RestSearchParameterTypeEnum.TOKEN;
break;
case URI:
paramType = RestSearchParameterTypeEnum.URI;
break;
}
}
if (theNextSp.getStatus() != null) {
switch (theNextSp.getStatusElement().getValueAsEnum()) {

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
@ -46,7 +47,7 @@ public class SearchParamRegistryImplTest {
@MockBean
private ModelConfig myModelConfig;
@MockBean
private IInterceptorBroadcaster myInterceptorBroadcaster;
private IInterceptorService myInterceptorBroadcaster;
@Configuration
static class SpringConfig {

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -157,7 +157,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-converter</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
</dependency>
</dependencies>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jpa</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,12 +1,14 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.JettyUtil;
@ -29,11 +31,13 @@ import org.hl7.fhir.dstu3.model.OperationOutcome.IssueType;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -44,11 +48,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class ServerExceptionDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerExceptionDstu3Test.class);
public static BaseServerResponseException ourException;
public static Exception ourException;
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static int ourPort;
private static Server ourServer;
private static RestfulServer ourServlet;
@AfterEach
public void after() {
ourException = null;
}
@Test
public void testAddHeadersNotFound() throws Exception {
@ -56,8 +66,8 @@ public class ServerExceptionDstu3Test {
OperationOutcome operationOutcome = new OperationOutcome();
operationOutcome.addIssue().setCode(IssueType.BUSINESSRULE);
ourException = new ResourceNotFoundException("SOME MESSAGE");
ourException.addResponseHeader("X-Foo", "BAR BAR");
ourException = new ResourceNotFoundException("SOME MESSAGE")
.addResponseHeader("X-Foo", "BAR BAR");
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
@ -99,6 +109,65 @@ public class ServerExceptionDstu3Test {
}
@Test
public void testMethodThrowsNonHapiUncheckedExceptionHandledCleanly() throws Exception {
ourException = new NullPointerException("Hello");
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(500, status.getStatusLine().getStatusCode());
byte[] responseContentBytes = IOUtils.toByteArray(status.getEntity().getContent());
String responseContent = new String(responseContentBytes, Charsets.UTF_8);
ourLog.info(status.getStatusLine().toString());
ourLog.info(responseContent);
assertThat(responseContent, containsString("\"diagnostics\":\"Failed to call access method: java.lang.NullPointerException: Hello\""));
}
}
@Test
public void testMethodThrowsNonHapiCheckedExceptionHandledCleanly() throws Exception {
ourException = new IOException("Hello");
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(500, status.getStatusLine().getStatusCode());
byte[] responseContentBytes = IOUtils.toByteArray(status.getEntity().getContent());
String responseContent = new String(responseContentBytes, Charsets.UTF_8);
ourLog.info(status.getStatusLine().toString());
ourLog.info(responseContent);
assertThat(responseContent, containsString("\"diagnostics\":\"Failed to call access method: java.io.IOException: Hello\""));
}
}
@Test
public void testInterceptorThrowsNonHapiUncheckedExceptionHandledCleanly() throws Exception {
ourServlet.getInterceptorService().registerAnonymousInterceptor(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, new IAnonymousInterceptor() {
@Override
public void invoke(Pointcut thePointcut, HookParams theArgs) {
throw new NullPointerException("Hello");
}
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=json");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
assertEquals(500, status.getStatusLine().getStatusCode());
byte[] responseContentBytes = IOUtils.toByteArray(status.getEntity().getContent());
String responseContent = new String(responseContentBytes, Charsets.UTF_8);
ourLog.info(status.getStatusLine().toString());
ourLog.info(responseContent);
assertThat(responseContent, containsString("\"diagnostics\":\"Hello\""));
}
ourServlet.getInterceptorService().unregisterAllInterceptors();
}
@Test
public void testPostWithNoBody() throws IOException {
@ -143,7 +212,10 @@ public class ServerExceptionDstu3Test {
}
@Search()
public List<Patient> search() {
public List<Patient> search() throws Exception {
if (ourException == null) {
return Collections.emptyList();
}
throw ourException;
}
@ -168,15 +240,15 @@ public class ServerExceptionDstu3Test {
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
ourServlet = new RestfulServer(ourCtx);
ourServlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
ourServlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(ourServlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
ourPort = JettyUtil.getPortForStartedServer(ourServer);
ourPort = JettyUtil.getPortForStartedServer(ourServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -33,6 +33,7 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.r5.terminologies.ValueSetExpander;
import org.hl7.fhir.r5.utils.IResourceValidator;
import org.hl7.fhir.utilities.TranslationServices;
import org.hl7.fhir.utilities.cache.BasePackageCacheManager;
import org.hl7.fhir.utilities.cache.NpmPackage;
import org.hl7.fhir.utilities.i18n.I18nBase;
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
@ -171,6 +172,11 @@ public final class HapiWorkerContext extends I18nBase implements IWorkerContext
return validateCode(theOptions, system, code, display, theVs);
}
@Override
public void validateCodeBatch(ValidationOptions options, List<? extends CodingValidationRequest> codes, ValueSet vs) {
throw new UnsupportedOperationException();
}
@Override
public ValidationResult validateCode(ValidationOptions theOptions, String theSystem, String theCode, String theDisplay) {
IValidationSupport.CodeValidationResult result = myValidationSupport.validateCode(new ValidationSupportContext(myValidationSupport), convertConceptValidationOptions(theOptions), theSystem, theCode, theDisplay, null);
@ -406,7 +412,17 @@ public final class HapiWorkerContext extends I18nBase implements IWorkerContext
}
@Override
public void loadFromPackage(NpmPackage pi, IContextResourceLoader loader, String[] types) throws FHIRException {
public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader) throws FHIRException {
throw new UnsupportedOperationException();
}
@Override
public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader, String[] types) throws FHIRException {
throw new UnsupportedOperationException();
}
@Override
public int loadFromPackageAndDependencies(NpmPackage pi, IContextResourceLoader loader, BasePackageCacheManager pcm) throws FHIRException {
throw new UnsupportedOperationException();
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>5.1.0-SNAPSHOT</version>
<version>5.2.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -41,6 +41,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class BaseController {
@ -49,7 +50,7 @@ public class BaseController {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseController.class);
@Autowired
protected TesterConfig myConfig;
private Map<FhirVersionEnum, FhirContext> myContexts = new HashMap<FhirVersionEnum, FhirContext>();
private final Map<FhirVersionEnum, FhirContext> myContexts = new HashMap<FhirVersionEnum, FhirContext>();
private List<String> myFilterHeaders;
@Autowired
private ITemplateEngine myTemplateEngine;
@ -78,19 +79,6 @@ public class BaseController {
return loadAndAddConf(theServletRequest, theRequest, theModel);
}
private Header[] applyHeaderFilters(Header[] theAllHeaders) {
if (myFilterHeaders == null || myFilterHeaders.isEmpty()) {
return theAllHeaders;
}
ArrayList<Header> retVal = new ArrayList<Header>();
for (Header next : theAllHeaders) {
if (!myFilterHeaders.contains(next.getName().toLowerCase())) {
retVal.add(next);
}
}
return retVal.toArray(new Header[retVal.size()]);
}
private Header[] applyHeaderFilters(Map<String, List<String>> theAllHeaders) {
ArrayList<Header> retVal = new ArrayList<Header>();
for (String nextKey : theAllHeaders.keySet()) {
@ -274,7 +262,7 @@ public class BaseController {
}
protected RuntimeResourceDefinition getResourceType(HomeRequest theRequest, HttpServletRequest theReq) throws ServletException {
String resourceName = StringUtils.defaultString(theReq.getParameter(PARAM_RESOURCE));
String resourceName = sanitizeUrlPart(defaultString(theReq.getParameter(PARAM_RESOURCE)));
RuntimeResourceDefinition def = getContext(theRequest).getResourceDefinition(resourceName);
if (def == null) {
throw new ServletException("Invalid resourceName: " + resourceName);
@ -317,7 +305,7 @@ public class BaseController {
ca.uhn.fhir.model.dstu2.resource.Conformance conformance;
try {
conformance = (ca.uhn.fhir.model.dstu2.resource.Conformance) client.fetchConformance().ofType(Conformance.class).execute();
conformance = client.fetchConformance().ofType(Conformance.class).execute();
} catch (Exception e) {
ourLog.warn("Failed to load conformance statement, error was: {}", e.toString());
theModel.put("errorMsg", toDisplayError("Failed to load conformance statement, error was: " + e.toString(), e));
@ -326,7 +314,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(conformance));
Map<String, Number> resourceCounts = new HashMap<String, Number>();
Map<String, Number> resourceCounts = new HashMap<>();
long total = 0;
for (ca.uhn.fhir.model.dstu2.resource.Conformance.Rest nextRest : conformance.getRest()) {
for (ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource nextResource : nextRest.getResource()) {
@ -385,7 +373,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement));
Map<String, Number> resourceCounts = new HashMap<String, Number>();
Map<String, Number> resourceCounts = new HashMap<>();
long total = 0;
for (CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) {
@ -446,7 +434,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement));
Map<String, Number> resourceCounts = new HashMap<String, Number>();
Map<String, Number> resourceCounts = new HashMap<>();
long total = 0;
for (org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) {
@ -507,7 +495,7 @@ public class BaseController {
theModel.put("jsonEncodedConf", getContext(theRequest).newJsonParser().encodeResourceToString(capabilityStatement));
Map<String, Number> resourceCounts = new HashMap<String, Number>();
Map<String, Number> resourceCounts = new HashMap<>();
long total = 0;
for (org.hl7.fhir.r5.model.CapabilityStatement.CapabilityStatementRestComponent nextRest : capabilityStatement.getRest()) {
@ -614,25 +602,6 @@ public class BaseController {
protected void processAndAddLastClientInvocation(GenericClient theClient, ResultType theResultType, ModelMap theModelMap, long theLatency, String outcomeDescription,
CaptureInterceptor theInterceptor, HomeRequest theRequest) {
try {
// ApacheHttpRequest lastRequest = theInterceptor.getLastRequest();
// HttpResponse lastResponse = theInterceptor.getLastResponse();
// String requestBody = null;
// String requestUrl = lastRequest != null ? lastRequest.getApacheRequest().getURI().toASCIIString() : null;
// String action = lastRequest != null ? lastRequest.getApacheRequest().getMethod() : null;
// String resultStatus = lastResponse != null ? lastResponse.getStatusLine().toString() : null;
// String resultBody = StringUtils.defaultString(theInterceptor.getLastResponseBody());
//
// if (lastRequest instanceof HttpEntityEnclosingRequest) {
// HttpEntity entity = ((HttpEntityEnclosingRequest) lastRequest).getEntity();
// if (entity.isRepeatable()) {
// requestBody = IOUtils.toString(entity.getContent());
// }
// }
//
// ContentType ct = lastResponse != null ? ContentType.get(lastResponse.getEntity()) : null;
// String mimeType = ct != null ? ct.getMimeType() : null;
IHttpRequest lastRequest = theInterceptor.getLastRequest();
IHttpResponse lastResponse = theInterceptor.getLastResponse();
String requestBody = null;

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.to.model.HomeRequest;
import ca.uhn.fhir.to.model.ResourceRequest;
import ca.uhn.fhir.to.model.TransactionRequest;
import ca.uhn.fhir.util.UrlUtil;
import com.google.gson.stream.JsonWriter;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.CapabilityStatement;
@ -49,6 +50,7 @@ import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
import static ca.uhn.fhir.util.UrlUtil.sanitizeUrlPart;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -128,7 +130,7 @@ public class Controller extends BaseController {
return "resource";
}
String id = StringUtils.defaultString(theServletRequest.getParameter("resource-delete-id"));
String id = sanitizeUrlPart(defaultString(theServletRequest.getParameter("resource-delete-id")));
if (StringUtils.isBlank(id)) {
populateModelForResource(theServletRequest, theRequest, theModel);
theModel.put("errorMsg", toDisplayError("No ID specified", null));
@ -184,7 +186,7 @@ public class Controller extends BaseController {
FhirContext context = getContext(theRequest);
GenericClient client = theRequest.newClient(theReq, context, myConfig, interceptor);
String url = defaultString(theReq.getParameter("page-url"));
String url = sanitizeUrlPart(defaultString(theReq.getParameter("page-url")));
if (myConfig.isRefuseToFetchThirdPartyUrls()) {
if (!url.startsWith(theModel.get("base").toString())) {
ourLog.warn(logPrefix(theModel) + "Refusing to load page URL: {}", url);
@ -230,7 +232,7 @@ public class Controller extends BaseController {
theModel.put("errorMsg", toDisplayError(e.toString(), e));
return "resource";
}
String id = StringUtils.defaultString(theServletRequest.getParameter("id"));
String id = sanitizeUrlPart(defaultString(theServletRequest.getParameter("id")));
if (StringUtils.isBlank(id)) {
populateModelForResource(theServletRequest, theRequest, theModel);
theModel.put("errorMsg", toDisplayError("No ID specified", null));
@ -238,7 +240,7 @@ public class Controller extends BaseController {
}
ResultType returnsResource = ResultType.RESOURCE;
String versionId = StringUtils.defaultString(theServletRequest.getParameter("vid"));
String versionId = sanitizeUrlPart(defaultString(theServletRequest.getParameter("vid")));
String outcomeDescription;
if (StringUtils.isBlank(versionId)) {
versionId = null;
@ -353,7 +355,7 @@ public class Controller extends BaseController {
return "resource";
}
clientCodeJsonWriter.name("resource");
clientCodeJsonWriter.value(theServletRequest.getParameter("resource"));
clientCodeJsonWriter.value(sanitizeUrlPart(theServletRequest.getParameter("resource")));
} else {
query = search.forAllResources();
clientCodeJsonWriter.name("resource");
@ -394,7 +396,7 @@ public class Controller extends BaseController {
clientCodeJsonWriter.name("includes");
clientCodeJsonWriter.beginArray();
String[] incValues = theServletRequest.getParameterValues(Constants.PARAM_INCLUDE);
String[] incValues = sanitizeUrlPart(theServletRequest.getParameterValues(Constants.PARAM_INCLUDE));
if (incValues != null) {
for (String next : incValues) {
if (isNotBlank(next)) {
@ -407,7 +409,7 @@ public class Controller extends BaseController {
clientCodeJsonWriter.name("revincludes");
clientCodeJsonWriter.beginArray();
String[] revIncValues = theServletRequest.getParameterValues(Constants.PARAM_REVINCLUDE);
String[] revIncValues = sanitizeUrlPart(theServletRequest.getParameterValues(Constants.PARAM_REVINCLUDE));
if (revIncValues != null) {
for (String next : revIncValues) {
if (isNotBlank(next)) {
@ -418,7 +420,7 @@ public class Controller extends BaseController {
}
clientCodeJsonWriter.endArray();
String limit = theServletRequest.getParameter("resource-search-limit");
String limit = sanitizeUrlPart(theServletRequest.getParameter("resource-search-limit"));
if (isNotBlank(limit)) {
if (!limit.matches("[0-9]+")) {
populateModelForResource(theServletRequest, theRequest, theModel);
@ -434,13 +436,13 @@ public class Controller extends BaseController {
clientCodeJsonWriter.nullValue();
}
String[] sort = theServletRequest.getParameterValues("sort_by");
String[] sort = sanitizeUrlPart(theServletRequest.getParameterValues("sort_by"));
if (sort != null) {
for (String next : sort) {
if (isBlank(next)) {
continue;
}
String direction = theServletRequest.getParameter("sort_direction");
String direction = sanitizeUrlPart(theServletRequest.getParameter("sort_direction"));
if ("asc".equals(direction)) {
query.sort().ascending(new StringClientParam(next));
} else if ("desc".equals(direction)) {
@ -545,6 +547,7 @@ public class Controller extends BaseController {
type = def.getImplementingClass();
}
// Don't sanitize this param, it's a raw resource body and may well be XML
String body = validate ? theReq.getParameter("resource-validate-body") : theReq.getParameter("resource-create-body");
if (isBlank(body)) {
theModel.put("errorMsg", toDisplayError("No message body specified", null));
@ -583,7 +586,7 @@ public class Controller extends BaseController {
outcomeDescription = "Validate Resource";
client.validate().resource(resource).prettyPrint().execute();
} else {
String id = theReq.getParameter("resource-create-id");
String id = sanitizeUrlPart(theReq.getParameter("resource-create-id"));
if ("update".equals(theMethod)) {
outcomeDescription = "Update Resource";
client.update(id, resource);
@ -626,17 +629,17 @@ public class Controller extends BaseController {
if ("history-type".equals(theMethod)) {
RuntimeResourceDefinition def = getContext(theRequest).getResourceDefinition(theRequest.getResource());
type = def.getImplementingClass();
id = StringUtils.defaultString(theReq.getParameter("resource-history-id"));
id = sanitizeUrlPart(defaultString(theReq.getParameter("resource-history-id")));
}
DateTimeDt since = null;
String sinceStr = theReq.getParameter("since");
String sinceStr = sanitizeUrlPart(theReq.getParameter("since"));
if (isNotBlank(sinceStr)) {
since = new DateTimeDt(sinceStr);
}
Integer limit = null;
String limitStr = theReq.getParameter("limit");
String limitStr = sanitizeUrlPart(theReq.getParameter("limit"));
if (isNotBlank(limitStr)) {
limit = Integer.parseInt(limitStr);
}
@ -811,17 +814,17 @@ public class Controller extends BaseController {
}
private boolean handleSearchParam(String paramIdxString, HttpServletRequest theReq, IQuery theQuery, JsonWriter theClientCodeJsonWriter) throws IOException {
String nextName = theReq.getParameter("param." + paramIdxString + ".name");
String nextName = sanitizeUrlPart(theReq.getParameter("param." + paramIdxString + ".name"));
if (isBlank(nextName)) {
return false;
}
String nextQualifier = StringUtils.defaultString(theReq.getParameter("param." + paramIdxString + ".qualifier"));
String nextType = theReq.getParameter("param." + paramIdxString + ".type");
String nextQualifier = sanitizeUrlPart(defaultString(theReq.getParameter("param." + paramIdxString + ".qualifier")));
String nextType = sanitizeUrlPart(theReq.getParameter("param." + paramIdxString + ".type"));
List<String> parts = new ArrayList<String>();
for (int i = 0; i < 5; i++) {
parts.add(defaultString(theReq.getParameter("param." + paramIdxString + "." + i)));
parts.add(sanitizeUrlPart(defaultString(theReq.getParameter("param." + paramIdxString + "." + i))));
}
List<String> values;

View File

@ -48,13 +48,13 @@ public class FhirTesterConfig {
.addServer()
.withId("hapi")
.withFhirVersion(FhirVersionEnum.DSTU2)
.withBaseUrl("http://fhirtest.uhn.ca/baseDstu2")
.withBaseUrl("http://hapi.fhir.org/baseDstu2")
.withName("Public HAPI Test Server")
.allowsApiKey()
.addServer()
.withId("home3")
.withFhirVersion(FhirVersionEnum.DSTU3)
.withBaseUrl("http://fhirtest.uhn.ca/baseDstu3")
.withBaseUrl("http://hapi.fhir.org/baseDstu3")
.withName("Public HAPI Test Server (STU3)")
.addServer()
.withId("home")

Some files were not shown because too many files have changed in this diff Show More