diff --git a/README.md b/README.md index bf7df70bf1c..351737d7722 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/example-projects/hapi-fhir-jaxrs-sse/pom.xml b/example-projects/hapi-fhir-jaxrs-sse/pom.xml index d52505386c2..fe3b1066360 100644 --- a/example-projects/hapi-fhir-jaxrs-sse/pom.xml +++ b/example-projects/hapi-fhir-jaxrs-sse/pom.xml @@ -79,7 +79,7 @@ com.google.guava guava - 21.0 + 24.1.1 com.google.inject.extensions diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 2759c7efb46..81696e34d5c 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 0c9490e8fa9..12afb86318b 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 7e7c7779ce8..683e0307c87 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/NumberClientParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/NumberClientParam.java index c0d68e96b7a..715e8784bf0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/NumberClientParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/NumberClientParam.java @@ -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>() { @Override public ICriterion number(long theNumber) { - return new StringCriterion(getParamName(), Long.toString(theNumber)); + return new StringCriterion<>(getParamName(), Long.toString(theNumber)); } @Override public ICriterion number(String theNumber) { - return new StringCriterion(getParamName(), (theNumber)); + return new StringCriterion<>(getParamName(), (theNumber)); } }; } @@ -56,12 +56,12 @@ public class NumberClientParam extends BaseClientParam implements IParam { return new IMatches>() { @Override public ICriterion number(long theNumber) { - return new StringCriterion(getParamName(), ParamPrefixEnum.GREATERTHAN, Long.toString(theNumber)); + return new StringCriterion<>(getParamName(), ParamPrefixEnum.GREATERTHAN, Long.toString(theNumber)); } @Override public ICriterion number(String theNumber) { - return new StringCriterion(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>() { @Override public ICriterion number(long theNumber) { - return new StringCriterion(getParamName(), ParamPrefixEnum.GREATERTHAN_OR_EQUALS, Long.toString(theNumber)); + return new StringCriterion<>(getParamName(), ParamPrefixEnum.GREATERTHAN_OR_EQUALS, Long.toString(theNumber)); } @Override public ICriterion number(String theNumber) { - return new StringCriterion(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>() { @Override public ICriterion number(long theNumber) { - return new StringCriterion(getParamName(), ParamPrefixEnum.LESSTHAN, Long.toString(theNumber)); + return new StringCriterion<>(getParamName(), ParamPrefixEnum.LESSTHAN, Long.toString(theNumber)); } @Override public ICriterion number(String theNumber) { - return new StringCriterion(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>() { @Override public ICriterion number(long theNumber) { - return new StringCriterion(getParamName(), ParamPrefixEnum.LESSTHAN_OR_EQUALS, Long.toString(theNumber)); + return new StringCriterion<>(getParamName(), ParamPrefixEnum.LESSTHAN_OR_EQUALS, Long.toString(theNumber)); } @Override public ICriterion number(String theNumber) { - return new StringCriterion(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>() { @Override public ICriterion number(long theNumber) { - return new StringCriterion(getParamName(), thePrefix, Long.toString(theNumber)); + return new StringCriterion<>(getParamName(), thePrefix, Long.toString(theNumber)); } @Override public ICriterion number(String theNumber) { - return new StringCriterion(getParamName(), thePrefix, (theNumber)); + return new StringCriterion<>(getParamName(), thePrefix, (theNumber)); } }; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberParam.java index 3dc08ef5d1c..fc5003111e2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberParam.java @@ -56,7 +56,7 @@ public class NumberParam extends BaseParamWithPrefix implements IQu * Constructor * * @param theValue - * A string value, e.g. ">5.0" + * A string value, e.g. "gt5.0" */ public NumberParam(String theValue) { setValueAsQueryToken(null, null, null, theValue); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java index ecea91e02cc..56ceb01e010 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java @@ -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 " and * < characters in order to prevent injection attacks. - * + *

* The following characters are escaped: *

    *
  • '
  • @@ -379,7 +385,6 @@ public class UrlUtil { *
  • >
  • *
  • \n (newline)
  • *
- * */ 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 toQueryStringMap(HashMap> map) { HashMap retVal = new HashMap<>(); for (Entry> nextEntry : map.entrySet()) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index e6ff8222ab4..f522b95a2d2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -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(); diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 1029fa41c2a..fb1b44a18b3 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -3,14 +3,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index ddd8a7cb709..2a26f67a435 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index 3b8220a067b..1d2cc869360 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 500d69a18c5..912abaffdf6 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java index 1fdd38b4c75..d402c32bf43 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -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; diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java index 27c4da9cfed..f96ce78a509 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfigDstu3.java @@ -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; } + } diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java index 41da2296758..bb5a2674bd0 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -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)); + } } diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 32bb08fbd6a..3034ea56ad4 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 888527bfe95..d1822c5e435 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index af66c3e54e3..cc01952b9bf 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index d8cb452596d..527b69b5f03 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 6d75c54b1cb..f3d4517912b 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index b68ee82abb6..a677e4a818f 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -73,13 +73,13 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT compile ca.uhn.hapi.fhir hapi-fhir-jpaserver-subscription - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT compile @@ -96,7 +96,7 @@ ca.uhn.hapi.fhir hapi-fhir-testpage-overlay - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT classes diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1856-avoid-xml-seserialization-issue-for-subs-delivery.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1856-avoid-xml-seserialization-issue-for-subs-delivery.yaml index 77964969282..21e1630f358 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1856-avoid-xml-seserialization-issue-for-subs-delivery.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/1856-avoid-xml-seserialization-issue-for-subs-delivery.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi new file mode 100644 index 00000000000..5e2757c426a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2022-invalidate-caches-on-expunge.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2022-invalidate-caches-on-expunge.yaml new file mode 100644 index 00000000000..692ace07dc4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2022-invalidate-caches-on-expunge.yaml @@ -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!" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2023-force-sp-cache-flush-sooner.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2023-force-sp-cache-flush-sooner.yaml new file mode 100644 index 00000000000..fc5968f2043 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2023-force-sp-cache-flush-sooner.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2025-disable-referential-integrity-for-some-paths.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2025-disable-referential-integrity-for-some-paths.yaml new file mode 100644 index 00000000000..e70f23e29b8 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2025-disable-referential-integrity-for-some-paths.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2026-resolve-xss-testpage-vulnerability.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2026-resolve-xss-testpage-vulnerability.yaml new file mode 100644 index 00000000000..717b82c474d --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2026-resolve-xss-testpage-vulnerability.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/237-update-extension-metyods.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/237-update-extension-methods.yaml similarity index 94% rename from hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/237-update-extension-metyods.yaml rename to hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/237-update-extension-methods.yaml index 7b56f3c2a43..83e0e1a2296 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/237-update-extension-metyods.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/237-update-extension-methods.yaml @@ -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 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/version.yaml new file mode 100644 index 00000000000..8178f8fe4fb --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2020-08-13" +codename: "Manticore" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_2_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_2_0/changes.yaml new file mode 100644 index 00000000000..2b3be2df14c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_2_0/changes.yaml @@ -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): +
    +
  • Flyway (JPA): 6.4.1-> 6.5.4
  • +
" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/contributing/hacking_guide.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/contributing/hacking_guide.md index 69d9100446a..2c8d7b12074 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/contributing/hacking_guide.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/contributing/hacking_guide.md @@ -15,7 +15,7 @@ The following is a list of key subprojects you might open in your IDE: # Getting the Sources

- Build Status + Build Status

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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index 09b2bd28452..46fe776ec67 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -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. + + + +# 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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/interceptors.md index 20b86b004ba..c44e517a534 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/interceptors.md @@ -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 diff --git a/hapi-fhir-elasticsearch-6/pom.xml b/hapi-fhir-elasticsearch-6/pom.xml index e56854ab3bb..c6cb4b94077 100644 --- a/hapi-fhir-elasticsearch-6/pom.xml +++ b/hapi-fhir-elasticsearch-6/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-igpacks/pom.xml b/hapi-fhir-igpacks/pom.xml index d9e352d6413..cf3e52a767b 100644 --- a/hapi-fhir-igpacks/pom.xml +++ b/hapi-fhir-igpacks/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml 4.0.0 diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 1c0564d16b9..ec880668d86 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index ade618da234..663b4f2399c 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index a751d170992..3ad4c277bc0 100644 --- a/hapi-fhir-jaxrsserver-example/pom.xml +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-api/pom.xml b/hapi-fhir-jpaserver-api/pom.xml index e1e5748d398..1b5c56ba17c 100644 --- a/hapi-fhir-jpaserver-api/pom.xml +++ b/hapi-fhir-jpaserver-api/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IDao.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IDao.java index bc9f9e12396..ca1c690f468 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IDao.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IDao.java @@ -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"); diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DeleteConflictList.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DeleteConflictList.java index 74e40219307..84c2030a82d 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DeleteConflictList.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DeleteConflictList.java @@ -34,6 +34,7 @@ import java.util.function.Predicate; public class DeleteConflictList implements Iterable { private final List myList = new ArrayList<>(); private final Set myResourceIdsMarkedForDeletion; + private final Set myResourceIdsToIgnoreConflict; private int myRemoveModCount; /** @@ -41,6 +42,7 @@ public class DeleteConflictList implements Iterable { */ public DeleteConflictList() { myResourceIdsMarkedForDeletion = new HashSet<>(); + myResourceIdsToIgnoreConflict = new HashSet<>(); } /** @@ -49,6 +51,7 @@ public class DeleteConflictList implements Iterable { */ public DeleteConflictList(DeleteConflictList theParentList) { myResourceIdsMarkedForDeletion = theParentList.myResourceIdsMarkedForDeletion; + myResourceIdsToIgnoreConflict = theParentList.myResourceIdsToIgnoreConflict; } @@ -64,6 +67,18 @@ public class DeleteConflictList implements Iterable { 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); } diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 5dd85caa300..c7b123f83f8 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 36a02f2c08e..d6fc005a7c3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -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(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 0628ff48933..7afbb87d637 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -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 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 tags = theTagList; if (theEntity.isHasTags()) { @@ -708,7 +705,7 @@ public abstract class BaseHapiFhirDao 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 tags = theTagList; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 812f47b1dbf..5f34c1661ba 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -994,22 +994,25 @@ public abstract class BaseHapiFhirResourceDao 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); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java index 4672d60f996..c0ae4a899aa 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java @@ -51,6 +51,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao protected void postPersist(ResourceTable theEntity, SearchParameter theResource) { super.postPersist(theEntity, theResource); markAffectedResources(theResource); + } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java index 44b1148fa81..23c1fa34316 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceExpungeService.java @@ -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) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index 8ff45e38dba..55c1c9482e9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -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 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 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); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java index f7f1fdbc687..2cbbaafa165 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/delete/DeleteConflictService.java @@ -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 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); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java index fc8aa12388c..a2875454e06 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptor.java @@ -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); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java new file mode 100644 index 00000000000..e3e778cbb72 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptor.java @@ -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. + *

+ * For example, suppose this interceptor is configured with a path of AuditEvent.entity.what, + * and an AuditEvent resource exists in the repository that has a reference in that path to resource + * Patient/123. Normally this reference would prevent the Patient resource from being deleted unless + * the AuditEvent was first deleted as well (or a cascading delete was used). + * With this interceptor in place, the Patient resource could be deleted, and the AuditEvent would remain intact. + *

+ */ +@Interceptor +public class OverridePathBasedReferentialIntegrityForDeletesInterceptor { + + private static final Logger ourLog = LoggerFactory.getLogger(OverridePathBasedReferentialIntegrityForDeletesInterceptor.class); + private final Set 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. AuditEvent.agent.who + */ + 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 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 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; + } + } + + } + + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java index bdeb2e803ca..8687c414f01 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchCustomSearchParamTest.java @@ -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(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java index 620be635195..53da7983125 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java @@ -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")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index 3745e2f2b53..350bf1aa0ab 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -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")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java index 97cc8d26b42..1cac6951212 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4DeleteTest.java @@ -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() { + + + + } + + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index 7b935e91508..398f6aae40f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -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 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 732b0e451ae..9a6804f4020 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -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\": \"
\"\n" + + " \"div\": \"
HELLO
\"\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())); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CascadingDeleteInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptorTest.java similarity index 82% rename from hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CascadingDeleteInterceptorR4Test.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptorTest.java index 231d1ebc2de..afa85cc0522 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CascadingDeleteInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/CascadingDeleteInterceptorTest.java @@ -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 { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptorTest.java new file mode 100644 index 00000000000..14363d30a1a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/OverridePathBasedReferentialIntegrityForDeletesInterceptorTest.java @@ -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); + } + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java index 77c3c89e41d..76c30864d4a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/ResourceProviderDstu2Test.java @@ -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 */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java index f815d4cad34..a3832ebdb74 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BinaryAccessProviderR4Test.java @@ -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 diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java index ababc396273..2df8b6570a3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ExpungeR4Test.java @@ -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())); + + } + + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/HookInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/HookInterceptorR4Test.java index 040655c5158..80857380f8d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/HookInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/HookInterceptorR4Test.java @@ -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); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 5e9159f4c8f..5298e159403 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -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")); } } diff --git a/hapi-fhir-jpaserver-batch/pom.xml b/hapi-fhir-jpaserver-batch/pom.xml index 17d3851f73d..bc7b9f593d2 100644 --- a/hapi-fhir-jpaserver-batch/pom.xml +++ b/hapi-fhir-jpaserver-batch/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-empi/pom.xml b/hapi-fhir-jpaserver-empi/pom.xml index 7fcd22c274e..f657cd065d9 100644 --- a/hapi-fhir-jpaserver-empi/pom.xml +++ b/hapi-fhir-jpaserver-empi/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -55,13 +55,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java index f1b425ebc03..eddf6d8e439 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java @@ -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; diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index b01c017ec53..ffc9eabb3a6 100644 --- a/hapi-fhir-jpaserver-migrate/pom.xml +++ b/hapi-fhir-jpaserver-migrate/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 0d091ad7b4d..c31d6607a05 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index aca48a8eda0..7891d8247d5 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 46b41128f83..54844451975 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -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> createBuiltInSearchParamMap(FhirContext theFhirContext) { - Map> resourceNameToSearchParams = new HashMap<>(); - - Set resourceNames = theFhirContext.getResourceTypes(); - - for (String resourceName : resourceNames) { - RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName); - String nextResourceName = nextResDef.getName(); - HashMap 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> createBuiltInSearchParamMap(FhirContext theFhirContext) { + Map> resourceNameToSearchParams = new HashMap<>(); + + Set resourceNames = theFhirContext.getResourceTypes(); + + for (String resourceName : resourceNames) { + RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName); + String nextResourceName = nextResDef.getName(); + HashMap nameToParam = new HashMap<>(); + resourceNameToSearchParams.put(nextResourceName, nameToParam); + + for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) { + nameToParam.put(nextSp.getName(), nextSp); + } + } + return Collections.unmodifiableMap(resourceNameToSearchParams); + } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java index 0ce4a925e79..87f8ec0bc7d 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParameterCanonicalizer.java @@ -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()) { diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java index 43eab94aa59..789c6716570 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImplTest.java @@ -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 { diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 49cdbd5a904..8958aa6ad1d 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 22d8cf83731..aa04dd0888f 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 6da16e6154b..2f3c11b3f7c 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml @@ -157,7 +157,7 @@ ca.uhn.hapi.fhir hapi-fhir-converter - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT diff --git a/hapi-fhir-server-empi/pom.xml b/hapi-fhir-server-empi/pom.xml index 3a00cbe9979..b9759c1dda8 100644 --- a/hapi-fhir-server-empi/pom.xml +++ b/hapi-fhir-server-empi/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 2fb5c5bfc41..e57afa70464 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 20b28d9f538..7afdd3f9976 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 2aa29a39cf4..be4e5406fc7 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index 172179d3278..d29270ada82 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index f82983eca27..d450f6b60b5 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml index 1e775b24432..896111a3e97 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT hapi-fhir-spring-boot-sample-server-jpa diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index 313bbc066b5..143a8cc98b0 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT hapi-fhir-spring-boot-samples diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index 52916fdbdec..f773d09efd8 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 31be0e2d12f..5231c4407b3 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 50855a66acf..f79133f2e72 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 6b7d221dcf4..0b2352a3ffa 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 48a45e42d2a..7e6a2c78f03 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java index 0d9eb02415c..86163674490 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerExceptionDstu3Test.java @@ -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 search() { + public List 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(); diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 715cb77cfbf..d12aa7952d5 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index f20bd38071d..37bbf0c32d1 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 974483c63aa..7f631e12f32 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java index 897de87cd86..3bbc52fb524 100644 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java +++ b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java @@ -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 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(); } diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 974b9e9257c..0838d2e7d1a 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 28ef8c7af91..59023100bcb 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java index 3d855284db4..39647b449c8 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java @@ -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 myContexts = new HashMap(); + private final Map myContexts = new HashMap(); private List 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
retVal = new ArrayList
(); - for (Header next : theAllHeaders) { - if (!myFilterHeaders.contains(next.getName().toLowerCase())) { - retVal.add(next); - } - } - return retVal.toArray(new Header[retVal.size()]); - } - private Header[] applyHeaderFilters(Map> theAllHeaders) { ArrayList
retVal = new ArrayList
(); 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 resourceCounts = new HashMap(); + Map 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 resourceCounts = new HashMap(); + Map 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 resourceCounts = new HashMap(); + Map 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 resourceCounts = new HashMap(); + Map 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; diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java index c0af8e3ea98..3a8031f07ca 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java @@ -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 parts = new ArrayList(); 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 values; diff --git a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java index 035ff2caa16..8df0cd3f1f1 100644 --- a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java +++ b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java @@ -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") diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index dd5f6eea395..ac6d12d1177 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index 2970965dec0..36edd948d51 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index c269362f608..15b320aded4 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index 4c821400648..2a60ccd16c3 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 344f6ba2ca4..72a869a7a59 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 44818476928..8f4c0c9cf63 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java index 243fb93814e..9452feb433a 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java @@ -179,7 +179,7 @@ class ValidatorWrapper { i--; } - if (message.endsWith("\" could not be resolved, so has not been checked") && next.getLevel() == ValidationMessage.IssueSeverity.WARNING) { + if (message.endsWith("' could not be resolved, so has not been checked") && next.getLevel() == ValidationMessage.IssueSeverity.WARNING) { next.setLevel(ValidationMessage.IssueSeverity.ERROR); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index 848ab9e1d5c..0d4ea9f735c 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -29,6 +29,7 @@ import org.hl7.fhir.r5.model.ValueSet; 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; @@ -56,8 +57,8 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo private static final FhirContext ourR5Context = FhirContext.forR5(); private final ValidationSupportContext myValidationSupportContext; private final IVersionTypeConverter myModelConverter; - private volatile List myAllStructures; private final LoadingCache myFetchResourceCache; + private volatile List myAllStructures; private org.hl7.fhir.r5.model.Parameters myExpansionProfile; public VersionSpecificWorkerContextWrapper(ValidationSupportContext theValidationSupportContext, IVersionTypeConverter theModelConverter) { @@ -122,8 +123,18 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo } @Override - public void loadFromPackage(NpmPackage pi, IContextResourceLoader loader, String[] types) throws FHIRException { + public int loadFromPackage(NpmPackage pi, IContextResourceLoader loader) throws FileNotFoundException, IOException, 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 FileNotFoundException, IOException, FHIRException { + throw new UnsupportedOperationException(); } @Override @@ -529,6 +540,14 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return doValidation(convertedVs, validationOptions, system, code, display); } + @Override + public void validateCodeBatch(ValidationOptions options, List codes, ValueSet vs) { + for (CodingValidationRequest next : codes) { + ValidationResult outcome = validateCode(options, next.getCoding(), vs); + next.setResult(outcome); + } + } + @Nonnull private ValidationResult doValidation(IBaseResource theValueSet, ConceptValidationOptions theValidationOptions, String theSystem, String theCode, String theDisplay) { IValidationSupport.CodeValidationResult result; diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index 26524f3f3dd..35265595c7c 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -352,7 +352,7 @@ public class FhirInstanceValidatorDstu3Test { List all = logResultsAndReturnAll(result); assertEquals(1, all.size()); assertEquals(ResultSeverityEnum.ERROR, all.get(0).getSeverity()); - assertEquals("Unknown code 'urn:iso:std:iso:3166#QQ' for \"urn:iso:std:iso:3166#QQ\"", all.get(0).getMessage()); + assertEquals("Unknown code 'urn:iso:std:iso:3166#QQ' for 'urn:iso:std:iso:3166#QQ'", all.get(0).getMessage()); } } @@ -477,7 +477,7 @@ public class FhirInstanceValidatorDstu3Test { QuestionnaireResponse qr = loadResource("/dstu3/fmc02-questionnaireresponse-01.json", QuestionnaireResponse.class); ValidationResult result = myVal.validateWithResult(qr); List errors = logResultsAndReturnNonInformationalOnes(result); - assertThat(errors.get(0).getMessage(), containsString("Item has answer, even though it is not enabled (item id = \"BO_ConsDrop\")")); + assertThat(errors.get(0).getMessage(), containsString("Item has answer, even though it is not enabled (item id = 'BO_ConsDrop')")); assertEquals(1, errors.size()); } @@ -643,6 +643,9 @@ public class FhirInstanceValidatorDstu3Test { ourLog.info("Skipping logical type: {}", next.getId()); continue; } + if (sd.getUrl().equals("http://hl7.org/fhir/StructureDefinition/Resource")) { + continue; + } } ourLog.info("Validating {}", next.getId()); @@ -725,7 +728,7 @@ public class FhirInstanceValidatorDstu3Test { ValidationResult results = myVal.validateWithResult(is); List outcome = logResultsAndReturnNonInformationalOnes(results); assertEquals(1, outcome.size()); - assertEquals("Unknown code 'http://dicom.nema.org/resources/ontology/DCM#BAR' for \"http://dicom.nema.org/resources/ontology/DCM#BAR\"", outcome.get(0).getMessage()); + assertEquals("Unknown code 'http://dicom.nema.org/resources/ontology/DCM#BAR' for 'http://dicom.nema.org/resources/ontology/DCM#BAR'", outcome.get(0).getMessage()); // assertEquals("The Coding provided is not in the value set http://hl7.org/fhir/ValueSet/dicom-cid29, and a code should come from this value set unless it has no suitable code. (error message = Unknown code[BAR] in system[http://dicom.nema.org/resources/ontology/DCM])", outcome.get(1).getMessage()); } @@ -762,7 +765,7 @@ public class FhirInstanceValidatorDstu3Test { Patient resource = loadResource("/dstu3/nl/nl-core-patient-01.json", Patient.class); ValidationResult results = myVal.validateWithResult(resource); List outcome = logResultsAndReturnNonInformationalOnes(results); - assertThat(outcome.toString(), containsString("The Coding provided is not in the value set http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.1--20171231000000")); + assertThat(outcome.toString(), containsString("The Coding provided (urn:oid:2.16.840.1.113883.2.4.4.16.34#6030) is not in the value set http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.20.5.1--20171231000000")); } private void loadNL() throws IOException { @@ -1084,7 +1087,7 @@ public class FhirInstanceValidatorDstu3Test { List errors = logResultsAndReturnAll(output); assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity()); - assertEquals("Unknown code for \"http://loinc.org#12345\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://loinc.org#12345'", errors.get(0).getMessage()); } @Test @@ -1121,7 +1124,7 @@ public class FhirInstanceValidatorDstu3Test { myInstanceVal.setValidationSupport(myValidationSupport); ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); - assertThat(errors.toString(), containsString("Profile reference \"http://foo/structuredefinition/myprofile\" could not be resolved, so has not been checked")); + assertThat(errors.toString(), containsString("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked")); } @Test @@ -1165,7 +1168,7 @@ public class FhirInstanceValidatorDstu3Test { ValidationResult output = myVal.validateWithResult(input); logResultsAndReturnAll(output); assertEquals( - "The value provided (\"notvalidcode\") is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status), and a code is required from this value set) (error message = Unknown code 'notvalidcode')", + "The value provided ('notvalidcode') is not in the value set http://hl7.org/fhir/ValueSet/observation-status (http://hl7.org/fhir/ValueSet/observation-status), and a code is required from this value set) (error message = Unknown code 'notvalidcode')", output.getMessages().get(0).getMessage()); } @@ -1197,7 +1200,7 @@ public class FhirInstanceValidatorDstu3Test { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnAll(output); assertThat(errors.toString(), errors.size(), greaterThan(0)); - assertEquals("Unknown code for \"http://acme.org#9988877\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://acme.org#9988877'", errors.get(0).getMessage()); } @@ -1233,7 +1236,7 @@ public class FhirInstanceValidatorDstu3Test { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("Unknown code for \"http://loinc.org#1234\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://loinc.org#1234'", errors.get(0).getMessage()); } @Test diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 89b7d2dd3eb..95aecce3537 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -364,7 +364,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link0\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link0'")); } @Test @@ -490,7 +490,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString(" No response answer found for required item \"link1\"")); + assertThat(errors.toString(), containsString(" No response answer found for required item 'link1'")); } @Test @@ -532,7 +532,7 @@ public class QuestionnaireResponseValidatorDstu3Test { qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("HELLO")); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Item has answer, even though it is not enabled (item id = \"link1\")")); + assertThat(errors.toString(), containsString("Item has answer, even though it is not enabled (item id = 'link1')")); // link0 has an answer, and it's the right one qa = new QuestionnaireResponse(); @@ -541,7 +541,7 @@ public class QuestionnaireResponseValidatorDstu3Test { qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding("http://foo", "YES", null)); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link1\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link1'")); } @Test @@ -669,7 +669,7 @@ public class QuestionnaireResponseValidatorDstu3Test { // Without an answer ValidationResult errors = myVal.validateWithResult(qr); - assertThat(errors.toString(), containsString("No response answer found for required item \"link2\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link2'")); // With an answer qr.getItem().get(2).addAnswer().setValue(new StringType("AAA")); @@ -1064,7 +1064,7 @@ public class QuestionnaireResponseValidatorDstu3Test { errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Unknown code for \"http://codesystems.com/system#code1\"")); + assertThat(errors.toString(), containsString("Unknown code for 'http://codesystems.com/system#code1'")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item[0].answer[0]")); // Partial code @@ -1132,7 +1132,7 @@ public class QuestionnaireResponseValidatorDstu3Test { qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("")); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link0\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link0'")); } @Test @@ -1150,7 +1150,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); - assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + assertThat(errors.toString(), containsString("LinkId 'link1' not found in questionnaire")); } @Test @@ -1168,7 +1168,7 @@ public class QuestionnaireResponseValidatorDstu3Test { ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); - assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + assertThat(errors.toString(), containsString("LinkId 'link1' not found in questionnaire")); } @BeforeAll diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index 72c38ab01c7..48d5cb31eb7 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -320,7 +320,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult result = val.validateWithResult(p); List all = logResultsAndReturnErrorOnes(result); assertFalse(result.isSuccessful()); - assertEquals("The code \"AA \" is not valid (whitespace rules)", all.get(0).getMessage()); + assertEquals("The code 'AA ' is not valid (whitespace rules)", all.get(0).getMessage()); } @@ -524,7 +524,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myVal.validateWithResult(encoded); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("The value \"%%%2@()()\" is not a valid Base64 value", errors.get(0).getMessage()); + assertEquals("The value '%%%2@()()' is not a valid Base64 value", errors.get(0).getMessage()); } @@ -826,7 +826,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { " \"resourceType\":\"Patient\"," + " \"text\": {\n" + " \"status\": \"generated\",\n" + - " \"div\": \"
\"\n" + + " \"div\": \"
HELLO
\"\n" + " },\n" + " \"id\":\"123\"" + "}"; @@ -842,7 +842,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { "\"resourceType\":\"Patient\"," + " \"text\": {\n" + " \"status\": \"generated\",\n" + - " \"div\": \"
\"\n" + + " \"div\": \"
HELLO
\"\n" + " },\n" + "\"id\":\"123\"," + "\"foo\":\"123\"" + @@ -1111,7 +1111,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { List errors = logResultsAndReturnAll(output); assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity()); - assertEquals("Unknown code for \"http://loinc.org#12345\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://loinc.org#12345'", errors.get(0).getMessage()); } @Test @@ -1153,7 +1153,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("Profile reference \"http://foo/structuredefinition/myprofile\" could not be resolved, so has not been checked", errors.get(0).getMessage()); + assertEquals("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked", errors.get(0).getMessage()); assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity()); } @@ -1202,7 +1202,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myVal.validateWithResult(input); logResultsAndReturnAll(output); assertEquals( - "The value provided (\"notvalidcode\") is not in the value set http://hl7.org/fhir/ValueSet/observation-status|4.0.1 (http://hl7.org/fhir/ValueSet/observation-status), and a code is required from this value set) (error message = Unknown code 'notvalidcode')", + "The value provided ('notvalidcode') is not in the value set http://hl7.org/fhir/ValueSet/observation-status|4.0.1 (http://hl7.org/fhir/ValueSet/observation-status), and a code is required from this value set) (error message = Unknown code 'notvalidcode')", output.getMessages().get(0).getMessage()); } @@ -1266,7 +1266,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnAll(output); assertThat(errors.toString(), errors.size(), greaterThan(0)); - assertEquals("Unknown code for \"http://acme.org#9988877\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://acme.org#9988877'", errors.get(0).getMessage()); } @@ -1304,7 +1304,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("Unknown code for \"http://loinc.org#1234\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://loinc.org#1234'", errors.get(0).getMessage()); } @Test @@ -1354,8 +1354,8 @@ public class FhirInstanceValidatorR4Test extends BaseTest { output = myVal.validateWithResult(input); all = logResultsAndReturnNonInformationalOnes(output); assertEquals(2, all.size()); - assertThat(all.get(0).getMessage(), containsString("Validation failed for \"http://unitsofmeasure.org#Heck\"")); - assertThat(all.get(1).getMessage(), containsString("The value provided (\"Heck\") is not in the value set http://hl7.org/fhir/ValueSet/ucum-bodytemp")); + assertThat(all.get(0).getMessage(), containsString("Validation failed for 'http://unitsofmeasure.org#Heck'")); + assertThat(all.get(1).getMessage(), containsString("The value provided ('Heck') is not in the value set http://hl7.org/fhir/ValueSet/ucum-bodytemp")); } @@ -1453,7 +1453,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size(), errors.toString()); - assertThat(errors.get(0).getMessage(), containsString("The value provided (\"BLAH\") is not in the value set http://hl7.org/fhir/ValueSet/currencies")); + assertThat(errors.get(0).getMessage(), containsString("The value provided ('BLAH') is not in the value set http://hl7.org/fhir/ValueSet/currencies")); } @@ -1492,7 +1492,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { ValidationResult output = myVal.validateWithResult(allergy); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(0, errors.size(), errors.toString()); - assertThat(errors.get(0).getMessage(), containsString("The value provided (\"BLAH\") is not in the value set http://hl7.org/fhir/ValueSet/currencies")); + assertThat(errors.get(0).getMessage(), containsString("The value provided ('BLAH') is not in the value set http://hl7.org/fhir/ValueSet/currencies")); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java index e126ce078a5..152e911da2a 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/QuestionnaireResponseValidatorR4Test.java @@ -246,7 +246,7 @@ public class QuestionnaireResponseValidatorR4Test { errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Unknown code for \"http://codesystems.com/system#code1\"")); + assertThat(errors.toString(), containsString("Unknown code for 'http://codesystems.com/system#code1'")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item[0].answer[0]")); qa = new QuestionnaireResponse(); @@ -257,7 +257,7 @@ public class QuestionnaireResponseValidatorR4Test { errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Unknown code 'http://codesystems.com/system2#code3' for \"http://codesystems.com/system2#code3\"")); + assertThat(errors.toString(), containsString("Unknown code 'http://codesystems.com/system2#code3' for 'http://codesystems.com/system2#code3'")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item[0].answer[0]")); } @@ -319,7 +319,7 @@ public class QuestionnaireResponseValidatorR4Test { ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link0\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link0'")); } @Test @@ -641,7 +641,7 @@ public class QuestionnaireResponseValidatorR4Test { qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("")); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link0\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link0'")); } @@ -711,7 +711,7 @@ public class QuestionnaireResponseValidatorR4Test { ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); - assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + assertThat(errors.toString(), containsString("LinkId 'link1' not found in questionnaire")); } @Test @@ -729,7 +729,7 @@ public class QuestionnaireResponseValidatorR4Test { ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); - assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + assertThat(errors.toString(), containsString("LinkId 'link1' not found in questionnaire")); } @Test diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java index 23433a1018a..1ba19b530a8 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java @@ -232,7 +232,7 @@ public class FhirInstanceValidatorR5Test { ValidationResult result = val.validateWithResult(p); List all = logResultsAndReturnErrorOnes(result); assertFalse(result.isSuccessful()); - assertEquals("The code \"AA \" is not valid (whitespace rules)", all.get(0).getMessage()); + assertEquals("The code 'AA ' is not valid (whitespace rules)", all.get(0).getMessage()); } @@ -345,7 +345,7 @@ public class FhirInstanceValidatorR5Test { ValidationResult output = myVal.validateWithResult(encoded); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("The value \"%%%2@()()\" is not a valid Base64 value", errors.get(0).getMessage()); + assertEquals("The value '%%%2@()()' is not a valid Base64 value", errors.get(0).getMessage()); } @@ -451,7 +451,7 @@ public class FhirInstanceValidatorR5Test { " \"resourceType\":\"Patient\"," + " \"text\": {\n" + " \"status\": \"generated\",\n" + - " \"div\": \"
\"\n" + + " \"div\": \"
HELLO
\"\n" + " },\n" + " \"id\":\"123\"" + "}"; @@ -467,7 +467,7 @@ public class FhirInstanceValidatorR5Test { "\"resourceType\":\"Patient\"," + " \"text\": {\n" + " \"status\": \"generated\",\n" + - " \"div\": \"
\"\n" + + " \"div\": \"
HELLO
\"\n" + " },\n" + "\"id\":\"123\"," + "\"foo\":\"123\"" + @@ -673,7 +673,7 @@ public class FhirInstanceValidatorR5Test { ValidationResult output = myVal.validateWithResult(input); assertEquals(1, output.getMessages().size(), output.toString()); - assertEquals("This \"Patient\" cannot be parsed as a FHIR object (no namespace)", output.getMessages().get(0).getMessage()); + assertEquals("This 'Patient' cannot be parsed as a FHIR object (no namespace)", output.getMessages().get(0).getMessage()); ourLog.info(output.getMessages().get(0).getLocationString()); } @@ -755,7 +755,7 @@ public class FhirInstanceValidatorR5Test { List errors = logResultsAndReturnAll(output); assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity()); - assertEquals("Unknown code for \"http://loinc.org#12345\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://loinc.org#12345'", errors.get(0).getMessage()); } @Test @@ -795,7 +795,7 @@ public class FhirInstanceValidatorR5Test { myInstanceVal.setValidationSupport(myValidationSupport); ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); - assertThat(errors.toString(), containsString("Profile reference \"http://foo/structuredefinition/myprofile\" could not be resolved, so has not been checked")); + assertThat(errors.toString(), containsString("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked")); } @Test @@ -843,7 +843,7 @@ public class FhirInstanceValidatorR5Test { ValidationResult output = myVal.validateWithResult(input); logResultsAndReturnAll(output); assertEquals( - "The value provided (\"notvalidcode\") is not in the value set http://hl7.org/fhir/ValueSet/observation-status|4.4.0 (http://hl7.org/fhir/ValueSet/observation-status), and a code is required from this value set) (error message = Unknown code 'notvalidcode')", + "The value provided ('notvalidcode') is not in the value set http://hl7.org/fhir/ValueSet/observation-status|4.4.0 (http://hl7.org/fhir/ValueSet/observation-status), and a code is required from this value set) (error message = Unknown code 'notvalidcode')", output.getMessages().get(0).getMessage()); } @@ -877,7 +877,7 @@ public class FhirInstanceValidatorR5Test { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnAll(output); assertThat(errors.toString(), errors.size(), greaterThan(0)); - assertEquals("Unknown code for \"http://acme.org#9988877\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://acme.org#9988877'", errors.get(0).getMessage()); } @@ -915,7 +915,7 @@ public class FhirInstanceValidatorR5Test { ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("Unknown code for \"http://loinc.org#1234\"", errors.get(0).getMessage()); + assertEquals("Unknown code for 'http://loinc.org#1234'", errors.get(0).getMessage()); } @Test diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java index 86a33a1a248..11878ec05dd 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/QuestionnaireResponseValidatorR5Test.java @@ -250,7 +250,7 @@ public class QuestionnaireResponseValidatorR5Test { errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Unknown code for \"http://codesystems.com/system#code1\"")); + assertThat(errors.toString(), containsString("Unknown code for 'http://codesystems.com/system#code1'")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item[0].answer[0]")); qa = new QuestionnaireResponse(); @@ -261,7 +261,7 @@ public class QuestionnaireResponseValidatorR5Test { errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Unknown code 'http://codesystems.com/system2#code3' for \"http://codesystems.com/system2#code3\"")); + assertThat(errors.toString(), containsString("Unknown code 'http://codesystems.com/system2#code3' for 'http://codesystems.com/system2#code3'")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item[0].answer[0]")); } @@ -323,7 +323,7 @@ public class QuestionnaireResponseValidatorR5Test { ValidationResult errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link0\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link0'")); } @Test @@ -584,7 +584,7 @@ public class QuestionnaireResponseValidatorR5Test { qa.addItem().setLinkId("link0").addAnswer().setValue(new Coding().setDisplay("")); errors = myVal.validateWithResult(qa); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("No response answer found for required item \"link0\"")); + assertThat(errors.toString(), containsString("No response answer found for required item 'link0'")); } @@ -603,7 +603,7 @@ public class QuestionnaireResponseValidatorR5Test { ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); - assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + assertThat(errors.toString(), containsString("LinkId 'link1' not found in questionnaire")); } @Test @@ -621,7 +621,7 @@ public class QuestionnaireResponseValidatorR5Test { ourLog.info(errors.toString()); assertThat(errors.toString(), containsString(" - QuestionnaireResponse")); - assertThat(errors.toString(), containsString("LinkId \"link1\" not found in questionnaire")); + assertThat(errors.toString(), containsString("LinkId 'link1' not found in questionnaire")); } @Test diff --git a/hapi-fhir-validation/src/test/resources/bug872-ext-with-hl7-url.json b/hapi-fhir-validation/src/test/resources/bug872-ext-with-hl7-url.json index f05cd34e26d..0aa5d2e6ebf 100644 --- a/hapi-fhir-validation/src/test/resources/bug872-ext-with-hl7-url.json +++ b/hapi-fhir-validation/src/test/resources/bug872-ext-with-hl7-url.json @@ -2,7 +2,7 @@ "resourceType": "Patient", "text": { "status": "generated", - "div": "
" + "div": "
HELLO
" }, "extension": [ { diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 555a59caa36..650f877c7df 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml @@ -58,37 +58,37 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r5 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT org.apache.velocity diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 05f230210af..8c83b8dacbd 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index ddc34694707..502cc36b392 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io @@ -678,7 +678,7 @@ - 5.0.9 + 5.1.0 1.0.2 -Dfile.encoding=UTF-8 -Xmx2048m @@ -717,7 +717,7 @@ 9.4.30.v20200611 3.0.2 - 6.4.1 + 6.5.4 5.4.14.Final diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index 57d87df0199..647c5676335 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index a07fff46c3f..54aae250dc9 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 19d7b614cce..ccffbd5aa4e 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index 47360a5a445..a22c7e9f6fb 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.1.0-SNAPSHOT + 5.2.0-SNAPSHOT ../../pom.xml