From 44712dc287f17755f81c1b6d33c788edd22b576f Mon Sep 17 00:00:00 2001 From: Ari Ruotsalainen Date: Thu, 15 Nov 2018 15:36:57 +0200 Subject: [PATCH 01/56] Add maven wrapper for easier and more coherent builds https://www.baeldung.com/maven-wrapper --- .mvn/wrapper/MavenWrapperDownloader.java | 110 +++++++++ .mvn/wrapper/maven-wrapper.properties | 1 + mvnw | 286 +++++++++++++++++++++++ mvnw.cmd | 161 +++++++++++++ 4 files changed, 558 insertions(+) create mode 100755 .mvn/wrapper/MavenWrapperDownloader.java create mode 100755 .mvn/wrapper/maven-wrapper.properties create mode 100755 mvnw create mode 100755 mvnw.cmd diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 00000000000..fa4f7b499fd --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you 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. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 00000000000..00d32aab1d4 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 00000000000..5551fde8e7d --- /dev/null +++ b/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100755 index 00000000000..e5cfb0ae9ea --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From ee52d6fb31580549fea0defeb92da4150972b2a7 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 9 Jan 2019 20:20:46 -0600 Subject: [PATCH 02/56] Add SearchNarrowingInterceptor --- .../example/AuthorizationInterceptors.java | 61 +++- .../rest/gclient/ReferenceClientParam.java | 16 +- .../uhn/fhir/rest/param/BaseAndListParam.java | 11 +- .../ca/uhn/fhir/rest/param/ParameterUtil.java | 9 + .../fhir/instance/model/api/IAnyResource.java | 10 +- .../client/method/BaseQueryParameter.java | 2 +- hapi-fhir-server/pom.xml | 4 + .../auth/AuthorizationInterceptor.java | 2 + .../interceptor/auth/AuthorizedList.java | 84 +++++ .../auth/SearchNarrowingInterceptor.java | 198 ++++++++++++ .../fhir/rest/client/SearchClientTest.java | 225 +++++++------ .../auth/SearchNarrowingInterceptorTest.java | 300 ++++++++++++++++++ pom.xml | 5 +- src/changes/changes.xml | 6 + src/site/xdoc/doc_rest_server_security.xml | 40 ++- 15 files changed, 850 insertions(+), 123 deletions(-) create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java create mode 100644 hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java diff --git a/examples/src/main/java/example/AuthorizationInterceptors.java b/examples/src/main/java/example/AuthorizationInterceptors.java index 3ef5822b6d1..28d508a5b38 100644 --- a/examples/src/main/java/example/AuthorizationInterceptors.java +++ b/examples/src/main/java/example/AuthorizationInterceptors.java @@ -1,15 +1,11 @@ package example; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.util.List; - -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.instance.model.api.IBaseResource; - import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -17,6 +13,12 @@ import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.auth.*; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; @SuppressWarnings("unused") public class AuthorizationInterceptors { @@ -158,4 +160,47 @@ public class AuthorizationInterceptors { //END SNIPPET: patchAll } + + + //START SNIPPET: narrowing + public class MyPatientSearchNarrowingInterceptor extends SearchNarrowingInterceptor { + + /** + * This method must be overridden to provide the list of compartments + * and/or resources that the current user should have access to + */ + @Override + protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) { + // Process authorization header - The following is a fake + // implementation. Obviously we'd want something more real + // for a production scenario. + // + // In this basic example we have two hardcoded bearer tokens, + // one which is for a user that has access to one patient, and + // another that has full access. + String authHeader = theRequestDetails.getHeader("Authorization"); + if ("Bearer dfw98h38r".equals(authHeader)) { + + // This user will have access to two compartments + return new AuthorizedList() + .addCompartment("Patient/123") + .addCompartment("Patient/456"); + + } else if ("Bearer 39ff939jgg".equals(authHeader)) { + + // This user has access to everything + return new AuthorizedList(); + + } else { + + throw new AuthenticationException("Unknown bearer token"); + + } + + } + + } + //END SNIPPET: narrowing + + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java index 9a8cdcda269..7b2aba70b6b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ReferenceClientParam.java @@ -1,8 +1,10 @@ package ca.uhn.fhir.rest.gclient; import ca.uhn.fhir.context.FhirContext; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IIdType; +import java.util.Arrays; import java.util.Collection; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -97,7 +99,19 @@ public class ReferenceClientParam extends BaseClientParam implements IParam { public ICriterion hasAnyOfIds(Collection theIds) { return new StringCriterion<>(getParamName(), theIds); } - + + /** + * Match the referenced resource if the resource has ANY of the given IDs + * (this is an OR search, not an AND search), (this can be the logical ID or + * the absolute URL of the resource). Note that to specify an AND search, + * simply add a subsequent {@link IQuery#where(ICriterion) where} criteria + * with the same parameter. + */ + public ICriterion hasAnyOfIds(String... theIds) { + Validate.notNull(theIds, "theIds must not be null"); + return hasAnyOfIds(Arrays.asList(theIds)); + } + private static class ReferenceChainCriterion implements ICriterion, ICriterionInternal { private final String myResourceTypeQualifier; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java index b6533e892dc..fcb50e4c461 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.param; * 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. @@ -62,5 +62,10 @@ public abstract class BaseAndListParam> implement return myValues.toString(); } - + /** + * Returns the number of AND parameters + */ + public int size() { + return myValues.size(); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java index 7c74ece0228..c33bc9ed6b2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ParameterUtil.java @@ -20,8 +20,10 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /* * #%L @@ -208,6 +210,13 @@ public class ParameterUtil { || IPrimitiveType.class.isAssignableFrom(theClass); } + public static String escapeAndJoinOrList(Collection theValues) { + return theValues + .stream() + .map(ParameterUtil::escape) + .collect(Collectors.joining(",")); + } + public static int nonEscapedIndexOf(String theString, char theCharacter) { for (int i = 0; i < theString.length(); i++) { if (theString.charAt(i) == theCharacter) { diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java index 7dd655693b4..13ce67e35f5 100644 --- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java +++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IAnyResource.java @@ -29,14 +29,14 @@ public interface IAnyResource extends IBaseResource { * Search parameter constant for _language */ @SearchParamDefinition(name="_language", path="", description="The language of the resource", type="string" ) - public static final String SP_RES_LANGUAGE = "_language"; + String SP_RES_LANGUAGE = "_language"; /** * Search parameter constant for _id */ @SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="token" ) - public static final String SP_RES_ID = "_id"; + String SP_RES_ID = "_id"; /** * Fluent Client search parameter constant for _id @@ -46,7 +46,7 @@ public interface IAnyResource extends IBaseResource { * Path: Resource._id
*

*/ - public static final TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID); + TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID); String getId(); @@ -55,11 +55,11 @@ public interface IAnyResource extends IBaseResource { IPrimitiveType getLanguageElement(); - public Object getUserData(String name); + Object getUserData(String name); @Override IAnyResource setId(String theId); - public void setUserData(String name, Object value); + void setUserData(String name, Object value); } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseQueryParameter.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseQueryParameter.java index 2f056184b13..9e260e56755 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseQueryParameter.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/BaseQueryParameter.java @@ -81,7 +81,7 @@ public abstract class BaseQueryParameter implements IParameter { String paramName = isNotBlank(qualifier) ? getName() + qualifier : getName(); List paramValues = theTargetQueryArguments.get(paramName); if (paramValues == null) { - paramValues = new ArrayList(value.size()); + paramValues = new ArrayList<>(value.size()); theTargetQueryArguments.put(paramName, paramValues); } diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 3ea4bbff59f..570c20552f2 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -59,6 +59,10 @@ test + + org.apache.commons + commons-collections4 + diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 13679f9e4aa..4f0c9bdd7ed 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -54,6 +54,8 @@ import static org.apache.commons.lang3.StringUtils.defaultString; * Documentation on Server Security * for information on how to use this interceptor. *

+ * + * @see SearchNarrowingInterceptor */ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter implements IRuleApplier { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java new file mode 100644 index 00000000000..f99918e3274 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java @@ -0,0 +1,84 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.apache.commons.lang3.Validate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Return type for {@link SearchNarrowingInterceptor#buildAuthorizedList(RequestDetails)} + */ +public class AuthorizedList { + + private List myCompartments; + private List myResources; + + List getCompartments() { + return myCompartments; + } + + List getResources() { + return myResources; + } + + /** + * Adds a compartment that the user should be allowed to access + * + * @param theCompartment The compartment name, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 as well as Observations where Observation.subject="Patient/123"m, etc. + * @return Returns this for easy method chaining + */ + public AuthorizedList addCompartment(String theCompartment) { + Validate.notNull(theCompartment, "theCompartment must not be null"); + if (myCompartments == null) { + myCompartments = new ArrayList<>(); + } + myCompartments.add(theCompartment); + + return this; + } + + /** + * Adds a compartment that the user should be allowed to access + * + * @param theCompartments The compartment names, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 as well as Observations where Observation.subject="Patient/123"m, etc. + * @return Returns this for easy method chaining + */ + public AuthorizedList addCompartments(String... theCompartments) { + Validate.notNull(theCompartments, "theCompartments must not be null"); + for (String next : theCompartments) { + addCompartment(next); + } + return this; + } + + /** + * Adds a resource that the user should be allowed to access + * + * @param theResource The resource name, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 but not Observations where Observation.subject="Patient/123"m, etc. + * @return Returns this for easy method chaining + */ + public AuthorizedList addResource(String theResource) { + Validate.notNull(theResource, "theResource must not be null"); + if (myResources == null) { + myResources = new ArrayList<>(); + } + myResources.add(theResource); + + return this; + } + + /** + * Adds a resource that the user should be allowed to access + * + * @param theResources The resource names, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 but not Observations where Observation.subject="Patient/123"m, etc. + * @return Returns this for easy method chaining + */ + public AuthorizedList addResources(String... theResources) { + Validate.notNull(theResources, "theResources must not be null"); + for (String next : theResources) { + addResource(next); + } + return this; + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java new file mode 100644 index 00000000000..92bbb719633 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java @@ -0,0 +1,198 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.rest.api.QualifiedParamList; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.ParameterUtil; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.*; + +/** + * This interceptor can be used to automatically narrow the scope of searches in order to + * automatically restrict the searches to specific compartments. + *

+ * For example, this interceptor + * could be used to restrict a user to only viewing data belonging to Patient/123 (i.e. data + * in the Patient/123 compartment). In this case, a user performing a search + * for
+ * http://baseurl/Observation?category=laboratory
+ * would receive results as though they had requested
+ * http://baseurl/Observation?subject=Patient/123&category=laboratory + *

+ *

+ * Note that this interceptor should be used in combination with {@link AuthorizationInterceptor} + * if you are restricting results because of a security restriction. This interceptor is not + * intended to be a failsafe way of preventing users from seeing the wrong data (that is the + * purpose of AuthorizationInterceptor). This interceptor is simply intended as a convenience to + * help users simplify their queries while not receiving security errors for to trying to access + * data they do not have access to see. + *

+ * + * @see AuthorizationInterceptor + */ +public abstract class SearchNarrowingInterceptor extends InterceptorAdapter { + + /** + * Subclasses should override this method to supply the set of compartments that + * the user making the request should actually have access to. + *

+ * Typically this is done by examining theRequestDetails to find + * out who the current user is and then building a list of Strings. + *

+ * + * @param theRequestDetails The individual request currently being applied + */ + protected AuthorizedList buildAuthorizedList(@SuppressWarnings("unused") RequestDetails theRequestDetails) { + return new AuthorizedList(); + } + + + @Override + public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { + + // We don't support this operation type yet + Validate.isTrue(theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_SYSTEM); + + if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_TYPE) { + return true; + } + + FhirContext ctx = theRequestDetails.getServer().getFhirContext(); + RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName()); + HashMap> parameterToOrValues = new HashMap<>(); + AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails); + + /* + * Create a map of search parameter values that need to be added to the + * given request + */ + Collection compartments = authorizedList.getCompartments(); + if (compartments != null) { + processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, compartments, true); + } + Collection resources = authorizedList.getResources(); + if (resources != null) { + processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, resources, false); + } + + /* + * Add any param values to the actual request + */ + if (parameterToOrValues.size() > 0) { + Map newParameters = new HashMap<>(theRequestDetails.getParameters()); + for (Map.Entry> nextEntry : parameterToOrValues.entrySet()) { + String nextParamName = nextEntry.getKey(); + List nextAllowedValues = nextEntry.getValue(); + + if (!newParameters.containsKey(nextParamName)) { + + /* + * If we don't already have a parameter of the given type, add one + */ + String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues); + String[] paramValues = {nextValuesJoined}; + newParameters.put(nextParamName, paramValues); + + } else { + + /* + * If the client explicitly requested the given parameter already, we'll + * just update the request to have the intersection of the values that the client + * requested, and the values that the user is allowed to see + */ + String[] existingValues = newParameters.get(nextParamName); + boolean restrictedExistingList = false; + for (int i = 0; i < existingValues.length; i++) { + + String nextExistingValue = existingValues[i]; + List nextRequestedValues = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue); + List nextPermittedValues = ListUtils.intersection(nextRequestedValues, nextAllowedValues); + if (nextPermittedValues.size() > 0) { + restrictedExistingList = true; + existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues); + } + + } + + /* + * If none of the values that were requested by the client overlap at all + * with the values that the user is allowed to see, we'll just add the permitted + * list as a new list. Ultimately this scenario actually means that the client + * shouldn't get *any* results back, and adding a new AND parameter (that doesn't + * overlap at all with the others) is one way of ensuring that. + */ + if (!restrictedExistingList) { + String[] newValues = Arrays.copyOf(existingValues, existingValues.length + 1); + newValues[existingValues.length] = ParameterUtil.escapeAndJoinOrList(nextAllowedValues); + newParameters.put(nextParamName, newValues); + } + } + + } + theRequestDetails.setParameters(newParameters); + } + + return true; + } + + private void processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, HashMap> theParameterToOrValues, Collection theResourcesOrCompartments, boolean theAreCompartments) { + String lastCompartmentName = null; + String lastSearchParamName=null; + for (String nextCompartment : theResourcesOrCompartments) { + Validate.isTrue(StringUtils.countMatches(nextCompartment, '/') == 1, "Invalid compartment name (must be in form \"ResourceType/xxx\": %s", nextCompartment); + String compartmentName = nextCompartment.substring(0, nextCompartment.indexOf('/')); + + String searchParamName = null; + if (compartmentName.equalsIgnoreCase(lastCompartmentName)) { + + // Avoid doing a lookup for the same thing repeatedly + searchParamName = lastSearchParamName; + + } else { + + if (compartmentName.equalsIgnoreCase(theRequestDetails.getResourceName())) { + + searchParamName = "_id"; + + } else if (theAreCompartments) { + + List searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName); + if (searchParams.size() > 0) { + + // Resources like Observation have several fields that add the resource to + // the compartment. In the case of Observation, it's subject, patient and performer. + // For this kind of thing, we'll prefer the one called "patient". + RuntimeSearchParam searchParam = + searchParams + .stream() + .filter(t -> t.getName().equalsIgnoreCase(compartmentName)) + .findFirst() + .orElse(searchParams.get(0)); + searchParamName = searchParam.getName(); + + } + } + + lastCompartmentName = compartmentName; + lastSearchParamName = searchParamName; + + } + + if (searchParamName != null) { + List orValues = theParameterToOrValues.computeIfAbsent(searchParamName, t -> new ArrayList<>()); + orValues.add(nextCompartment); + } + } + } + +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/SearchClientTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/SearchClientTest.java index 37375c04f9c..4c2428d7530 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/SearchClientTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/SearchClientTest.java @@ -1,16 +1,19 @@ package ca.uhn.fhir.rest.client; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.StringReader; -import java.nio.charset.Charset; -import java.util.*; - +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.rest.annotation.IncludeParam; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.ReaderInputStream; import org.apache.http.HttpResponse; @@ -23,118 +26,144 @@ import org.apache.http.message.BasicStatusLine; import org.hamcrest.Matchers; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Encounter; -import org.junit.*; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.rest.annotation.*; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.client.api.IBasicClient; -import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.util.TestUtil; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class SearchClientTest { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchClientTest.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchClientTest.class); - private FhirContext ourCtx; - private HttpClient ourHttpClient; - private HttpResponse ourHttpResponse; + private FhirContext ourCtx; + private HttpClient ourHttpClient; + private HttpResponse ourHttpResponse; - @Before - public void before() { - ourCtx = FhirContext.forR4(); + @Before + public void before() { + ourCtx = FhirContext.forR4(); - ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); - ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient); - ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); - } + ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + } - @Test - public void testPostOnLongParamsList() throws Exception { - String resp = createBundle(); + @Test + public void testPostOnLongParamsList() throws Exception { + String resp = createBundle(); - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse); - when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); - when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse); + when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); + when(ourHttpResponse.getEntity().getContent()).thenAnswer(t->new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Set includes = new HashSet(); - includes.add(new Include("one")); - includes.add(new Include("two")); - TokenOrListParam params = new TokenOrListParam(); - for (int i = 0; i < 1000; i++) { - params.add(new TokenParam("system", "value")); - } - List found = client.searchByList(params, includes); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Set includes = new HashSet(); + includes.add(new Include("one")); + includes.add(new Include("two")); + TokenOrListParam params = new TokenOrListParam(); + for (int i = 0; i < 1000; i++) { + params.add(new TokenParam("system", "value")); + } - assertEquals(1, found.size()); + // With OR list - Encounter encounter = found.get(0); - assertNotNull(encounter.getSubject().getReference()); - HttpUriRequest value = capt.getValue(); + List found = client.searchByList(params, includes); - assertTrue("Expected request of type POST on long params list", value instanceof HttpPost); - HttpPost post = (HttpPost) value; - String body = IOUtils.toString(post.getEntity().getContent()); - ourLog.info(body); - assertThat(body, Matchers.containsString("_include=one")); - assertThat(body, Matchers.containsString("_include=two")); - } + assertEquals(1, found.size()); - @Test - public void testReturnTypedList() throws Exception { - - String resp = createBundle(); + Encounter encounter = found.get(0); + assertNotNull(encounter.getSubject().getReference()); + HttpUriRequest value = capt.getValue(); - ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse); - when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); - when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); + assertTrue("Expected request of type POST on long params list", value instanceof HttpPost); + HttpPost post = (HttpPost) value; + String body = IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(body); + assertThat(body, Matchers.containsString("_include=one")); + assertThat(body, Matchers.containsString("_include=two")); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - List found = client.search(); - assertEquals(1, found.size()); + // With AND list - Encounter encounter = found.get(0); - assertNotNull(encounter.getSubject().getReference()); - } + TokenAndListParam paramsAndList = new TokenAndListParam(); + paramsAndList.addAnd(params); + found = client.searchByList(paramsAndList, includes); - private String createBundle() { - Bundle bundle = new Bundle(); - - Encounter enc = new Encounter(); - enc.getSubject().setReference("Patient/1"); - - bundle.addEntry().setResource(enc); - - String retVal = ourCtx.newXmlParser().encodeResourceToString(bundle); - return retVal; - } + assertEquals(1, found.size()); - private interface ITestClient extends IBasicClient { + encounter = found.get(0); + assertNotNull(encounter.getSubject().getReference()); + value = capt.getAllValues().get(1); - @Search - List search(); + assertTrue("Expected request of type POST on long params list", value instanceof HttpPost); + post = (HttpPost) value; + body = IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8); + ourLog.info(body); + assertThat(body, Matchers.containsString("_include=one")); + assertThat(body, Matchers.containsString("_include=two")); + } - @Search - List searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set theIncludes) throws BaseServerResponseException; + @Test + public void testReturnTypedList() throws Exception { - } + String resp = createBundle(); - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse); + when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); + when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + List found = client.search(); + assertEquals(1, found.size()); + + Encounter encounter = found.get(0); + assertNotNull(encounter.getSubject().getReference()); + } + + private String createBundle() { + Bundle bundle = new Bundle(); + + Encounter enc = new Encounter(); + enc.getSubject().setReference("Patient/1"); + + bundle.addEntry().setResource(enc); + + String retVal = ourCtx.newXmlParser().encodeResourceToString(bundle); + return retVal; + } + + private interface ITestClient extends IBasicClient { + + @Search + List search(); + + @Search + List searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set theIncludes) throws BaseServerResponseException; + + @Search + List searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenAndListParam tokenOrListParam, @IncludeParam Set theIncludes) throws BaseServerResponseException; + + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java new file mode 100644 index 00000000000..f0ce3456333 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java @@ -0,0 +1,300 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IQueryParameterOr; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.param.BaseAndListParam; +import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.lang3.Validate; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hamcrest.Matchers; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Resource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class SearchNarrowingInterceptorTest { + + private static String ourLastHitMethod; + private static FhirContext ourCtx; + private static TokenAndListParam ourLastIdParam; + private static TokenAndListParam ourLastCodeParam; + private static ReferenceAndListParam ourLastSubjectParam; + private static ReferenceAndListParam ourLastPatientParam; + private static ReferenceAndListParam ourLastPerformerParam; + private static StringAndListParam ourLastNameParam; + private static List ourReturn; + private static Server ourServer; + private static IGenericClient ourClient; + private static AuthorizedList ourNextCompartmentList; + + @Before + public void before() { + ourLastHitMethod = null; + ourReturn = Collections.emptyList(); + ourLastIdParam = null; + ourLastNameParam = null; + ourLastSubjectParam = null; + ourLastPatientParam = null; + ourLastPerformerParam = null; + ourLastCodeParam = null; + ourNextCompartmentList = null; + } + + @Test + public void testNarrowObservationsByPatientContext_ClientRequestedNoParams() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Observation") + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertNull(ourLastIdParam); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/123,Patient/456")); + } + + /** + * Should not make any changes + */ + @Test + public void testNarrowObservationsByPatientResources_ClientRequestedNoParams() { + + ourNextCompartmentList = new AuthorizedList().addResources("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Observation") + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertNull(ourLastIdParam); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + } + + @Test + public void testNarrowPatientByPatientResources_ClientRequestedNoParams() { + + ourNextCompartmentList = new AuthorizedList().addResources("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Patient") + .execute(); + + assertEquals("Patient.search", ourLastHitMethod); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456")); + } + + @Test + public void testNarrowPatientByPatientContext_ClientRequestedNoParams() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Patient") + .execute(); + + assertEquals("Patient.search", ourLastHitMethod); + assertNull(ourLastNameParam); + assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456")); + } + + @Test + public void testNarrowPatientByPatientContext_ClientRequestedSomeOverlap() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Patient") + .where(IAnyResource.RES_ID.exactly().codes("Patient/123", "Patient/999")) + .execute(); + + assertEquals("Patient.search", ourLastHitMethod); + assertNull(ourLastNameParam); + assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123")); + } + + @Test + public void testNarrowObservationsByPatientContext_ClientRequestedSomeOverlap() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Observation") + .where(Observation.PATIENT.hasAnyOfIds("Patient/456", "Patient/777")) + .and(Observation.PATIENT.hasAnyOfIds("Patient/456", "Patient/888")) + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertNull(ourLastIdParam); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/456", "Patient/456")); + } + + @Test + public void testNarrowObservationsByPatientContext_ClientRequestedNoOverlap() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Observation") + .where(Observation.PATIENT.hasAnyOfIds("Patient/111", "Patient/777")) + .and(Observation.PATIENT.hasAnyOfIds("Patient/111", "Patient/888")) + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertNull(ourLastIdParam); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/111,Patient/777", "Patient/111,Patient/888", "Patient/123,Patient/456")); + } + + private List toStrings(BaseAndListParam> theParams) { + List> valuesAsQueryTokens = theParams.getValuesAsQueryTokens(); + + return valuesAsQueryTokens + .stream() + .map(IQueryParameterOr::getValuesAsQueryTokens) + .map(t -> t + .stream() + .map(j -> j.getValueAsQueryToken(ourCtx)) + .collect(Collectors.joining(","))) + .collect(Collectors.toList()); + } + + public static class DummyPatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search() + public List search( + @OptionalParam(name = "_id") TokenAndListParam theIdParam, + @OptionalParam(name = "name") StringAndListParam theNameParam + ) { + ourLastHitMethod = "Patient.search"; + ourLastIdParam = theIdParam; + ourLastNameParam = theNameParam; + return ourReturn; + } + + } + + public static class DummyObservationResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Observation.class; + } + + + @Search() + public List search( + @OptionalParam(name = "_id") TokenAndListParam theIdParam, + @OptionalParam(name = Observation.SP_SUBJECT) ReferenceAndListParam theSubjectParam, + @OptionalParam(name = Observation.SP_PATIENT) ReferenceAndListParam thePatientParam, + @OptionalParam(name = Observation.SP_PERFORMER) ReferenceAndListParam thePerformerParam, + @OptionalParam(name = "code") TokenAndListParam theCodeParam + ) { + ourLastHitMethod = "Observation.search"; + ourLastIdParam = theIdParam; + ourLastSubjectParam = theSubjectParam; + ourLastPatientParam = thePatientParam; + ourLastPerformerParam = thePerformerParam; + ourLastCodeParam = theCodeParam; + return ourReturn; + } + + } + + private static class MySearchNarrowingInterceptor extends SearchNarrowingInterceptor { + @Override + protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) { + Validate.notNull(ourNextCompartmentList); + return ourNextCompartmentList; + } + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forR4(); + + int ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider(); + DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer ourServlet = new RestfulServer(ourCtx); + ourServlet.setFhirContext(ourCtx); + ourServlet.setResourceProviders(patProvider, obsProv); + ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100)); + ourServlet.registerInterceptor(new MySearchNarrowingInterceptor()); + ServletHolder servletHolder = new ServletHolder(ourServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourCtx.getRestfulClientFactory().setSocketTimeout(1000000); + ourClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort); + } + + +} diff --git a/pom.xml b/pom.xml index 91b544b88fb..3754403f1f0 100644 --- a/pom.xml +++ b/pom.xml @@ -519,6 +519,7 @@ 3.8.1 10.14.2.0 2.0.18 + 2.3.2 25.0-jre 2.8.5 2.2.11_1 @@ -639,7 +640,7 @@ com.google.errorprone error_prone_core - 2.3.2 + ${error_prone_core_version} com.google.guava @@ -2308,7 +2309,7 @@ com.google.errorprone error_prone_core - 2.3.2 + ${error_prone_core_version} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7f77730f380..d68cb4989ec 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -264,6 +264,12 @@ OperationDefinitions are now created for named queries in server module. Thanks to Stig Døssing for the pull request! + + A new server interceptor has been added called "SearchNarrowingInterceptor". + This interceptor can be used to automatically narrow the scope of searches + performed by the user to limit them to specific resources or compartments + that the user should have access to. + diff --git a/src/site/xdoc/doc_rest_server_security.xml b/src/site/xdoc/doc_rest_server_security.xml index 111ba9186ea..1264ac5c504 100644 --- a/src/site/xdoc/doc_rest_server_security.xml +++ b/src/site/xdoc/doc_rest_server_security.xml @@ -96,10 +96,10 @@

- AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet - been heavily tested. Use with caution, and do lots of testing! We welcome - feedback and suggestions on this feature. In addition, this documentation is - not yet complete. More examples and details will be added soon! Please get in + AuthorizationInterceptor has been well tested, but it is impossible to + predeict every scenario and environment in which HAPI FHIR will be used. + Use with caution, and do lots of testing! We welcome + feedback and suggestions on this feature. Please get in touch if you'd like to help test, have suggestions, etc.

@@ -253,7 +253,37 @@ - + +
+ +

+ HAPI FHIR 3.7.0 introduced a new interceptor, the + SearchNarrowingInterceptor. +

+

+ This interceptor is designed to be used in conjunction with AuthorizationInterceptor. It + uses a similar strategy where a dynamic list is built up for each request, but the + purpose of this interceptor is to modify client searches that are received (after + HAPI FHIR received the HTTP request, but before the search is actually performed) + to restrict the search to only search for specific resources or compartments that the + user has access to. +

+

+ This could be used, for example, to allow the user to perform a search for
+ http://baseurl/Observation?category=laboratory
+ and then receive results as though they had requested
+ http://baseurl/Observation?subject=Patient/123&category=laboratory. +

+

+ An example of this interceptor follows: +

+ + + + + +
+ From b8f200f89755d9c300dd4f4c3cf4e961521ae241 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 10 Jan 2019 06:15:31 -0700 Subject: [PATCH 03/56] Correctly expose chains in DSTU2 server conformance statmeent --- .../dstu2/ServerConformanceProvider.java | 22 ++- .../ServerConformanceProviderDstu2Test.java | 52 ++++++ ...rCapabilityStatementProviderDstu3Test.java | 163 +++++++++++------- src/changes/changes.xml | 7 + 4 files changed, 178 insertions(+), 66 deletions(-) diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index 9fdd410d46c..99d863f578b 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -387,16 +387,24 @@ public class ServerConformanceProvider implements IServerConformanceProvider t.getName().equals(finalNextParamUnchainedName)) + .findFirst() + .orElseGet(() -> resource.addSearchParam()); + param.setName(nextParamUnchainedName); if (StringUtils.isNotBlank(chain)) { param.addChain(chain); - } - - if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { - for (String nextWhitelist : new TreeSet(nextParameter.getQualifierWhitelist())) { - if (nextWhitelist.startsWith(".")) { - param.addChain(nextWhitelist.substring(1)); + } else { + if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { + for (String nextWhitelist : new TreeSet(nextParameter.getQualifierWhitelist())) { + if (nextWhitelist.startsWith(".")) { + param.addChain(nextWhitelist.substring(1)); + } } } } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java index 7b688291353..1ff78efc688 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java @@ -579,6 +579,45 @@ public class ServerConformanceProviderDstu2Test { assertEquals(2, param.getChain().size()); } + @Test + public void testSearchReferenceParameterWithExplicitChainsDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new SearchProviderWithExplicitChains()); + + ServerConformanceProvider sc = new ServerConformanceProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(0); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + Conformance conformance = sc.getServerConformance(createHttpServletRequest()); + + String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); + + RestResource resource = findRestResource(conformance, "Patient"); + + assertEquals(1, resource.getSearchParam().size()); + RestResourceSearchParam param = resource.getSearchParam().get(0); + assertEquals("organization", param.getName()); + assertEquals("bar", param.getChain().get(0).getValue()); + assertEquals("baz.bob", param.getChain().get(1).getValue()); + assertEquals("foo", param.getChain().get(2).getValue()); + assertEquals(3, param.getChain().size()); + } + @Test public void testSystemHistorySupported() throws Exception { @@ -851,6 +890,19 @@ public class ServerConformanceProviderDstu2Test { } + public static class SearchProviderWithExplicitChains { + + @Search(type = Patient.class) + public Patient findPatient1( + @Description(shortDefinition = "The organization at which this person is a patient") + @RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo, + @RequiredParam(name = "organization.bar") ReferenceAndListParam theBar, + @RequiredParam(name = "organization.baz.bob") ReferenceAndListParam theBazbob) { + return null; + } + + } + public static class SystemHistoryProvider { @History diff --git a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java index 345b41dab0f..e35c1c7fa53 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java @@ -1,56 +1,49 @@ package org.hl7.fhir.dstu3.hapi.rest.server; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.*; - -import javax.servlet.ServletConfig; -import javax.servlet.http.HttpServletRequest; - -import ca.uhn.fhir.model.primitive.InstantDt; -import org.hl7.fhir.dstu3.model.*; -import org.hl7.fhir.dstu3.model.CapabilityStatement.*; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.junit.AfterClass; -import org.junit.Test; - -import com.google.common.collect.Lists; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.*; -import ca.uhn.fhir.rest.server.method.*; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.ResourceBinding; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestulfulServerConfiguration; +import ca.uhn.fhir.rest.server.method.BaseMethodBinding; +import ca.uhn.fhir.rest.server.method.IParameter; +import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.method.SearchParameter; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import com.google.common.collect.Lists; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.CapabilityStatement.*; import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; import org.hl7.fhir.dstu3.model.OperationDefinition.OperationDefinitionParameterComponent; import org.hl7.fhir.dstu3.model.OperationDefinition.OperationKind; import org.hl7.fhir.dstu3.model.OperationDefinition.OperationParameterUse; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Test; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ServerCapabilityStatementProviderDstu3Test { - private static FhirContext ourCtx; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderDstu3Test.class); + private static FhirContext ourCtx; private static FhirValidator ourValidator; static { @@ -88,6 +81,46 @@ public class ServerCapabilityStatementProviderDstu3Test { return resource; } + @Test + public void testSearchReferenceParameterWithExplicitChainsDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new SearchProviderWithExplicitChains()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(0); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest()); + + String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); + + CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); + + assertEquals(1, resource.getSearchParam().size()); + CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); + assertEquals("organization", param.getName()); + +// assertEquals("bar", param.getChain().get(0).getValue()); +// assertEquals("baz.bob", param.getChain().get(1).getValue()); +// assertEquals("foo", param.getChain().get(2).getValue()); +// assertEquals(3, param.getChain().size()); + } + @Test public void testConditionalOperations() throws Exception { @@ -235,7 +268,9 @@ public class ServerCapabilityStatementProviderDstu3Test { assertNull(res.getConditionalUpdateElement().getValue()); } - /** See #379 */ + /** + * See #379 + */ @Test public void testOperationAcrossMultipleTypes() throws Exception { RestfulServer rs = new RestfulServer(ourCtx); @@ -302,7 +337,7 @@ public class ServerCapabilityStatementProviderDstu3Test { assertEquals("Patient", opDef.getParameter().get(0).getType()); } } - + @Test public void testOperationDocumentation() throws Exception { @@ -544,7 +579,7 @@ public class ServerCapabilityStatementProviderDstu3Test { @Test public void testSearchReferenceParameterWithList() throws Exception { - RestfulServer rsNoType = new RestfulServer(ourCtx){ + RestfulServer rsNoType = new RestfulServer(ourCtx) { @Override public RestulfulServerConfiguration createConfiguration() { RestulfulServerConfiguration retVal = super.createConfiguration(); @@ -561,7 +596,7 @@ public class ServerCapabilityStatementProviderDstu3Test { String confNoType = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(confNoType); - RestfulServer rsWithType = new RestfulServer(ourCtx){ + RestfulServer rsWithType = new RestfulServer(ourCtx) { @Override public RestulfulServerConfiguration createConfiguration() { RestulfulServerConfiguration retVal = super.createConfiguration(); @@ -720,8 +755,8 @@ public class ServerCapabilityStatementProviderDstu3Test { assertThat(param.getUse(), is(OperationParameterUse.IN)); CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() - .filter(r -> patientResourceName.equals(r.getType())) - .findAny().get(); + .filter(r -> patientResourceName.equals(r.getType())) + .findAny().get(); assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty())); } @@ -783,13 +818,21 @@ public class ServerCapabilityStatementProviderDstu3Test { ValidationResult result = ourValidator.validateWithResult(theOpDef); String outcome = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); ourLog.info("Outcome: {}", outcome); - + assertTrue(outcome, result.isSuccessful()); } - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); + public static class SearchProviderWithExplicitChains { + + @Search(type = Patient.class) + public Patient findPatient1( + @Description(shortDefinition = "The organization at which this person is a patient") + @RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo, + @RequiredParam(name = "organization.bar") ReferenceAndListParam theBar, + @RequiredParam(name = "organization.baz.bob") ReferenceAndListParam theBazbob) { + return null; + } + } @SuppressWarnings("unused") @@ -836,7 +879,7 @@ public class ServerCapabilityStatementProviderDstu3Test { @Search(type = Patient.class) public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, - @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { + @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { return null; } @@ -847,7 +890,7 @@ public class ServerCapabilityStatementProviderDstu3Test { @Operation(name = "someOp") public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { + @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { return null; } @@ -868,7 +911,7 @@ public class ServerCapabilityStatementProviderDstu3Test { @Operation(name = "someOp") public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { + @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { return null; } @@ -912,9 +955,9 @@ public class ServerCapabilityStatementProviderDstu3Test { @SuppressWarnings("unused") public static class PlainProviderWithExtendedOperationOnNoType { - @Operation(name = "plain", idempotent = true, returnParameters = { @OperationParam(min = 1, max = 2, name = "out1", type = StringType.class) }) + @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { + @OperationParam(name = "end") DateType theEnd) { return null; } @@ -925,7 +968,7 @@ public class ServerCapabilityStatementProviderDstu3Test { @Operation(name = "everything", idempotent = true) public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { + @OperationParam(name = "end") DateType theEnd) { return null; } @@ -942,8 +985,8 @@ public class ServerCapabilityStatementProviderDstu3Test { @Description(shortDefinition = "This is a search for stuff!") @Search public List findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, - @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, - @IncludeParam(allow = { "DiagnosticReport.result" }) Set theIncludes) throws Exception { + @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, + @IncludeParam(allow = {"DiagnosticReport.result"}) Set theIncludes) throws Exception { return null; } @@ -974,7 +1017,7 @@ public class ServerCapabilityStatementProviderDstu3Test { @Search(type = Patient.class) public Patient findPatient2( - @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = { Patient.class }) ReferenceAndListParam theLink) { + @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { return null; } @@ -984,15 +1027,15 @@ public class ServerCapabilityStatementProviderDstu3Test { public static class SearchProviderWithWhitelist { @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = { "foo", - "bar" }) ReferenceAndListParam theIdentifier) { + public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", + "bar"}) ReferenceAndListParam theIdentifier) { return null; } } @SuppressWarnings("unused") - public static class SearchProviderWithListNoType implements IResourceProvider { + public static class SearchProviderWithListNoType implements IResourceProvider { @Override public Class getResourceType() { @@ -1000,7 +1043,6 @@ public class ServerCapabilityStatementProviderDstu3Test { } - @Search() public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { return null; @@ -1009,7 +1051,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } @SuppressWarnings("unused") - public static class SearchProviderWithListWithType implements IResourceProvider { + public static class SearchProviderWithListWithType implements IResourceProvider { @Override public Class getResourceType() { @@ -1017,15 +1059,13 @@ public class ServerCapabilityStatementProviderDstu3Test { } - - @Search(type=Patient.class) + @Search(type = Patient.class) public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { return null; } } - public static class SystemHistoryProvider { @History @@ -1063,7 +1103,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } } - + public static class TypeLevelOperationProvider implements IResourceProvider { public static final String OPERATION_NAME = "op"; @@ -1110,4 +1150,9 @@ public class ServerCapabilityStatementProviderDstu3Test { } + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d68cb4989ec..a62b977bde1 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -270,6 +270,13 @@ performed by the user to limit them to specific resources or compartments that the user should have access to.
+ + In a DSTU2 server, if search parameters are expressed with chains directly in the + parameter name (e.g. + @RequiredParam(name="subject.name.family")]]>) the second + part of the chain was lost when the chain was described in the server + CapabilityStatement. This has been corrected. +
From aee7b2b882fd9ed363cfcfc95bcf46a77b96c852 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 10 Jan 2019 07:26:04 -0700 Subject: [PATCH 04/56] Make sure that sub-request transaction searches and reads preserve HTTP headers --- .../uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java | 2 +- .../fhir/jpa/dao/TransactionProcessor.java | 2 +- .../provider/ServletSubRequestDetails.java | 22 ++++++-- .../dbcache/DaoSubscriptionProvider.java | 5 +- .../dbmatcher/DaoSubscriptionMatcher.java | 5 +- ...tionInterceptorResourceProviderR4Test.java | 55 +++++++++++++++++++ .../server/servlet/ServletRequestDetails.java | 18 +++++- src/changes/changes.xml | 6 ++ 8 files changed, 98 insertions(+), 17 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 32bdbe3a77f..1d4e7456b9e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -233,7 +233,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { Integer originalOrder = originalRequestOrder.get(nextReqEntry); Entry nextRespEntry = response.getEntry().get(originalOrder); - ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); + ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(theRequestDetails); requestDetails.setServletRequest(theRequestDetails.getServletRequest()); requestDetails.setRequestType(RequestTypeEnum.GET); requestDetails.setServer(theRequestDetails.getServer()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index 898895ede5a..a8fd3445431 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -384,7 +384,7 @@ public class TransactionProcessor { Integer originalOrder = originalRequestOrder.get(nextReqEntry); BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(response).get(originalOrder); - ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); + ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(theRequestDetails); requestDetails.setServletRequest(theRequestDetails.getServletRequest()); requestDetails.setRequestType(RequestTypeEnum.GET); requestDetails.setServer(theRequestDetails.getServer()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java index 3ba5e226b4d..1fba4b84008 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java @@ -29,11 +29,25 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; public class ServletSubRequestDetails extends ServletRequestDetails { - private Map> myHeaders = new HashMap<>(); + private Map> myHeaders = new HashMap<>(); + + /** + * Constructor + * + * @param theRequestDetails The parent request details + */ + public ServletSubRequestDetails(ServletRequestDetails theRequestDetails) { + if (theRequestDetails != null) { + Map> headers = theRequestDetails.getHeaders(); + for (Map.Entry> next : headers.entrySet()) { + myHeaders.put(next.getKey().toLowerCase(), next.getValue()); + } + } + } public void addHeader(String theName, String theValue) { String lowerCase = theName.toLowerCase(); - ArrayList list = myHeaders.get(lowerCase); + List list = myHeaders.get(lowerCase); if (list == null) { list = new ArrayList<>(); myHeaders.put(lowerCase, list); @@ -43,7 +57,7 @@ public class ServletSubRequestDetails extends ServletRequestDetails { @Override public String getHeader(String theName) { - ArrayList list = myHeaders.get(theName.toLowerCase()); + List list = myHeaders.get(theName.toLowerCase()); if (list == null || list.isEmpty()) { return null; } @@ -52,7 +66,7 @@ public class ServletSubRequestDetails extends ServletRequestDetails { @Override public List getHeaders(String theName) { - ArrayList list = myHeaders.get(theName.toLowerCase()); + List list = myHeaders.get(theName.toLowerCase()); if (list == null || list.isEmpty()) { return null; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java index 97a5e46dc5a..18725dbe386 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbcache/DaoSubscriptionProvider.java @@ -42,10 +42,7 @@ public class DaoSubscriptionProvider implements ISubscriptionProvider { @Override public IBundleProvider search(SearchParameterMap theMap) { IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); - - return subscriptionDao.search(theMap, req); + return subscriptionDao.search(theMap); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java index eca40b9b2b7..24c41e9c560 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java @@ -72,12 +72,9 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher { RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria); SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef); - RequestDetails req = new ServletSubRequestDetails(); - req.setSubRequest(true); - IFhirResourceDao responseDao = myDaoRegistry.getResourceDao(responseResourceDef.getImplementingClass()); responseCriteriaUrl.setLoadSynchronousUpTo(1); - return responseDao.search(responseCriteriaUrl, req); + return responseDao.search(responseCriteriaUrl); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java index 00125a6cbc3..347aab83d0e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/AuthorizationInterceptorResourceProviderR4Test.java @@ -4,6 +4,8 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; @@ -28,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.*; @@ -148,6 +151,58 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource } + @Test + public void testReadInTransaction() { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); + patient.addName().setFamily("Tester").addGiven("Raghad"); + IIdType id = ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute().getId().toUnqualifiedVersionless(); + + ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + String authHeader = theRequestDetails.getHeader("Authorization"); + if (!"Bearer AAA".equals(authHeader)) { + throw new AuthenticationException("Invalid auth header: " + authHeader); + } + return new RuleBuilder() + .allow().transaction().withAnyOperation().andApplyNormalRules().andThen() + .allow().read().resourcesOfType(Patient.class).withAnyId() + .build(); + } + }); + + SimpleRequestHeaderInterceptor interceptor = new SimpleRequestHeaderInterceptor("Authorization", "Bearer AAA"); + try { + ourClient.registerInterceptor(interceptor); + + Bundle bundle; + Bundle responseBundle; + + // Read + bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl(id.getValue()); + responseBundle = ourClient.transaction().withBundle(bundle).execute(); + patient = (Patient) responseBundle.getEntry().get(0).getResource(); + assertEquals("Tester", patient.getNameFirstRep().getFamily()); + + // Search + bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Patient?"); + responseBundle = ourClient.transaction().withBundle(bundle).execute(); + responseBundle = (Bundle) responseBundle.getEntry().get(0).getResource(); + patient = (Patient) responseBundle.getEntry().get(0).getResource(); + assertEquals("Tester", patient.getNameFirstRep().getFamily()); + + } finally { + ourClient.unregisterInterceptor(interceptor); + } + + } + /** * See #751 */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java index 33b9b096ed0..f6d4448a05c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java @@ -34,9 +34,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.charset.Charset; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; +import java.util.*; import java.util.zip.GZIPInputStream; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -145,4 +143,18 @@ public class ServletRequestDetails extends RequestDetails { this.myServletResponse = myServletResponse; } + public Map> getHeaders() { + Map> retVal = new HashMap<>(); + Enumeration names = myServletRequest.getHeaderNames(); + while (names.hasMoreElements()) { + String nextName = names.nextElement(); + ArrayList headerValues = new ArrayList<>(); + retVal.put(nextName, headerValues); + Enumeration valuesEnum = myServletRequest.getHeaders(nextName); + while (valuesEnum.hasMoreElements()) { + headerValues.add(valuesEnum.nextElement()); + } + } + return Collections.unmodifiableMap(retVal); + } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index a62b977bde1..d425b41e8fc 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -277,6 +277,12 @@ part of the chain was lost when the chain was described in the server CapabilityStatement. This has been corrected. + + In the JPA server, search/read operations being performed within a transaction bundle + did not pass the client request HTTP headers to the sub-request. This meant that + AuthorizationInterceptor could not authorize these requests if it was depending on + headers being present. + From 3c260e5ef213a8ba8a8840592a321fd58485b026 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 10 Jan 2019 07:36:37 -0700 Subject: [PATCH 05/56] Ignore unfinished test --- .../server/ServerCapabilityStatementProviderDstu3Test.java | 2 ++ pom.xml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java index e35c1c7fa53..7eebf45d480 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java @@ -29,6 +29,7 @@ import org.hl7.fhir.dstu3.model.OperationDefinition.OperationKind; import org.hl7.fhir.dstu3.model.OperationDefinition.OperationParameterUse; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.AfterClass; +import org.junit.Ignore; import org.junit.Test; import javax.servlet.ServletConfig; @@ -82,6 +83,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } @Test + @Ignore public void testSearchReferenceParameterWithExplicitChainsDocumentation() throws Exception { RestfulServer rs = new RestfulServer(ourCtx); diff --git a/pom.xml b/pom.xml index 3754403f1f0..077176f8252 100644 --- a/pom.xml +++ b/pom.xml @@ -767,7 +767,8 @@ com.microsoft.sqlserver mssql-jdbc - 6.2.2.jre8 + + 7.0.0.jre8 - mvn:javax.jws/javax.jws-api/1.1 + mvn:javax.jws/javax.jws-api/2.1.1 mvn:com.helger/ph-xml/${ph_commons_version} mvn:com.sun.xml.bind/jaxb-impl/${jaxb_core_version} mvn:com.sun.xml.bind/jaxb-core/${jaxb_core_version} From dec53c1eea56996bc73709fea4e5b4415c7d8dfd Mon Sep 17 00:00:00 2001 From: jpoth Date: Fri, 11 Jan 2019 15:33:12 +0100 Subject: [PATCH 07/56] Revert "update jws-api on Karaf" This reverts commit d9f96ea --- osgi/hapi-fhir-karaf-features/src/main/resources/features.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osgi/hapi-fhir-karaf-features/src/main/resources/features.xml b/osgi/hapi-fhir-karaf-features/src/main/resources/features.xml index eabac5481c3..0f13e59ba10 100644 --- a/osgi/hapi-fhir-karaf-features/src/main/resources/features.xml +++ b/osgi/hapi-fhir-karaf-features/src/main/resources/features.xml @@ -87,7 +87,7 @@ mvn:com.helger/ph-commons/${ph_commons_version} mvn:com.helger/ph-collection/${ph_commons_version} - mvn:javax.jws/javax.jws-api/2.1.1 + mvn:javax.jws/javax.jws-api/1.1 mvn:com.helger/ph-xml/${ph_commons_version} mvn:com.sun.xml.bind/jaxb-impl/${jaxb_core_version} mvn:com.sun.xml.bind/jaxb-core/${jaxb_core_version} From 860a8fb7a6981c5f68408a389b5639fc5110a7b1 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 11 Jan 2019 09:49:40 -0500 Subject: [PATCH 08/56] Emergency fix. Normally I'd write a test for this, but this startup behaviour is changing in my next PR. --- .../SubscriptionInterceptorLoader.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java index 43677179b73..d469a139a8c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java @@ -37,6 +37,7 @@ import java.util.Set; public class SubscriptionInterceptorLoader { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionInterceptorLoader.class); + // TODO KHS remove side-effects of autowiring these beans private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; private SubscriptionActivatingInterceptor mySubscriptionActivatingInterceptor; @@ -52,11 +53,16 @@ public class SubscriptionInterceptorLoader { if (!supportedSubscriptionTypes.isEmpty()) { loadSubscriptions(); - + if (mySubscriptionActivatingInterceptor == null) { + mySubscriptionActivatingInterceptor = myAppicationContext.getBean(SubscriptionActivatingInterceptor.class); + } ourLog.info("Registering subscription activating interceptor"); myDaoConfig.registerInterceptor(mySubscriptionActivatingInterceptor); } if (myDaoConfig.isSubscriptionMatchingEnabled()) { + if (mySubscriptionMatcherInterceptor == null) { + mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class); + } ourLog.info("Registering subscription matcher interceptor"); myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); } @@ -67,14 +73,6 @@ public class SubscriptionInterceptorLoader { // Load subscriptions into the SubscriptionRegistry myAppicationContext.getBean(SubscriptionLoader.class); ourLog.info("...{} subscriptions loaded", mySubscriptionRegistry.size()); - - // Once subscriptions have been loaded, now - if (mySubscriptionActivatingInterceptor == null) { - mySubscriptionActivatingInterceptor = myAppicationContext.getBean(SubscriptionActivatingInterceptor.class); - } - if (mySubscriptionMatcherInterceptor == null) { - mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class); - } } @VisibleForTesting From fc09ed6966fb4eb79aa396be2a00945987170962 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 11 Jan 2019 08:01:20 -0700 Subject: [PATCH 09/56] Fix #1163 - Correctly handle invalid CapabilityStatement --- .../client/impl/RestfulClientFactory.java | 2 +- .../ca/uhn/fhir/rest/client/ClientR4Test.java | 2290 +++++++++-------- src/changes/changes.xml | 6 + 3 files changed, 1164 insertions(+), 1134 deletions(-) diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java index c6100a4fa31..d6afdfc59e8 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/RestfulClientFactory.java @@ -301,7 +301,7 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory { conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute(); } catch (FhirClientConnectionException e) { if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3) && e.getCause() instanceof DataFormatException) { - capabilityStatementResourceName = "Conformance"; + capabilityStatementResourceName = "CapabilityStatement"; implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass(); conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute(); } else { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java index 8ab4dca6ac0..f0c4e42e79e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientR4Test.java @@ -10,7 +10,9 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.client.apache.ApacheHttpRequest; import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -57,25 +59,25 @@ import static org.mockito.Mockito.when; public class ClientR4Test { - private static FhirContext ourCtx = FhirContext.forR4(); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ClientR4Test.class); - private HttpClient myHttpClient; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ClientR4Test.class); + private static FhirContext ourCtx = FhirContext.forR4(); + private HttpClient myHttpClient; - private HttpResponse myHttpResponse; + private HttpResponse myHttpResponse; - // atom-document-large.xml + // atom-document-large.xml - @Before - public void before() { + @Before + public void before() { - myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); - ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); - ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); - } + myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + } - public String getHistoryBundleWithTwoResults() { + public String getHistoryBundleWithTwoResults() { /* * //@formatter:off String msg = "<id>6c1d93be-027f-468d-9d47-f826cd15cf42</id>" @@ -95,1219 +97,1241 @@ public class ClientR4Test { //@formatter:on */ - Bundle retVal = new Bundle(); - - Patient p1 = new Patient(); - p1.addName().setFamily("OldeerFamily").addGiven("PatientOne"); - retVal - .addEntry() - .setFullUrl("http://acme.com/Patient/111") - .setResource(p1); - - Patient p2 = new Patient(); - p2.addName().setFamily("NewerFamily").addGiven("PatientOne"); - retVal - .addEntry() - .setFullUrl("http://acme.com/Patient/222") - .setResource(p2); - - return ourCtx.newXmlParser().encodeResourceToString(retVal); - - } - - private String getPatient() { - Patient p = new Patient(); - p.getMeta().getLastUpdatedElement().setValueAsString("1995-11-15T03:58:08.000-01:00"); - p - .getMeta() - .addTag() - .setSystem("http://hl7.org/fhir/tag") - .setCode("http://foo/tagdefinition.html") - .setDisplay("Some tag"); - - p.setId("http://foo.com/Patient/123/_history/2333"); - p.addName().setFamily("Kramer").addGiven("Doe"); - p.addIdentifier().setValue("PRP1660"); - String msg = EncodingEnum.XML.newParser(ourCtx).setPrettyPrint(true).encodeResourceToString(p); - return msg; - } - - @Test - public void testCreate() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - CapturingInterceptor interceptor = new CapturingInterceptor(); - client.registerInterceptor(interceptor); - - MethodOutcome response = client.createPatient(patient); - - assertEquals(((ApacheHttpRequest) interceptor.getLastRequest()).getApacheRequest().getURI().toASCIIString(), "http://foo/Patient"); - - assertEquals(HttpPost.class, capt.getValue().getClass()); - HttpPost post = (HttpPost) capt.getValue(); - assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); - assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - assertEquals("200", response.getId().getVersionIdPart()); - } - - @Test - public void testCreateBad() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 400, "foobar")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("foobar"), Charset.forName("UTF-8"))); - - try { - ourCtx.newRestfulClient(ITestClient.class, "http://foo").createPatient(patient); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), StringContains.containsString("foobar")); - } - } - - /** - * Some servers (older ones?) return the resourcde you created instead of an OperationOutcome. We just need to ignore - * it. - */ - @Test - public void testCreateWithResourceResponse() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ourCtx.newXmlParser().encodeResourceToString(patient)), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome response = client.createPatient(patient); - - assertEquals(HttpPost.class, capt.getValue().getClass()); - HttpPost post = (HttpPost) capt.getValue(); - assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); - assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); - assertEquals("200", response.getId().getVersionIdPart()); - } - - @Test - public void testDelete() throws Exception { + Bundle retVal = new Bundle(); + + Patient p1 = new Patient(); + p1.addName().setFamily("OldeerFamily").addGiven("PatientOne"); + retVal + .addEntry() + .setFullUrl("http://acme.com/Patient/111") + .setResource(p1); + + Patient p2 = new Patient(); + p2.addName().setFamily("NewerFamily").addGiven("PatientOne"); + retVal + .addEntry() + .setFullUrl("http://acme.com/Patient/222") + .setResource(p2); + + return ourCtx.newXmlParser().encodeResourceToString(retVal); + + } + + private String getPatient() { + Patient p = new Patient(); + p.getMeta().getLastUpdatedElement().setValueAsString("1995-11-15T03:58:08.000-01:00"); + p + .getMeta() + .addTag() + .setSystem("http://hl7.org/fhir/tag") + .setCode("http://foo/tagdefinition.html") + .setDisplay("Some tag"); + + p.setId("http://foo.com/Patient/123/_history/2333"); + p.addName().setFamily("Kramer").addGiven("Doe"); + p.addIdentifier().setValue("PRP1660"); + String msg = EncodingEnum.XML.newParser(ourCtx).setPrettyPrint(true).encodeResourceToString(p); + return msg; + } + + @Test + public void testCreate() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + CapturingInterceptor interceptor = new CapturingInterceptor(); + client.registerInterceptor(interceptor); + + MethodOutcome response = client.createPatient(patient); + + assertEquals(((ApacheHttpRequest) interceptor.getLastRequest()).getApacheRequest().getURI().toASCIIString(), "http://foo/Patient"); + + assertEquals(HttpPost.class, capt.getValue().getClass()); + HttpPost post = (HttpPost) capt.getValue(); + assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); + assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + assertEquals("200", response.getId().getVersionIdPart()); + } + + @Test + public void testCreateBad() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 400, "foobar")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("foobar"), Charset.forName("UTF-8"))); + + try { + ourCtx.newRestfulClient(ITestClient.class, "http://foo").createPatient(patient); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), StringContains.containsString("foobar")); + } + } + + /** + * Some servers (older ones?) return the resourcde you created instead of an OperationOutcome. We just need to ignore + * it. + */ + @Test + public void testCreateWithResourceResponse() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ourCtx.newXmlParser().encodeResourceToString(patient)), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome response = client.createPatient(patient); + + assertEquals(HttpPost.class, capt.getValue().getClass()); + HttpPost post = (HttpPost) capt.getValue(); + assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); + assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); + assertEquals("200", response.getId().getVersionIdPart()); + } + + @Test + public void testDelete() throws Exception { - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setDiagnostics("Hello"); - String resp = ourCtx.newXmlParser().encodeResourceToString(oo); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDiagnostics("Hello"); + String resp = ourCtx.newXmlParser().encodeResourceToString(oo); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome response = client.deletePatient(new IdType("1234")); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome response = client.deletePatient(new IdType("1234")); - assertEquals(HttpDelete.class, capt.getValue().getClass()); - assertEquals("http://foo/Patient/1234", capt.getValue().getURI().toString()); - assertEquals("Hello", ((OperationOutcome) response.getOperationOutcome()).getIssueFirstRep().getDiagnosticsElement().getValue()); - } + assertEquals(HttpDelete.class, capt.getValue().getClass()); + assertEquals("http://foo/Patient/1234", capt.getValue().getURI().toString()); + assertEquals("Hello", ((OperationOutcome) response.getOperationOutcome()).getIssueFirstRep().getDiagnosticsElement().getValue()); + } - @Test - public void testDeleteNoResponse() throws Exception { + @Test + public void testDeleteNoResponse() throws Exception { - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 204, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 204, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.deleteDiagnosticReport(new IdType("1234")); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.deleteDiagnosticReport(new IdType("1234")); - assertEquals(HttpDelete.class, capt.getValue().getClass()); - assertEquals("http://foo/DiagnosticReport/1234", capt.getValue().getURI().toString()); - } + assertEquals(HttpDelete.class, capt.getValue().getClass()); + assertEquals("http://foo/DiagnosticReport/1234", capt.getValue().getURI().toString()); + } - @Test - public void testGetConformance() throws Exception { + @Test + public void testGetConformance() throws Exception { - CapabilityStatement cs = new CapabilityStatement(); - cs.getPublisherElement().setValue("Health Intersections"); - String msg = ourCtx.newXmlParser().encodeResourceToString(cs); + CapabilityStatement cs = new CapabilityStatement(); + cs.getPublisherElement().setValue("Health Intersections"); + String msg = ourCtx.newXmlParser().encodeResourceToString(cs); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - CapabilityStatement response = (CapabilityStatement) client.getServerConformanceStatement(); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + CapabilityStatement response = (CapabilityStatement) client.getServerConformanceStatement(); - assertEquals("http://foo/metadata", capt.getValue().getURI().toString()); - assertEquals("Health Intersections", response.getPublisherElement().getValue()); + assertEquals("http://foo/metadata", capt.getValue().getURI().toString()); + assertEquals("Health Intersections", response.getPublisherElement().getValue()); - } + } - @Test - public void testHistoryResourceInstance() throws Exception { + @Test + public void testHistoryResourceInstance() throws Exception { - String msg = getHistoryBundleWithTwoResults(); + String msg = getHistoryBundleWithTwoResults(); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Bundle response = client.getHistoryPatientInstance(new IdType("111")); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Bundle response = client.getHistoryPatientInstance(new IdType("111")); - assertEquals("http://foo/Patient/111/_history", capt.getValue().getURI().toString()); + assertEquals("http://foo/Patient/111/_history", capt.getValue().getURI().toString()); - assertEquals(2, response.getEntry().size()); + assertEquals(2, response.getEntry().size()); - verifyHistoryBundleWithTwoResults(response); - } + verifyHistoryBundleWithTwoResults(response); + } - @Test - public void testHistoryResourceType() throws Exception { + @Test + public void testHistoryResourceType() throws Exception { - String msg = getHistoryBundleWithTwoResults(); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + String msg = getHistoryBundleWithTwoResults(); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Bundle response = client.getHistoryPatientType(); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Bundle response = client.getHistoryPatientType(); - assertEquals("http://foo/Patient/_history", capt.getValue().getURI().toString()); + assertEquals("http://foo/Patient/_history", capt.getValue().getURI().toString()); - verifyHistoryBundleWithTwoResults(response); - } + verifyHistoryBundleWithTwoResults(response); + } - @Test - public void testHistoryServer() throws Exception { - String msg = getHistoryBundleWithTwoResults(); + @Test + public void testHistoryServer() throws Exception { + String msg = getHistoryBundleWithTwoResults(); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Bundle response = client.getHistoryServer(); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Bundle response = client.getHistoryServer(); - assertEquals("http://foo/_history", capt.getValue().getURI().toString()); + assertEquals("http://foo/_history", capt.getValue().getURI().toString()); - assertEquals(2, response.getEntry().size()); + assertEquals(2, response.getEntry().size()); - verifyHistoryBundleWithTwoResults(response); - } + verifyHistoryBundleWithTwoResults(response); + } - @Test - public void testHistoryWithParams() throws Exception { + @Test + public void testHistoryWithParams() throws Exception { - final String msg = getHistoryBundleWithTwoResults(); + final String msg = getHistoryBundleWithTwoResults(); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); - } - }); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - // ensures the local timezone - String expectedDateString = new InstantType(new InstantType("2012-01-02T12:01:02").getValue()).getValueAsString(); - expectedDateString = expectedDateString.replace(":", "%3A").replace("+", "%2B"); + // ensures the local timezone + String expectedDateString = new InstantType(new InstantType("2012-01-02T12:01:02").getValue()).getValueAsString(); + expectedDateString = expectedDateString.replace(":", "%3A").replace("+", "%2B"); - client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T12:01:02"), new IntegerType(12)); - assertThat(capt.getAllValues().get(0).getURI().toString(), containsString("http://foo/Patient/111/_history?")); - assertThat(capt.getAllValues().get(0).getURI().toString(), containsString("_since=" + expectedDateString.replaceAll("\\..*", ""))); - assertThat(capt.getAllValues().get(0).getURI().toString(), containsString("_count=12")); + client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T12:01:02"), new IntegerType(12)); + assertThat(capt.getAllValues().get(0).getURI().toString(), containsString("http://foo/Patient/111/_history?")); + assertThat(capt.getAllValues().get(0).getURI().toString(), containsString("_since=" + expectedDateString.replaceAll("\\..*", ""))); + assertThat(capt.getAllValues().get(0).getURI().toString(), containsString("_count=12")); - client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T12:01:02").getValue(), new IntegerType(12).getValue()); - assertThat(capt.getAllValues().get(1).getURI().toString(), containsString("http://foo/Patient/111/_history?")); - assertThat(capt.getAllValues().get(1).getURI().toString(), containsString("_since=" + expectedDateString)); - assertThat(capt.getAllValues().get(1).getURI().toString(), containsString("_count=12")); + client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T12:01:02").getValue(), new IntegerType(12).getValue()); + assertThat(capt.getAllValues().get(1).getURI().toString(), containsString("http://foo/Patient/111/_history?")); + assertThat(capt.getAllValues().get(1).getURI().toString(), containsString("_since=" + expectedDateString)); + assertThat(capt.getAllValues().get(1).getURI().toString(), containsString("_count=12")); - client.getHistoryPatientInstance(new IdType("111"), null, new IntegerType(12)); - assertEquals("http://foo/Patient/111/_history?_count=12", capt.getAllValues().get(2).getURI().toString()); + client.getHistoryPatientInstance(new IdType("111"), null, new IntegerType(12)); + assertEquals("http://foo/Patient/111/_history?_count=12", capt.getAllValues().get(2).getURI().toString()); - client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T00:01:02"), null); - assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02", capt.getAllValues().get(3).getURI().toString()); + client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T00:01:02"), null); + assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02", capt.getAllValues().get(3).getURI().toString()); - client.getHistoryPatientInstance(new IdType("111"), new InstantType(), new IntegerType(12)); - assertEquals("http://foo/Patient/111/_history?_count=12", capt.getAllValues().get(4).getURI().toString()); + client.getHistoryPatientInstance(new IdType("111"), new InstantType(), new IntegerType(12)); + assertEquals("http://foo/Patient/111/_history?_count=12", capt.getAllValues().get(4).getURI().toString()); - client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T00:01:02"), new IntegerType()); - assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02", capt.getAllValues().get(5).getURI().toString()); + client.getHistoryPatientInstance(new IdType("111"), new InstantType("2012-01-02T00:01:02"), new IntegerType()); + assertEquals("http://foo/Patient/111/_history?_since=2012-01-02T00%3A01%3A02", capt.getAllValues().get(5).getURI().toString()); - } + } - @Test - public void testNonAnnotatedMethodFailsGracefully() { + @Test + public void testNonAnnotatedMethodFailsGracefully() { - // TODO: remove the read annotation and make sure we get a sensible - // error message to tell the user why the method isn't working - FhirContext ctx = ourCtx; - ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + // TODO: remove the read annotation and make sure we get a sensible + // error message to tell the user why the method isn't working + FhirContext ctx = ourCtx; + ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - ClientWithoutAnnotation client = ctx.newRestfulClient(ClientWithoutAnnotation.class, "http://wildfhir.aegis.net/fhir"); + ClientWithoutAnnotation client = ctx.newRestfulClient(ClientWithoutAnnotation.class, "http://wildfhir.aegis.net/fhir"); - try { - client.read(new IdType("8")); - fail(); - } catch (UnsupportedOperationException e) { - assertThat(e.getMessage(), containsString("annotation")); - } + try { + client.read(new IdType("8")); + fail(); + } catch (UnsupportedOperationException e) { + assertThat(e.getMessage(), containsString("annotation")); + } - } + } - @Test - public void testRead() throws Exception { + @Test + public void testRead() throws Exception { - String msg = getPatient(); + String msg = getPatient(); - ourLog.info(msg); + ourLog.info(msg); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - Header[] headers = new Header[] { - new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), - new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333") - }; + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + Header[] headers = new Header[]{ + new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"), + new BasicHeader(Constants.HEADER_CONTENT_LOCATION, "http://foo.com/Patient/123/_history/2333") + }; - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - // Patient response = client.findPatientByMrn(new - // IdentifierDt("urn:foo", "123")); - Patient response = client.getPatientById(new IdType("111")); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + // Patient response = client.findPatientByMrn(new + // IdentifierDt("urn:foo", "123")); + Patient response = client.getPatientById(new IdType("111")); - assertEquals("http://foo/Patient/111", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); + assertEquals("http://foo/Patient/111", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - assertEquals("http://foo.com/Patient/123/_history/2333", response.getId()); + assertEquals("http://foo.com/Patient/123/_history/2333", response.getId()); - InstantType lm = (InstantType) response.getMeta().getLastUpdatedElement(); - lm.setTimeZoneZulu(true); - assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); - - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(response)); - - List<Coding> tags = response.getMeta().getTag(); - assertNotNull(tags); - assertEquals(1, tags.size()); - assertEquals("http://foo/tagdefinition.html", tags.get(0).getCode()); - assertEquals("http://hl7.org/fhir/tag", tags.get(0).getSystem()); - assertEquals("Some tag", tags.get(0).getDisplay()); - - } - - @Test - public void testReadFailureInternalError() throws Exception { + InstantType lm = (InstantType) response.getMeta().getLastUpdatedElement(); + lm.setTimeZoneZulu(true); + assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "INTERNAL")); - Header[] headers = new Header[1]; - headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "2011-01-02T22:01:02"); - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT)); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Internal Failure"), Charset.forName("UTF-8"))); + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(response)); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - try { - client.getPatientById(new IdType("111")); - fail(); - } catch (InternalErrorException e) { - assertThat(e.getMessage(), containsString("INTERNAL")); - assertThat(e.getResponseBody(), containsString("Internal Failure")); - } + List<Coding> tags = response.getMeta().getTag(); + assertNotNull(tags); + assertEquals(1, tags.size()); + assertEquals("http://foo/tagdefinition.html", tags.get(0).getCode()); + assertEquals("http://hl7.org/fhir/tag", tags.get(0).getSystem()); + assertEquals("Some tag", tags.get(0).getDisplay()); - } + } - @Test - public void testReadFailureNoCharset() throws Exception { + @Test + public void testReadFailureInternalError() throws Exception { - //@formatter:off - String msg = "<OperationOutcome xmlns=\"http://hl7.org/fhir\"></OperationOutcome>"; - //@formatter:on + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "INTERNAL")); + Header[] headers = new Header[1]; + headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "2011-01-02T22:01:02"); + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT)); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Internal Failure"), Charset.forName("UTF-8"))); - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 404, "NOT FOUND")); - Header[] headers = new Header[1]; - headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "2011-01-02T22:01:02"); - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML)); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + try { + client.getPatientById(new IdType("111")); + fail(); + } catch (InternalErrorException e) { + assertThat(e.getMessage(), containsString("INTERNAL")); + assertThat(e.getResponseBody(), containsString("Internal Failure")); + } - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - try { - client.getPatientById(new IdType("111")); - fail(); - } catch (ResourceNotFoundException e) { - // good - } + } - } + @Test + public void testReadFailureNoCharset() throws Exception { - @Test - public void testReadNoCharset() throws Exception { - - String msg = getPatient(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - Header[] headers = new Header[1]; - headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"); - when(myHttpResponse.getAllHeaders()).thenReturn(headers); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML)); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - // Patient response = client.findPatientByMrn(new - // IdentifierDt("urn:foo", "123")); - Patient response = client.getPatientById(new IdType("111")); - - assertEquals("http://foo/Patient/111", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - - InstantType lm = (InstantType) response.getMeta().getLastUpdatedElement(); - lm.setTimeZoneZulu(true); - assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); - - } - - @Test - public void testResponseContainingOldStyleXmlContentType() throws Exception { - - String msg = getPatient(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "application/fhir+xml; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - // Patient response = client.findPatientByMrn(new - // IdentifierDt("urn:foo", "123")); - Patient response = client.getPatientById(new IdType("111")); - - assertEquals("http://foo/Patient/111", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchByCompartment() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - List<Patient> response = client.getPatientByCompartmentAndDob(new IdType("123"), new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - - assertEquals("http://foo/Patient/123/compartmentName?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValueElement().getValue()); - - try { - client.getPatientByCompartmentAndDob(new IdType(""), new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.toString(), containsString("null or empty for compartment")); - } - - } - - @Test - public void testSearchByCompositeParam() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - StringParam str = new StringParam("FOO$BAR"); - DateParam date = new DateParam("2001-01-01"); - client.getObservationByNameValueDate(new CompositeParam<StringParam, DateParam>(str, date)); - - assertEquals("http://foo/Observation?" + Observation.SP_CODE_VALUE_DATE + "=" + UrlUtil.escapeUrlParam("FOO\\$BAR$2001-01-01"), capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchByDateRange() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - DateRangeParam param = new DateRangeParam(); - param.setLowerBound(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-01")); - param.setUpperBound(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, "2021-01-01")); - client.getPatientByDateRange(param); - - assertEquals("http://foo/Patient?dateRange=ge2011-01-01&dateRange=le2021-01-01", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchByDob() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - // httpResponse = new BasicHttpResponse(statusline, catalog, locale) - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - List<Patient> response = client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - - assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchByQuantity() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Patient response = client.findPatientQuantity(new QuantityParam(ParamPrefixEnum.GREATERTHAN, 123L, "foo", "bar")); - - assertEquals("http://foo/Patient?quantityParam=gt123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchByToken() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Patient response = client.findPatientByMrn(new TokenParam("urn:foo", "123")); - - assertEquals("http://foo/Patient?identifier=urn%3Afoo%7C123", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchNamedQueryNoParams() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.getPatientNoParams(); - - assertEquals("http://foo/Patient?_query=someQueryNoParams", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchNamedQueryOneParam() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.getPatientOneParam(new StringParam("BB")); - - assertEquals("http://foo/Patient?_query=someQueryOneParam¶m1=BB", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchOrList() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - TokenOrListParam identifiers = new TokenOrListParam(); - identifiers.add("foo", "bar"); - identifiers.add("baz", "boz"); - client.getPatientMultipleIdentifiers(identifiers); - - assertEquals("http://foo/Patient?ids=foo%7Cbar%2Cbaz%7Cboz", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchWithCustomType() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClientWithCustomType client = ourCtx.newRestfulClient(ITestClientWithCustomType.class, "http://foo"); - CustomPatient response = client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - - assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchWithCustomTypeList() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClientWithCustomTypeList client = ourCtx.newRestfulClient(ITestClientWithCustomTypeList.class, "http://foo"); - List<CustomPatient> response = client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - - assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchWithElements() throws Exception { - - final String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); - } - }); - - // httpResponse = new BasicHttpResponse(statusline, catalog, locale) - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - ITestClientWithElements client = ourCtx.newRestfulClient(ITestClientWithElements.class, "http://foo"); - - int idx = 0; - - client.getPatientWithIncludes((String) null); - assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes((Set<String>) null); - assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes("test"); - assertEquals("http://foo/Patient?_elements=test", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes("test,foo"); - assertEquals("http://foo/Patient?_elements=test%2Cfoo", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes(new HashSet<String>(Arrays.asList("test", "foo", ""))); - assertEquals("http://foo/Patient?_elements=test%2Cfoo", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - } - - @Test - public void testSearchWithEscapedValues() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - StringAndListParam andListParam = new StringAndListParam(); - StringOrListParam orListParam1 = new StringOrListParam().addOr(new StringParam("NE,NE", false)).addOr(new StringParam("NE,NE", false)); - StringOrListParam orListParam2 = new StringOrListParam().addOr(new StringParam("E$E", true)); - StringOrListParam orListParam3 = new StringOrListParam().addOr(new StringParam("NE\\NE", false)); - StringOrListParam orListParam4 = new StringOrListParam().addOr(new StringParam("E|E", true)); - client.findPatient(andListParam.addAnd(orListParam1).addAnd(orListParam2).addAnd(orListParam3).addAnd(orListParam4)); - - assertThat(capt.getValue().getURI().toString(), containsString("%3A")); - assertEquals("http://foo/Patient?param=NE\\,NE,NE\\,NE¶m=NE\\\\NE¶m:exact=E\\$E¶m:exact=E\\|E", UrlUtil.unescape(capt.getValue().getURI().toString())); - - } - - @Test - public void testSearchWithFormatAndPrettyPrint() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - // TODO: document this - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getAllValues().get(0).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - client.setEncoding(EncodingEnum.JSON); // this needs to be actually - // implemented - client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - assertEquals("http://foo/Patient?birthdate=ge2011-01-02&_format=json", capt.getAllValues().get(1).getURI().toString()); - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - client.setPrettyPrint(true); - client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - assertEquals("http://foo/Patient?birthdate=ge2011-01-02&_format=json&_pretty=true", capt.getAllValues().get(2).getURI().toString()); - - } - - @Test - public void testSearchWithGenericReturnType() throws Exception { - - Bundle bundle = new Bundle(); - - Patient patient = new Patient(); - patient.addIdentifier().setValue("PRP1660"); - bundle.addEntry().setResource(patient); - - Organization org = new Organization(); - org.setName("FOO"); - patient.getManagingOrganization().setResource(org); - - String msg = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(bundle); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - // httpResponse = new BasicHttpResponse(statusline, catalog, locale) - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - List<IBaseResource> response = client.getPatientByDobWithGenericResourceReturnType(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - - assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); - ExtendedPatient patientResp = (ExtendedPatient) response.get(0); - assertEquals("PRP1660", patientResp.getIdentifier().get(0).getValueElement().getValue()); - - } - - @Test - public void testSearchWithGlobalSummary() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.setSummary(SummaryEnum.DATA); - client.findPatientByMrn(new TokenParam("sysm", "val")); - - assertEquals("http://foo/Patient?identifier=sysm%7Cval&_summary=data", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchWithIncludes() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.getPatientWithIncludes(new StringParam("aaa"), Arrays.asList(new Include[] { new Include("inc1"), new Include("inc2", true), new Include("inc3", true) })); - - assertEquals("http://foo/Patient?withIncludes=aaa&_include=inc1&_include%3Arecurse=inc2&_include%3Arecurse=inc3", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchWithOptionalParam() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - Bundle response = client.findPatientByName(new StringParam("AAA"), null); - - assertEquals("http://foo/Patient?family=AAA", capt.getValue().getURI().toString()); - Patient resource = (Patient) response.getEntry().get(0).getResource(); - assertEquals("PRP1660", resource.getIdentifier().get(0).getValueElement().getValue()); - - /* - * Now with a first name - */ - - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - response = client.findPatientByName(new StringParam("AAA"), new StringParam("BBB")); - - assertEquals("http://foo/Patient?family=AAA&given=BBB", capt.getValue().getURI().toString()); - resource = (Patient) response.getEntry().get(0).getResource(); - assertEquals("PRP1660", resource.getIdentifier().get(0).getValueElement().getValue()); - - } - - - - @Test - public void testSearchWithStringIncludes() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClientWithStringIncludes client = ourCtx.newRestfulClient(ITestClientWithStringIncludes.class, "http://foo"); - client.getPatientWithIncludes(new StringParam("aaa"), "inc1"); - - assertEquals("http://foo/Patient?withIncludes=aaa&_include=inc1", capt.getValue().getURI().toString()); - - } - - @Test - public void testSearchWithSummary() throws Exception { - - final String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { - @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { - return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); - } - }); - - // httpResponse = new BasicHttpResponse(statusline, catalog, locale) - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - - ITestClientWithSummary client = ourCtx.newRestfulClient(ITestClientWithSummary.class, "http://foo"); - - int idx = 0; - - client.getPatientWithIncludes((SummaryEnum) null); - assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes(SummaryEnum.COUNT); - assertEquals("http://foo/Patient?_summary=count", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes(SummaryEnum.DATA); - assertEquals("http://foo/Patient?_summary=data", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes(Arrays.asList(SummaryEnum.DATA)); - assertEquals("http://foo/Patient?_summary=data", capt.getAllValues().get(idx).getURI().toString()); - idx++; - - client.getPatientWithIncludes(Arrays.asList(SummaryEnum.COUNT, SummaryEnum.DATA)); - assertThat(capt.getAllValues().get(idx).getURI().toString(), either(equalTo("http://foo/Patient?_summary=data&_summary=count")).or(equalTo("http://foo/Patient?_summary=count&_summary=data"))); - idx++; - - client.getPatientWithIncludes(new ArrayList<SummaryEnum>()); - assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); - idx++; - } - - @Test - public void testUpdate() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome response = client.updatePatient(new IdType("100"), patient); - - assertEquals(HttpPut.class, capt.getValue().getClass()); - HttpPut post = (HttpPut) capt.getValue(); - assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/100")); - assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); - assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); - assertEquals("200", response.getId().getVersionIdPart()); - assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); - } - - /** - * Return a FHIR content type, but no content and make sure we handle this without crashing - */ - @Test - public void testUpdateWithEmptyResponse() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray(Constants.HEADER_LOCATION, "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome resp = client.updatePatient(new IdType("Patient/100/_history/200"), patient); - assertNull(resp.getResource()); - assertNull(resp.getOperationOutcome()); - - assertEquals(HttpPut.class, capt.getValue().getClass()); - HttpPut post = (HttpPut) capt.getValue(); - assertEquals("http://foo/Patient/100", post.getURI().toASCIIString()); - - } - - @Test(expected = ResourceVersionConflictException.class) - public void testUpdateWithResourceConflict() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_409_CONFLICT, "Conflict")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - client.updatePatient(new IdType("Patient/100/_history/200"), patient); - } - - @Test - public void testUpdateWithVersion() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome response = client.updatePatient(new IdType("Patient/100/_history/200"), patient); - - assertEquals(HttpPut.class, capt.getValue().getClass()); - HttpPut post = (HttpPut) capt.getValue(); - assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/100")); - assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); - assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); - assertEquals("200", response.getId().getVersionIdPart()); - } - - @Test - public void testValidateNoContentResponse() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_204_NO_CONTENT, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome response = client.validatePatient(patient); - - assertEquals(HttpPost.class, capt.getValue().getClass()); - HttpPost post = (HttpPost) capt.getValue(); - assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/$validate")); - assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); - assertNull(response.getOperationOutcome()); - assertNull(response.getResource()); - } - - @Test - public void testValidateOutcomeResponse() throws Exception { - - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setDiagnostics("ALL GOOD"); - String resp = ourCtx.newJsonParser().encodeResourceToString(oo); - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("urn:foo").setValue("123"); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); - when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); - - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - MethodOutcome response = client.validatePatient(patient); - - assertEquals(HttpPost.class, capt.getValue().getClass()); - HttpPost post = (HttpPost) capt.getValue(); - assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/$validate")); - assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); - assertNotNull(response.getOperationOutcome()); - assertEquals("ALL GOOD", ((OperationOutcome)response.getOperationOutcome()).getIssueFirstRep().getDiagnostics()); - assertNull(response.getResource()); - } - - - @Test - public void testVRead() throws Exception { - - //@formatter:off - String msg = "<Patient xmlns=\"http://hl7.org/fhir\">" - + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" - + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" - + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" - + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" - + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" - + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" - + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" - + "</Patient>"; + //@formatter:off + String msg = "<OperationOutcome xmlns=\"http://hl7.org/fhir\"></OperationOutcome>"; //@formatter:on - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 404, "NOT FOUND")); + Header[] headers = new Header[1]; + headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "2011-01-02T22:01:02"); + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML)); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); - // Patient response = client.findPatientByMrn(new - // IdentifierDt("urn:foo", "123")); - Patient response = client.getPatientById(new IdType("Patient/111/_history/999")); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + try { + client.getPatientById(new IdType("111")); + fail(); + } catch (ResourceNotFoundException e) { + // good + } - assertEquals("http://foo/Patient/111/_history/999", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); + } - } + @Test + public void testReadNoCharset() throws Exception { - private Header[] toHeaderArray(String theName, String theValue) { - return new Header[] { new BasicHeader(theName, theValue) }; - } + String msg = getPatient(); - private void verifyHistoryBundleWithTwoResults(Bundle response) { - assertEquals(2, response.getEntry().size()); - // Older resource - { - BundleEntryComponent olderEntry = response.getEntry().get(0); - assertEquals("http://acme.com/Patient/111", olderEntry.getResource().getId()); - } - // Newer resource - { - BundleEntryComponent newerEntry = response.getEntry().get(1); - assertEquals("http://acme.com/Patient/222", newerEntry.getResource().getId()); - } - } + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + Header[] headers = new Header[1]; + headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "Wed, 15 Nov 1995 04:58:08 GMT"); + when(myHttpResponse.getAllHeaders()).thenReturn(headers); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML)); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + // Patient response = client.findPatientByMrn(new + // IdentifierDt("urn:foo", "123")); + Patient response = client.getPatientById(new IdType("111")); - private static String getPatientFeedWithOneResult() { - return getPatientFeedWithOneResult(ourCtx); - } + assertEquals("http://foo/Patient/111", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - static String getPatientFeedWithOneResult(FhirContext theCtx) { + InstantType lm = (InstantType) response.getMeta().getLastUpdatedElement(); + lm.setTimeZoneZulu(true); + assertEquals("1995-11-15T04:58:08.000Z", lm.getValueAsString()); - Bundle retVal = new Bundle(); + } - Patient p = new Patient(); - p.addName().setFamily("Cardinal").addGiven("John"); - p.addIdentifier().setValue("PRP1660"); - retVal.addEntry().setResource(p); + @Test + public void testResponseContainingOldStyleXmlContentType() throws Exception { - return theCtx.newXmlParser().encodeResourceToString(retVal); + String msg = getPatient(); - // String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + - // "<title/>\n" + - // "<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" + - // "<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" + - // "<author>\n" + - // "<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" + - // "</author>\n" + - // "<entry>\n" + - // "<content type=\"text/xml\">" - // + "<Patient xmlns=\"http://hl7.org/fhir\">" - // + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" - // + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" - // + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" - // + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" - // + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" - // + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" - // + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" - // + "</Patient>" - // + "</content>\n" - // + " </entry>\n" - // + "</feed>"; - // return msg; - } + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "application/fhir+xml; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - private interface ClientWithoutAnnotation extends IBasicClient { - Patient read(@IdParam IdType theId); - } + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + // Patient response = client.findPatientByMrn(new + // IdentifierDt("urn:foo", "123")); + Patient response = client.getPatientById(new IdType("111")); - @ResourceDef(name = "Patient") - public static class CustomPatient extends Patient { + assertEquals("http://foo/Patient/111", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); - private static final long serialVersionUID = 1L; + } - // nothing - } + @Test + public void testSearchByCompartment() throws Exception { - public interface ITestClientWithCustomType extends IBasicClient { - @Search() - public CustomPatient getPatientByDob(@RequiredParam(name = Patient.SP_BIRTHDATE) DateParam theBirthDate); - } + String msg = getPatientFeedWithOneResult(); - public interface ITestClientWithCustomTypeList extends IBasicClient { - @Search() - public List<CustomPatient> getPatientByDob(@RequiredParam(name = Patient.SP_BIRTHDATE) DateParam theBirthDate); - } + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - public interface ITestClientWithElements extends IBasicClient { - @Search() - public List<Patient> getPatientWithIncludes(@Elements Set<String> theElements); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - @Search() - public List<Patient> getPatientWithIncludes(@Elements String theElements); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); - } + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + List<Patient> response = client.getPatientByCompartmentAndDob(new IdType("123"), new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); - public interface ITestClientWithStringIncludes extends IBasicClient { - @Search() - public Patient getPatientWithIncludes(@RequiredParam(name = "withIncludes") StringParam theString, @IncludeParam String theInclude); - } + assertEquals("http://foo/Patient/123/compartmentName?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValueElement().getValue()); - public interface ITestClientWithSummary extends IBasicClient { - @Search() - public List<Patient> getPatientWithIncludes(List<SummaryEnum> theSummary); + try { + client.getPatientByCompartmentAndDob(new IdType(""), new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.toString(), containsString("null or empty for compartment")); + } - @Search() - public List<Patient> getPatientWithIncludes(SummaryEnum theSummary); + } - } + @Test + public void testSearchByCompositeParam() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + StringParam str = new StringParam("FOO$BAR"); + DateParam date = new DateParam("2001-01-01"); + client.getObservationByNameValueDate(new CompositeParam<StringParam, DateParam>(str, date)); + + assertEquals("http://foo/Observation?" + Observation.SP_CODE_VALUE_DATE + "=" + UrlUtil.escapeUrlParam("FOO\\$BAR$2001-01-01"), capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchByDateRange() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + DateRangeParam param = new DateRangeParam(); + param.setLowerBound(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-01")); + param.setUpperBound(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, "2021-01-01")); + client.getPatientByDateRange(param); + + assertEquals("http://foo/Patient?dateRange=ge2011-01-01&dateRange=le2021-01-01", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchByDob() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + // httpResponse = new BasicHttpResponse(statusline, catalog, locale) + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + List<Patient> response = client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + + assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValueElement().getValue()); + + } + + @Test + public void testSearchByQuantity() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Patient response = client.findPatientQuantity(new QuantityParam(ParamPrefixEnum.GREATERTHAN, 123L, "foo", "bar")); + + assertEquals("http://foo/Patient?quantityParam=gt123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); + + } + + @Test + public void testSearchByToken() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Patient response = client.findPatientByMrn(new TokenParam("urn:foo", "123")); + + assertEquals("http://foo/Patient?identifier=urn%3Afoo%7C123", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); + + } + + @Test + public void testSearchNamedQueryNoParams() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.getPatientNoParams(); + + assertEquals("http://foo/Patient?_query=someQueryNoParams", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchNamedQueryOneParam() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.getPatientOneParam(new StringParam("BB")); + + assertEquals("http://foo/Patient?_query=someQueryOneParam¶m1=BB", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchOrList() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + TokenOrListParam identifiers = new TokenOrListParam(); + identifiers.add("foo", "bar"); + identifiers.add("baz", "boz"); + client.getPatientMultipleIdentifiers(identifiers); + + assertEquals("http://foo/Patient?ids=foo%7Cbar%2Cbaz%7Cboz", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchWithCustomType() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClientWithCustomType client = ourCtx.newRestfulClient(ITestClientWithCustomType.class, "http://foo"); + CustomPatient response = client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + + assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); + + } + + @Test + public void testSearchWithCustomTypeList() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClientWithCustomTypeList client = ourCtx.newRestfulClient(ITestClientWithCustomTypeList.class, "http://foo"); + List<CustomPatient> response = client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + + assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValueElement().getValue()); + + } + + @Test + public void testSearchWithElements() throws Exception { + + final String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + // httpResponse = new BasicHttpResponse(statusline, catalog, locale) + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + ITestClientWithElements client = ourCtx.newRestfulClient(ITestClientWithElements.class, "http://foo"); + + int idx = 0; + + client.getPatientWithIncludes((String) null); + assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes((Set<String>) null); + assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes("test"); + assertEquals("http://foo/Patient?_elements=test", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes("test,foo"); + assertEquals("http://foo/Patient?_elements=test%2Cfoo", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes(new HashSet<String>(Arrays.asList("test", "foo", ""))); + assertEquals("http://foo/Patient?_elements=test%2Cfoo", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + } + + @Test + public void testSearchWithEscapedValues() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + StringAndListParam andListParam = new StringAndListParam(); + StringOrListParam orListParam1 = new StringOrListParam().addOr(new StringParam("NE,NE", false)).addOr(new StringParam("NE,NE", false)); + StringOrListParam orListParam2 = new StringOrListParam().addOr(new StringParam("E$E", true)); + StringOrListParam orListParam3 = new StringOrListParam().addOr(new StringParam("NE\\NE", false)); + StringOrListParam orListParam4 = new StringOrListParam().addOr(new StringParam("E|E", true)); + client.findPatient(andListParam.addAnd(orListParam1).addAnd(orListParam2).addAnd(orListParam3).addAnd(orListParam4)); + + assertThat(capt.getValue().getURI().toString(), containsString("%3A")); + assertEquals("http://foo/Patient?param=NE\\,NE,NE\\,NE¶m=NE\\\\NE¶m:exact=E\\$E¶m:exact=E\\|E", UrlUtil.unescape(capt.getValue().getURI().toString())); + + } + + @Test + public void testSearchWithFormatAndPrettyPrint() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + // TODO: document this + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getAllValues().get(0).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + client.setEncoding(EncodingEnum.JSON); // this needs to be actually + // implemented + client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + assertEquals("http://foo/Patient?birthdate=ge2011-01-02&_format=json", capt.getAllValues().get(1).getURI().toString()); + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + client.setPrettyPrint(true); + client.getPatientByDob(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + assertEquals("http://foo/Patient?birthdate=ge2011-01-02&_format=json&_pretty=true", capt.getAllValues().get(2).getURI().toString()); + + } + + @Test + public void testSearchWithGenericReturnType() throws Exception { + + Bundle bundle = new Bundle(); + + Patient patient = new Patient(); + patient.addIdentifier().setValue("PRP1660"); + bundle.addEntry().setResource(patient); + + Organization org = new Organization(); + org.setName("FOO"); + patient.getManagingOrganization().setResource(org); + + String msg = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(bundle); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + // httpResponse = new BasicHttpResponse(statusline, catalog, locale) + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + List<IBaseResource> response = client.getPatientByDobWithGenericResourceReturnType(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + + assertEquals("http://foo/Patient?birthdate=ge2011-01-02", capt.getValue().getURI().toString()); + ExtendedPatient patientResp = (ExtendedPatient) response.get(0); + assertEquals("PRP1660", patientResp.getIdentifier().get(0).getValueElement().getValue()); + + } + + @Test + public void testSearchWithGlobalSummary() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.setSummary(SummaryEnum.DATA); + client.findPatientByMrn(new TokenParam("sysm", "val")); + + assertEquals("http://foo/Patient?identifier=sysm%7Cval&_summary=data", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchWithIncludes() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.getPatientWithIncludes(new StringParam("aaa"), Arrays.asList(new Include[]{new Include("inc1"), new Include("inc2", true), new Include("inc3", true)})); + + assertEquals("http://foo/Patient?withIncludes=aaa&_include=inc1&_include%3Arecurse=inc2&_include%3Arecurse=inc3", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchWithOptionalParam() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Bundle response = client.findPatientByName(new StringParam("AAA"), null); + + assertEquals("http://foo/Patient?family=AAA", capt.getValue().getURI().toString()); + Patient resource = (Patient) response.getEntry().get(0).getResource(); + assertEquals("PRP1660", resource.getIdentifier().get(0).getValueElement().getValue()); + + /* + * Now with a first name + */ + + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + response = client.findPatientByName(new StringParam("AAA"), new StringParam("BBB")); + + assertEquals("http://foo/Patient?family=AAA&given=BBB", capt.getValue().getURI().toString()); + resource = (Patient) response.getEntry().get(0).getResource(); + assertEquals("PRP1660", resource.getIdentifier().get(0).getValueElement().getValue()); + + } + + + @Test + public void testSearchWithStringIncludes() throws Exception { + + String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClientWithStringIncludes client = ourCtx.newRestfulClient(ITestClientWithStringIncludes.class, "http://foo"); + client.getPatientWithIncludes(new StringParam("aaa"), "inc1"); + + assertEquals("http://foo/Patient?withIncludes=aaa&_include=inc1", capt.getValue().getURI().toString()); + + } + + @Test + public void testSearchWithSummary() throws Exception { + + final String msg = getPatientFeedWithOneResult(); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + // httpResponse = new BasicHttpResponse(statusline, catalog, locale) + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + ITestClientWithSummary client = ourCtx.newRestfulClient(ITestClientWithSummary.class, "http://foo"); + + int idx = 0; + + client.getPatientWithIncludes((SummaryEnum) null); + assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes(SummaryEnum.COUNT); + assertEquals("http://foo/Patient?_summary=count", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes(SummaryEnum.DATA); + assertEquals("http://foo/Patient?_summary=data", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes(Arrays.asList(SummaryEnum.DATA)); + assertEquals("http://foo/Patient?_summary=data", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + client.getPatientWithIncludes(Arrays.asList(SummaryEnum.COUNT, SummaryEnum.DATA)); + assertThat(capt.getAllValues().get(idx).getURI().toString(), either(equalTo("http://foo/Patient?_summary=data&_summary=count")).or(equalTo("http://foo/Patient?_summary=count&_summary=data"))); + idx++; + + client.getPatientWithIncludes(new ArrayList<SummaryEnum>()); + assertEquals("http://foo/Patient", capt.getAllValues().get(idx).getURI().toString()); + idx++; + } + + @Test + public void testUpdate() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome response = client.updatePatient(new IdType("100"), patient); + + assertEquals(HttpPut.class, capt.getValue().getClass()); + HttpPut post = (HttpPut) capt.getValue(); + assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/100")); + assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); + assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); + assertEquals("200", response.getId().getVersionIdPart()); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, capt.getAllValues().get(0).getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue()); + } + + /** + * Return a FHIR content type, but no content and make sure we handle this without crashing + */ + @Test + public void testUpdateWithEmptyResponse() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray(Constants.HEADER_LOCATION, "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome resp = client.updatePatient(new IdType("Patient/100/_history/200"), patient); + assertNull(resp.getResource()); + assertNull(resp.getOperationOutcome()); + + assertEquals(HttpPut.class, capt.getValue().getClass()); + HttpPut post = (HttpPut) capt.getValue(); + assertEquals("http://foo/Patient/100", post.getURI().toASCIIString()); + + } + + @Test(expected = ResourceVersionConflictException.class) + public void testUpdateWithResourceConflict() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_409_CONFLICT, "Conflict")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + client.updatePatient(new IdType("Patient/100/_history/200"), patient); + } + + @Test + public void testUpdateWithVersion() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome response = client.updatePatient(new IdType("Patient/100/_history/200"), patient); + + assertEquals(HttpPut.class, capt.getValue().getClass()); + HttpPut post = (HttpPut) capt.getValue(); + assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/100")); + assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); + assertEquals("http://example.com/fhir/Patient/100/_history/200", response.getId().getValue()); + assertEquals("200", response.getId().getVersionIdPart()); + } + + @Test + public void testValidateNoContentResponse() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), Constants.STATUS_HTTP_204_NO_CONTENT, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome response = client.validatePatient(patient); + + assertEquals(HttpPost.class, capt.getValue().getClass()); + HttpPost post = (HttpPost) capt.getValue(); + assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/$validate")); + assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); + assertNull(response.getOperationOutcome()); + assertNull(response.getResource()); + } + + @Test + public void testValidateServerBaseWithInvalidResponse() throws Exception { + + String response = "AAAAAAA"; + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(response), Charset.forName("UTF-8"))); + + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE); + IGenericClient client = ourCtx.newRestfulGenericClient("http://testValidateServerBaseWithInvalidResponse"); + try { + client.read().resource("Patient").withId("1").execute(); + fail(); + } catch (FhirClientConnectionException e) { + assertEquals("Failed to retrieve the server metadata statement during client initialization. URL used was http://testValidateServerBaseWithInvalidResponse/metadata", e.getMessage()); + } + + } + + + @Test + public void testValidateOutcomeResponse() throws Exception { + + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDiagnostics("ALL GOOD"); + String resp = ourCtx.newJsonParser().encodeResourceToString(oo); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:foo").setValue("123"); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8"))); + when(myHttpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + MethodOutcome response = client.validatePatient(patient); + + assertEquals(HttpPost.class, capt.getValue().getClass()); + HttpPost post = (HttpPost) capt.getValue(); + assertThat(post.getURI().toASCIIString(), StringEndsWith.endsWith("/Patient/$validate")); + assertThat(IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8), StringContains.containsString("<Patient")); + assertNotNull(response.getOperationOutcome()); + assertEquals("ALL GOOD", ((OperationOutcome) response.getOperationOutcome()).getIssueFirstRep().getDiagnostics()); + assertNull(response.getResource()); + } + + + @Test + public void testVRead() throws Exception { + + //@formatter:off + String msg = "<Patient xmlns=\"http://hl7.org/fhir\">" + + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" + + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" + + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" + + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" + + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" + + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" + + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" + + "</Patient>"; + //@formatter:on + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + // Patient response = client.findPatientByMrn(new + // IdentifierDt("urn:foo", "123")); + Patient response = client.getPatientById(new IdType("Patient/111/_history/999")); + + assertEquals("http://foo/Patient/111/_history/999", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValueElement().getValue()); + + } + + private Header[] toHeaderArray(String theName, String theValue) { + return new Header[]{new BasicHeader(theName, theValue)}; + } + + private void verifyHistoryBundleWithTwoResults(Bundle response) { + assertEquals(2, response.getEntry().size()); + // Older resource + { + BundleEntryComponent olderEntry = response.getEntry().get(0); + assertEquals("http://acme.com/Patient/111", olderEntry.getResource().getId()); + } + // Newer resource + { + BundleEntryComponent newerEntry = response.getEntry().get(1); + assertEquals("http://acme.com/Patient/222", newerEntry.getResource().getId()); + } + } + + private interface ClientWithoutAnnotation extends IBasicClient { + Patient read(@IdParam IdType theId); + } + + public interface ITestClientWithCustomType extends IBasicClient { + @Search() + public CustomPatient getPatientByDob(@RequiredParam(name = Patient.SP_BIRTHDATE) DateParam theBirthDate); + } + + public interface ITestClientWithCustomTypeList extends IBasicClient { + @Search() + public List<CustomPatient> getPatientByDob(@RequiredParam(name = Patient.SP_BIRTHDATE) DateParam theBirthDate); + } + + public interface ITestClientWithElements extends IBasicClient { + @Search() + public List<Patient> getPatientWithIncludes(@Elements Set<String> theElements); + + @Search() + public List<Patient> getPatientWithIncludes(@Elements String theElements); + + } + + public interface ITestClientWithStringIncludes extends IBasicClient { + @Search() + public Patient getPatientWithIncludes(@RequiredParam(name = "withIncludes") StringParam theString, @IncludeParam String theInclude); + } + + public interface ITestClientWithSummary extends IBasicClient { + @Search() + public List<Patient> getPatientWithIncludes(List<SummaryEnum> theSummary); + + @Search() + public List<Patient> getPatientWithIncludes(SummaryEnum theSummary); + + } + + @ResourceDef(name = "Patient") + public static class CustomPatient extends Patient { + + private static final long serialVersionUID = 1L; + + // nothing + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + private static String getPatientFeedWithOneResult() { + return getPatientFeedWithOneResult(ourCtx); + } + + static String getPatientFeedWithOneResult(FhirContext theCtx) { + + Bundle retVal = new Bundle(); + + Patient p = new Patient(); + p.addName().setFamily("Cardinal").addGiven("John"); + p.addIdentifier().setValue("PRP1660"); + retVal.addEntry().setResource(p); + + return theCtx.newXmlParser().encodeResourceToString(retVal); + + // String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + + // "<title/>\n" + + // "<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" + + // "<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" + + // "<author>\n" + + // "<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" + + // "</author>\n" + + // "<entry>\n" + + // "<content type=\"text/xml\">" + // + "<Patient xmlns=\"http://hl7.org/fhir\">" + // + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" + // + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" + // + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" + // + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" + // + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" + // + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" + // + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" + // + "</Patient>" + // + "</content>\n" + // + " </entry>\n" + // + "</feed>"; + // return msg; + } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d425b41e8fc..425e4b9a43b 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -283,6 +283,12 @@ AuthorizationInterceptor could not authorize these requests if it was depending on headers being present. </action> + <action type="fix"> + When using a client in DSTU3/R4 mode, if the client attempted to validate the server + CapabilityStatement but was not able to parse the response, the client would throw + an exception with a misleading error about the Conformance resource not existing. This + has been corrected. Thanks to Shayaan Munshi for reporting and providing a test case! + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From 3e283740299d569a6ba9009c1fb736af34e13d20 Mon Sep 17 00:00:00 2001 From: Ari Ruotsalainen <Ari.Ruotsalainen@solita.fi> Date: Fri, 11 Jan 2019 17:11:53 +0200 Subject: [PATCH 10/56] Use windows line-endings in mvnw.cmd --- mvnw.cmd | 322 +++++++++++++++++++++++++++---------------------------- 1 file changed, 161 insertions(+), 161 deletions(-) diff --git a/mvnw.cmd b/mvnw.cmd index e5cfb0ae9ea..48363fa60b9 100755 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,161 +1,161 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 93bf2788ec42982a5f84a5aae93bd4064fbfca58 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sat, 12 Jan 2019 14:23:26 -0600 Subject: [PATCH 11/56] Add subscription narrowing interceptor and refactor RuleBuilder to be a bit cleaner --- .../uhn/fhir/rest/param/BaseAndListParam.java | 4 +- .../cache/SubscriptionCanonicalizer.java | 4 +- .../interceptor/auth/AuthorizedList.java | 44 +++-- .../auth/IAuthRuleBuilderRuleOp.java | 3 + .../server/interceptor/auth/RuleBuilder.java | 52 +++--- .../server/interceptor/auth/RuleImplOp.java | 107 ++++++++---- .../auth/SearchNarrowingInterceptor.java | 34 +++- .../interceptor/auth/RuleBuilderTest.java | 14 ++ .../AuthorizationInterceptorDstu3Test.java | 162 +++++++++++++----- .../auth/SearchNarrowingInterceptorTest.java | 22 ++- 10 files changed, 326 insertions(+), 120 deletions(-) create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java index fcb50e4c461..39e23a5bd68 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.param; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index d8661af2127..b7a3f09ce7c 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java index f99918e3274..441e0fc4e06 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.rest.server.interceptor.auth; +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2019 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.rest.api.server.RequestDetails; import org.apache.commons.lang3.Validate; @@ -11,15 +31,15 @@ import java.util.List; */ public class AuthorizedList { - private List<String> myCompartments; - private List<String> myResources; + private List<String> myAllowedCompartments; + private List<String> myAllowedInstances; - List<String> getCompartments() { - return myCompartments; + List<String> getAllowedCompartments() { + return myAllowedCompartments; } - List<String> getResources() { - return myResources; + List<String> getAllowedInstances() { + return myAllowedInstances; } /** @@ -30,10 +50,10 @@ public class AuthorizedList { */ public AuthorizedList addCompartment(String theCompartment) { Validate.notNull(theCompartment, "theCompartment must not be null"); - if (myCompartments == null) { - myCompartments = new ArrayList<>(); + if (myAllowedCompartments == null) { + myAllowedCompartments = new ArrayList<>(); } - myCompartments.add(theCompartment); + myAllowedCompartments.add(theCompartment); return this; } @@ -60,10 +80,10 @@ public class AuthorizedList { */ public AuthorizedList addResource(String theResource) { Validate.notNull(theResource, "theResource must not be null"); - if (myResources == null) { - myResources = new ArrayList<>(); + if (myAllowedInstances == null) { + myAllowedInstances = new ArrayList<>(); } - myResources.add(theResource); + myAllowedInstances.add(theResource); return this; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java index e9521ad5860..ce1b1702fdf 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java @@ -2,6 +2,8 @@ package ca.uhn.fhir.rest.server.interceptor.auth; import org.hl7.fhir.instance.model.api.IIdType; +import java.util.Collection; + /* * #%L * HAPI FHIR - Server Framework @@ -58,4 +60,5 @@ public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthR */ IAuthRuleFinished instance(IIdType theId); + IAuthRuleFinished instances(Collection<IIdType> theInstances); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index dc8d258392e..0557989e55f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -172,7 +172,6 @@ public class RuleBuilder implements IAuthRuleBuilder { private PolicyEnum myRuleMode; private String myRuleName; - private RuleOpEnum myRuleOp; RuleBuilderRule(PolicyEnum theRuleMode, String theRuleName) { myRuleMode = theRuleMode; @@ -186,8 +185,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOp delete() { - myRuleOp = RuleOpEnum.DELETE; - return new RuleBuilderRuleOp(); + return new RuleBuilderRuleOp(RuleOpEnum.DELETE); } @Override @@ -211,14 +209,12 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderPatch patch() { - myRuleOp = RuleOpEnum.PATCH; return new PatchBuilder(); } @Override public IAuthRuleBuilderRuleOp read() { - myRuleOp = RuleOpEnum.READ; - return new RuleBuilderRuleOp(); + return new RuleBuilderRuleOp(RuleOpEnum.READ); } @Override @@ -233,8 +229,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOp write() { - myRuleOp = RuleOpEnum.WRITE; - return new RuleBuilderRuleOp(); + return new RuleBuilderRuleOp(RuleOpEnum.WRITE); } @Override @@ -245,7 +240,6 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleConditional implements IAuthRuleBuilderRuleConditional { private AppliesTypeEnum myAppliesTo; - private Set<?> myAppliesToTypes; private RestOperationTypeEnum myOperationType; @@ -291,13 +285,15 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleOp implements IAuthRuleBuilderRuleOp { - private AppliesTypeEnum myAppliesTo; - private Set<?> myAppliesToTypes; + private final RuleOpEnum myRuleOp; + + public RuleBuilderRuleOp(RuleOpEnum theRuleOp) { + myRuleOp = theRuleOp; + } @Override public IAuthRuleBuilderRuleOpClassifier allResources() { - myAppliesTo = AppliesTypeEnum.ALL_RESOURCES; - return new RuleBuilderRuleOpClassifier(); + return new RuleBuilderRuleOpClassifier(AppliesTypeEnum.ALL_RESOURCES, null); } @Override @@ -312,15 +308,22 @@ public class RuleBuilder implements IAuthRuleBuilder { Validate.notBlank(theId.getValue(), "theId.getValue() must not be null or empty"); Validate.notBlank(theId.getIdPart(), "theId must contain an ID part"); - return new RuleBuilderRuleOpClassifier(Collections.singletonList(theId)).finished(); + List<IIdType> instances = Collections.singletonList(theId); + return instances(instances); + } + + @Override + public IAuthRuleFinished instances(Collection<IIdType> theInstances) { + Validate.notNull(theInstances, "theInstances must not be null"); + Validate.notEmpty(theInstances, "theInstances must not be empty"); + + return new RuleBuilderRuleOpClassifier(theInstances).finished(); } @Override public IAuthRuleBuilderRuleOpClassifier resourcesOfType(Class<? extends IBaseResource> theType) { Validate.notNull(theType, "theType must not be null"); - myAppliesTo = AppliesTypeEnum.TYPES; - myAppliesToTypes = Collections.singleton(theType); - return new RuleBuilderRuleOpClassifier(); + return new RuleBuilderRuleOpClassifier(AppliesTypeEnum.TYPES, Collections.singleton(theType)); } private class RuleBuilderRuleOpClassifier implements IAuthRuleBuilderRuleOpClassifier { @@ -328,21 +331,26 @@ public class RuleBuilder implements IAuthRuleBuilder { private ClassifierTypeEnum myClassifierType; private String myInCompartmentName; private Collection<? extends IIdType> myInCompartmentOwners; - private List<IIdType> myAppliesToInstances; + private Collection<IIdType> myAppliesToInstances; + private final AppliesTypeEnum myAppliesTo; + private final Set<?> myAppliesToTypes; /** * Constructor */ - RuleBuilderRuleOpClassifier() { + RuleBuilderRuleOpClassifier(AppliesTypeEnum theAppliesTo, Set<Class<? extends IBaseResource>> theAppliesToTypes) { super(); + myAppliesTo = theAppliesTo; + myAppliesToTypes=theAppliesToTypes; } /** * Constructor */ - RuleBuilderRuleOpClassifier(List<IIdType> theAppliesToInstances) { + RuleBuilderRuleOpClassifier(Collection<IIdType> theAppliesToInstances) { myAppliesToInstances = theAppliesToInstances; myAppliesTo = AppliesTypeEnum.INSTANCES; + myAppliesToTypes = null; } private IAuthRuleBuilderRuleOpClassifierFinished finished() { @@ -554,6 +562,10 @@ public class RuleBuilder implements IAuthRuleBuilder { private class PatchBuilder implements IAuthRuleBuilderPatch { + public PatchBuilder() { + super(); + } + @Override public IAuthRuleFinished allRequests() { BaseRule rule = new RuleImplPatch(myRuleName) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index 7ed4209830d..c818140ba3d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -20,10 +20,7 @@ import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -37,9 +34,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -48,6 +45,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ +@SuppressWarnings("EnumSwitchStatementWhichMissesCases") class RuleImplOp extends BaseRule /* implements IAuthRule */ { private AppliesTypeEnum myAppliesTo; @@ -57,7 +55,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { private ClassifierTypeEnum myClassifierType; private RuleOpEnum myOp; private TransactionAppliesToEnum myTransactionAppliesToOp; - private List<IIdType> myAppliesToInstances; + private Collection<IIdType> myAppliesToInstances; /** * Constructor @@ -77,7 +75,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); IBaseResource appliesToResource; - IIdType appliesToResourceId = null; + Collection<IIdType> appliesToResourceId = null; String appliesToResourceType = null; Map<String, String[]> appliesToSearchParams = null; switch (myOp) { @@ -90,7 +88,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { switch (theOperation) { case READ: case VREAD: - appliesToResourceId = theInputResourceId; + appliesToResourceId = Collections.singleton(theInputResourceId); appliesToResourceType = theInputResourceId.getResourceType(); break; case SEARCH_SYSTEM: @@ -105,6 +103,29 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } appliesToResourceType = theRequestDetails.getResourceName(); appliesToSearchParams = theRequestDetails.getParameters(); + + /* + * If this is a search with an "_id" parameter, we can treat this + * as a read for the given resource ID(s) + */ + if (theRequestDetails.getParameters().containsKey("_id")) { + String[] idValues = theRequestDetails.getParameters().get("_id"); + appliesToResourceId = new ArrayList<>(); + for (String next : idValues) { + IIdType nextId = ctx.getVersion().newIdType().setValue(next); + if (nextId.hasIdPart()){ + if (!nextId.hasResourceType()) { + nextId = nextId.withResourceType(appliesToResourceType); + } + if (nextId.getResourceType().equals(appliesToResourceType)) { + appliesToResourceId.add(nextId); + } + } + } + if (appliesToResourceId.isEmpty()) { + appliesToResourceId = null; + } + } break; case HISTORY_TYPE: if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { @@ -116,7 +137,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { return new Verdict(PolicyEnum.ALLOW, this); } - appliesToResourceId = theInputResourceId; + appliesToResourceId = Collections.singleton(theInputResourceId); break; case GET_PAGE: return new Verdict(PolicyEnum.ALLOW, this); @@ -145,7 +166,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } appliesToResource = theOutputResource; if (theOutputResource != null) { - appliesToResourceId = theOutputResource.getIdElement(); + appliesToResourceId = Collections.singleton(theOutputResource.getIdElement()); } break; case WRITE: @@ -160,7 +181,9 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { case META_ADD: case META_DELETE: appliesToResource = theInputResource; - appliesToResourceId = theInputResourceId; + if (theInputResourceId != null) { + appliesToResourceId = Collections.singletonList(theInputResourceId); + } break; default: return null; @@ -291,19 +314,29 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { switch (myAppliesTo) { case INSTANCES: - if (appliesToResourceId != null) { - for (IIdType next : myAppliesToInstances) { - if (isNotBlank(next.getResourceType())) { - if (!next.getResourceType().equals(appliesToResourceId.getResourceType())) { + if (appliesToResourceId != null && appliesToResourceId.size() > 0) { + int haveMatches = 0; + for (IIdType requestAppliesToResource : appliesToResourceId) { + + for (IIdType next : myAppliesToInstances) { + if (isNotBlank(next.getResourceType())) { + if (!next.getResourceType().equals(requestAppliesToResource.getResourceType())) { + continue; + } + } + if (!next.getIdPart().equals(requestAppliesToResource.getIdPart())) { continue; } + if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { + return null; + } + haveMatches++; + break; } - if (!next.getIdPart().equals(appliesToResourceId.getIdPart())) { - continue; - } - if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) { - return null; - } + + } + + if (haveMatches == appliesToResourceId.size()) { return newVerdict(); } } @@ -326,10 +359,14 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } } } - if (appliesToResourceId != null && appliesToResourceId.hasResourceType()) { - Class<? extends IBaseResource> type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceId.getResourceType()).getImplementingClass(); - if (myAppliesToTypes.contains(type) == false) { - return null; + if (appliesToResourceId != null) { + for (IIdType nextRequestAppliesToResourceId : appliesToResourceId) { + if (nextRequestAppliesToResourceId.hasResourceType()) { + Class<? extends IBaseResource> type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(nextRequestAppliesToResourceId.getResourceType()).getImplementingClass(); + if (myAppliesToTypes.contains(type) == false) { + return null; + } + } } } if (appliesToResourceType != null) { @@ -356,6 +393,16 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { case IN_COMPARTMENT: FhirTerser t = ctx.newTerser(); boolean foundMatch = false; + + if (appliesToResourceId != null && appliesToResourceId.size() > 0) { + boolean haveOwnersForAll = appliesToResourceId + .stream() + .allMatch(n -> myClassifierCompartmentOwners.contains(n.toUnqualifiedVersionless())); + if (haveOwnersForAll) { + foundMatch = true; + } + } + for (IIdType next : myClassifierCompartmentOwners) { if (appliesToResource != null) { if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, appliesToResource, next)) { @@ -363,12 +410,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { break; } } - if (appliesToResourceId != null && appliesToResourceId.hasResourceType() && appliesToResourceId.hasIdPart()) { - if (appliesToResourceId.toUnqualifiedVersionless().getValue().equals(next.toUnqualifiedVersionless().getValue())) { - foundMatch = true; - break; - } - } /* * If the client has permission to read compartment @@ -490,7 +531,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { myAppliesTo = theAppliesTo; } - public void setAppliesToInstances(List<IIdType> theAppliesToInstances) { + public void setAppliesToInstances(Collection<IIdType> theAppliesToInstances) { myAppliesToInstances = theAppliesToInstances; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java index 92bbb719633..9317cfcdef7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.rest.server.interceptor.auth; +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2019 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.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; @@ -51,9 +71,12 @@ public abstract class SearchNarrowingInterceptor extends InterceptorAdapter { * </p> * * @param theRequestDetails The individual request currently being applied + * @return The list of allowed compartments and instances that should be used + * for search narrowing. If this method returns <code>null</code>, no narrowing will + * be performed */ protected AuthorizedList buildAuthorizedList(@SuppressWarnings("unused") RequestDetails theRequestDetails) { - return new AuthorizedList(); + return null; } @@ -71,16 +94,19 @@ public abstract class SearchNarrowingInterceptor extends InterceptorAdapter { RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName()); HashMap<String, List<String>> parameterToOrValues = new HashMap<>(); AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails); + if (authorizedList == null) { + return true; + } /* * Create a map of search parameter values that need to be added to the * given request */ - Collection<String> compartments = authorizedList.getCompartments(); + Collection<String> compartments = authorizedList.getAllowedCompartments(); if (compartments != null) { processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, compartments, true); } - Collection<String> resources = authorizedList.getResources(); + Collection<String> resources = authorizedList.getAllowedInstances(); if (resources != null) { processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, resources, false); } @@ -147,7 +173,7 @@ public abstract class SearchNarrowingInterceptor extends InterceptorAdapter { private void processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, HashMap<String, List<String>> theParameterToOrValues, Collection<String> theResourcesOrCompartments, boolean theAreCompartments) { String lastCompartmentName = null; - String lastSearchParamName=null; + String lastSearchParamName = null; for (String nextCompartment : theResourcesOrCompartments) { Validate.isTrue(StringUtils.countMatches(nextCompartment, '/') == 1, "Invalid compartment name (must be in form \"ResourceType/xxx\": %s", nextCompartment); String compartmentName = nextCompartment.substring(0, nextCompartment.indexOf('/')); diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java new file mode 100644 index 00000000000..70f9afabd61 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class RuleBuilderTest { + + @Test + public void testCollapseReadInstancesIntoSingleRule() { + + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java index 603194a0e35..5fb6b005d8b 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.api.*; import ca.uhn.fhir.rest.api.server.IRequestOperationCallback; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; @@ -629,7 +630,7 @@ public class AuthorizationInterceptorDstu3Test { httpPost.setEntity(new StringEntity(ourCtx.newJsonParser().encodeResourceToString(bundle), ContentType.create(Constants.CT_FHIR_JSON_NEW, Charsets.UTF_8))); status = ourClient.execute(httpPost); responseString = extractResponseAndClose(status); - assertEquals(responseString,403, status.getStatusLine().getStatusCode()); + assertEquals(responseString, 403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); bundle.getEntry().clear(); @@ -640,7 +641,7 @@ public class AuthorizationInterceptorDstu3Test { httpPost.setEntity(new StringEntity(ourCtx.newJsonParser().encodeResourceToString(bundle), ContentType.create(Constants.CT_FHIR_JSON_NEW, Charsets.UTF_8))); status = ourClient.execute(httpPost); responseString = extractResponseAndClose(status); - assertEquals(responseString,200, status.getStatusLine().getStatusCode()); + assertEquals(responseString, 200, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourHitMethod = false; @@ -652,7 +653,7 @@ public class AuthorizationInterceptorDstu3Test { httpPost.setEntity(new StringEntity(ourCtx.newJsonParser().encodeResourceToString(bundle), ContentType.create(Constants.CT_FHIR_JSON_NEW, Charsets.UTF_8))); status = ourClient.execute(httpPost); responseString = extractResponseAndClose(status); - assertEquals(responseString,403, status.getStatusLine().getStatusCode()); + assertEquals(responseString, 403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourHitMethod = false; @@ -664,7 +665,7 @@ public class AuthorizationInterceptorDstu3Test { httpPost.setEntity(new StringEntity(ourCtx.newJsonParser().encodeResourceToString(bundle), ContentType.create(Constants.CT_FHIR_JSON_NEW, Charsets.UTF_8))); status = ourClient.execute(httpPost); responseString = extractResponseAndClose(status); - assertEquals(responseString,200, status.getStatusLine().getStatusCode()); + assertEquals(responseString, 200, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); } @@ -2499,6 +2500,73 @@ public class AuthorizationInterceptorDstu3Test { } + @Test + public void testReadByInstanceAllowsTargetedSearch() throws Exception { + ourConditionalCreateId = "1"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allow("Rule 1").read().instance("Patient/900").andThen() + .allow("Rule 1").read().instance("Patient/700").andThen() + .build(); + } + }); + + HttpResponse status; + String response; + HttpGet httpGet; + ourReturn = Collections.singletonList(createPatient(900)); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=900"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/900"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=901"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertFalse(ourHitMethod); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/901"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertFalse(ourHitMethod); + + ourHitMethod = false; + // technically this is invalid, but just in case.. + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_id=Patient/901"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertFalse(ourHitMethod); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_id=901"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertFalse(ourHitMethod); + } + @Test public void testReadPageRight() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -3205,45 +3273,6 @@ public class AuthorizationInterceptorDstu3Test { assertTrue(ourHitMethod); } - @AfterClass - public static void afterClassClearContext() throws Exception { - ourServer.stop(); - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @BeforeClass - public static void beforeClass() throws Exception { - - ourPort = PortUtil.findFreePort(); - ourServer = new Server(ourPort); - - DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider(); - DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider(); - DummyOrganizationResourceProvider orgProv = new DummyOrganizationResourceProvider(); - DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider(); - DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider(); - DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider(); - DummyMessageHeaderResourceProvider mshProv = new DummyMessageHeaderResourceProvider(); - PlainProvider plainProvider = new PlainProvider(); - - ServletHandler proxyHandler = new ServletHandler(); - ourServlet = new RestfulServer(ourCtx); - ourServlet.setFhirContext(ourCtx); - ourServlet.setResourceProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv, mshProv); - ourServlet.setPlainProviders(plainProvider); - ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100)); - ServletHolder servletHolder = new ServletHolder(ourServlet); - proxyHandler.addServletWithMapping(servletHolder, "/*"); - ourServer.setHandler(proxyHandler); - ourServer.start(); - - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); - HttpClientBuilder builder = HttpClientBuilder.create(); - builder.setConnectionManager(connectionManager); - ourClient = builder.build(); - - } - public static class DummyCarePlanResourceProvider implements IResourceProvider { @Override @@ -3314,7 +3343,7 @@ public class AuthorizationInterceptorDstu3Test { } @Operation(name = "process-message", idempotent = true) - public Parameters operation0(@OperationParam(name="content") Bundle theInput) { + public Parameters operation0(@OperationParam(name = "content") Bundle theInput) { ourHitMethod = true; return (Parameters) new Parameters().setId("1"); } @@ -3397,7 +3426,9 @@ public class AuthorizationInterceptorDstu3Test { } @Search() - public List<Resource> search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + public List<Resource> search( + @OptionalParam(name = "_id") TokenAndListParam theIds, + @OptionalParam(name = "subject") ReferenceParam theSubject) { ourHitMethod = true; return ourReturn; } @@ -3591,7 +3622,7 @@ public class AuthorizationInterceptorDstu3Test { @Transaction() public Bundle search(IRequestOperationCallback theRequestOperationCallback, @TransactionParam Bundle theInput) { ourHitMethod = true; - if (ourDeleted != null){ + if (ourDeleted != null) { for (IBaseResource next : ourDeleted) { theRequestOperationCallback.resourceDeleted(next); } @@ -3601,6 +3632,45 @@ public class AuthorizationInterceptorDstu3Test { } + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider(); + DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider(); + DummyOrganizationResourceProvider orgProv = new DummyOrganizationResourceProvider(); + DummyEncounterResourceProvider encProv = new DummyEncounterResourceProvider(); + DummyCarePlanResourceProvider cpProv = new DummyCarePlanResourceProvider(); + DummyDiagnosticReportResourceProvider drProv = new DummyDiagnosticReportResourceProvider(); + DummyMessageHeaderResourceProvider mshProv = new DummyMessageHeaderResourceProvider(); + PlainProvider plainProvider = new PlainProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + ourServlet = new RestfulServer(ourCtx); + ourServlet.setFhirContext(ourCtx); + ourServlet.setResourceProviders(patProvider, obsProv, encProv, cpProv, orgProv, drProv, mshProv); + ourServlet.setPlainProviders(plainProvider); + ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100)); + ourServlet.setDefaultResponseEncoding(EncodingEnum.JSON); + ServletHolder servletHolder = new ServletHolder(ourServlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java index f0ce3456333..ccd6c11a2c2 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java @@ -68,6 +68,24 @@ public class SearchNarrowingInterceptorTest { ourNextCompartmentList = null; } + @Test + public void testReturnNull() { + + ourNextCompartmentList = null; + + ourClient + .search() + .forResource("Patient") + .execute(); + + assertEquals("Patient.search", ourLastHitMethod); + assertNull(ourLastCodeParam); + assertNull(ourLastSubjectParam); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + assertNull(ourLastIdParam); + } + @Test public void testNarrowObservationsByPatientContext_ClientRequestedNoParams() { @@ -259,7 +277,9 @@ public class SearchNarrowingInterceptorTest { private static class MySearchNarrowingInterceptor extends SearchNarrowingInterceptor { @Override protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) { - Validate.notNull(ourNextCompartmentList); + if (ourNextCompartmentList == null) { + return null; + } return ourNextCompartmentList; } } From 63b8a70e8a631b289c4813274ca4fb290271102e Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sun, 13 Jan 2019 12:24:51 -0600 Subject: [PATCH 12/56] Allow storing ConceptMap with StructureDefinition as a source or target --- .../jpa/term/BaseHapiTerminologySvcImpl.java | 39 ++++++--- .../jpa/term/TerminologySvcImplR4Test.java | 82 ++++++++++++------- src/changes/changes.xml | 6 ++ 3 files changed, 86 insertions(+), 41 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java index 0c1938637e8..5f24e9d61d1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term; * 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. @@ -25,9 +25,9 @@ import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoCodeSystem; import ca.uhn.fhir.jpa.dao.data.*; -import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.util.ScrollableResultsIterator; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -137,6 +137,8 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, private int myFetchSize = DEFAULT_FETCH_SIZE; private ApplicationContext myApplicationContext; private TransactionTemplate myTxTemplate; + @Autowired + private PlatformTransactionManager myTransactionManager; /** * @param theAdd If true, add the code. If false, remove the code. @@ -368,9 +370,6 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, } } - @Autowired - private PlatformTransactionManager myTransactionManager; - @Override @Transactional public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { @@ -391,7 +390,7 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - txTemplate.execute(t->{ + txTemplate.execute(t -> { theDao.deleteInBatch(link); return null; }); @@ -1190,6 +1189,22 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, termConceptMap.setResource(theResourceTable); termConceptMap.setUrl(theConceptMap.getUrl()); + String source = theConceptMap.hasSourceUriType() ? theConceptMap.getSourceUriType().getValueAsString() : null; + String target = theConceptMap.hasTargetUriType() ? theConceptMap.getTargetUriType().getValueAsString() : null; + + /* + * If this is a mapping between "resources" instead of purely between + * "concepts" (this is a weird concept that is technically possible, at least as of + * FHIR R4), don't try to store the mappings. + * + * See here for a description of what that is: + * http://hl7.org/fhir/conceptmap.html#bnr + */ + if ("StructureDefinition".equals(new IdType(source).getResourceType()) || + "StructureDefinition".equals(new IdType(target).getResourceType())) { + return; + } + /* * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions. */ @@ -1202,11 +1217,9 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, Optional<TermConceptMap> optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrl(conceptMapUrl); if (!optionalExistingTermConceptMapByUrl.isPresent()) { try { - String source = theConceptMap.hasSourceUriType() ? theConceptMap.getSourceUriType().getValueAsString() : null; if (isNotBlank(source)) { termConceptMap.setSource(source); } - String target = theConceptMap.hasTargetUriType() ? theConceptMap.getTargetUriType().getValueAsString() : null; if (isNotBlank(target)) { termConceptMap.setTarget(target); } @@ -1244,12 +1257,12 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc, if (element.hasTarget()) { TermConceptMapGroupElementTarget termConceptMapGroupElementTarget; - for (ConceptMap.TargetElementComponent target : element.getTarget()) { + for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) { termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget(); termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement); - termConceptMapGroupElementTarget.setCode(target.getCode()); - termConceptMapGroupElementTarget.setDisplay(target.getDisplay()); - termConceptMapGroupElementTarget.setEquivalence(target.getEquivalence()); + termConceptMapGroupElementTarget.setCode(elementTarget.getCode()); + termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay()); + termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence()); myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget); if (codesSaved++ % 250 == 0) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java index bbec7646802..785de25822c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/TerminologySvcImplR4Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.term; +import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.entity.TermConceptMap; import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; @@ -8,12 +9,11 @@ import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.ConceptMap; import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence; import org.hl7.fhir.r4.model.UriType; -import org.junit.AfterClass; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.ExpectedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,11 +32,26 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { public final ExpectedException expectedException = ExpectedException.none(); private IIdType myConceptMapId; - private void persistConceptMap() { + @Before + public void before() { + myDaoConfig.setAllowExternalReferences(true); + } + + @After + public void after() { + myDaoConfig.setAllowExternalReferences(new DaoConfig().isAllowExternalReferences()); + } + + private void createAndPersistConceptMap() { + ConceptMap conceptMap = createConceptMap(); + persistConceptMap(conceptMap); + } + + private void persistConceptMap(ConceptMap theConceptMap) { new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - myConceptMapId = myConceptMapDao.create(createConceptMap(), mySrd).getId().toUnqualifiedVersionless(); + myConceptMapId = myConceptMapDao.create(theConceptMap, mySrd).getId().toUnqualifiedVersionless(); } }); } @@ -63,6 +78,17 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { } + + @Test + public void testCreateConceptMapWithVirtualSourceSystem() { + ConceptMap conceptMap = createConceptMap(); + conceptMap.getGroup().forEach(t->t.setSource(null)); + conceptMap.setSource(new CanonicalType("http://hl7.org/fhir/uv/livd/StructureDefinition/loinc-livd")); + + persistConceptMap(conceptMap); + + } + @Test public void testCreateConceptMapWithMissingTargetSystems() { @@ -113,17 +139,17 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testDuplicateConceptMapUrls() { - persistConceptMap(); + createAndPersistConceptMap(); expectedException.expect(UnprocessableEntityException.class); expectedException.expectMessage("Can not create multiple ConceptMap resources with ConceptMap.url \"http://example.com/my_concept_map\", already have one with resource ID: ConceptMap/" + myConceptMapId.getIdPart()); - persistConceptMap(); + createAndPersistConceptMap(); } @Test public void testStoreTermConceptMapAndChildren() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -301,7 +327,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateByCodeSystemsAndSourceCodeOneToMany() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -355,7 +381,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateByCodeSystemsAndSourceCodeOneToOne() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -429,7 +455,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateByCodeSystemsAndSourceCodeUnmapped() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -482,7 +508,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithCodeOnly() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -550,7 +576,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithSourceAndTargetSystem2() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -598,7 +624,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithSourceAndTargetSystem3() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -658,7 +684,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithSourceSystem() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -728,7 +754,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithSourceSystemAndVersion1() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -776,7 +802,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithSourceSystemAndVersion3() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -836,7 +862,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithSourceValueSet() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -906,7 +932,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateUsingPredicatesWithTargetValueSet() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -976,7 +1002,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverse() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1025,7 +1051,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseByCodeSystemsAndSourceCodeUnmapped() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1048,7 +1074,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithCodeOnly() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1104,7 +1130,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithSourceAndTargetSystem1() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1153,7 +1179,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithSourceAndTargetSystem4() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1202,7 +1228,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithSourceSystem() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1260,7 +1286,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithSourceSystemAndVersion() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1320,7 +1346,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithSourceValueSet() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); @@ -1378,7 +1404,7 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test { @Test public void testTranslateWithReverseUsingPredicatesWithTargetValueSet() { - persistConceptMap(); + createAndPersistConceptMap(); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 425e4b9a43b..3f4588b946f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -289,6 +289,12 @@ an exception with a misleading error about the Conformance resource not existing. This has been corrected. Thanks to Shayaan Munshi for reporting and providing a test case! </action> + <action type="fix"> + It is now possible to upload a ConceptMap to the JPA server containing mappings where the + source or target is a StructureDefinition canonical URI. This was previously blocked, as the + system could not apply these mappings. It is now permitted to be stored, although + the system will still not apply these mappings. + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From d5d4092c42048a29b390a2c1b228b77bd4a65928 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sun, 13 Jan 2019 13:26:56 -0500 Subject: [PATCH 13/56] Header updates --- .../uhn/fhir/rest/param/BaseAndListParam.java | 4 ++-- .../cache/SubscriptionCanonicalizer.java | 4 ++-- .../interceptor/auth/AuthorizedList.java | 20 +++++++++++++++++++ .../server/interceptor/auth/RuleImplOp.java | 4 ++-- .../auth/SearchNarrowingInterceptor.java | 20 +++++++++++++++++++ 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java index fcb50e4c461..39e23a5bd68 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseAndListParam.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.param; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index d8661af2127..b7a3f09ce7c 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java index f99918e3274..388bdd822e6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.rest.server.interceptor.auth; +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2019 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.rest.api.server.RequestDetails; import org.apache.commons.lang3.Validate; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index 7ed4209830d..3fa268cd319 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -37,9 +37,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java index 92bbb719633..e2ee93c104f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.rest.server.interceptor.auth; +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2019 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.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; From 38bade04290eeb3c18a4d948e7fdb745420530ed Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sun, 13 Jan 2019 17:22:29 -0600 Subject: [PATCH 14/56] Credit for #1118 --- pom.xml | 4 ++++ src/changes/changes.xml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index 077176f8252..9e3ef32c75b 100644 --- a/pom.xml +++ b/pom.xml @@ -489,6 +489,10 @@ <id>srdo</id> <name>Stig Døssing</name> </developer> + <developer> + <id>ruoat</id> + <name>Ari Ruotsalainen</name> + </developer> </developers> <licenses> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 3f4588b946f..db1c2e5c937 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -295,6 +295,10 @@ system could not apply these mappings. It is now permitted to be stored, although the system will still not apply these mappings. </action> + <action type="add"> + A wrapper script for Maven has been added, enabling new users to use Maven without having + to install it beforehand. Thanks to Ari Ruotsalainen for the Pull Request! + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From 4f5640e54112d6589b17c286a66957d6daafc669 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Mon, 14 Jan 2019 11:39:28 -0500 Subject: [PATCH 15/56] Resilient searchparamregistry (#1165) SearchParamRegistry and SubscriptionRegistry now both poll retrying connection to the server. This will help in particular in the scenario where the subscriptions are managed in a standalone server that depends on a FHIR Server being available to pull search parameters and subscriptions from. --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 1 - .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 5 +- .../fhir/jpa/dao/DaoSearchParamProvider.java | 8 +- .../SubscriptionInterceptorLoader.java | 10 +- .../SubscriptionMatcherInterceptor.java | 9 +- .../fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java | 4 +- ...ceDaoDstu2SearchCustomSearchParamTest.java | 50 ++++---- .../fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java | 8 +- ...ceDaoDstu3SearchCustomSearchParamTest.java | 48 ++++---- ...ResourceDaoDstu3UniqueSearchParamTest.java | 13 +-- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 6 +- ...ourceDaoR4SearchCustomSearchParamTest.java | 54 ++++----- ...hirResourceDaoR4UniqueSearchParamTest.java | 19 ++- .../dao/r4/SearchParamExtractorR4Test.java | 11 -- ...rceProviderCustomSearchParamDstu3Test.java | 66 ++++++----- .../r4/BaseResourceProviderR4Test.java | 2 +- ...sourceProviderCustomSearchParamR4Test.java | 66 ++++++----- .../FhirClientSearchParamProviderTest.java | 5 +- ...tivatesPreExistingSubscriptionsR4Test.java | 4 +- .../resthook/RestHookTestR4Test.java | 4 +- ...rceptorRegisteredToDaoConfigDstu2Test.java | 2 +- .../registry/BaseSearchParamRegistry.java | 108 +++++++++++------- .../registry/ISearchParamProvider.java | 2 +- .../registry/ISearchParamRegistry.java | 6 - .../fhir/jpa/searchparam/retry/Retrier.java | 42 +++++++ .../SearchParamExtractorDstu3Test.java | 11 -- .../jpa/searchparam/retry/RetrierTest.java | 87 ++++++++++++++ .../module/CanonicalSubscription.java | 2 + .../module/cache/SubscriptionLoader.java | 48 +++++--- .../module/cache/SubscriptionRegistry.java | 4 +- .../module/config/BaseSubscriptionConfig.java | 2 + .../FhirClientSearchParamProvider.java | 4 +- .../module/BaseSubscriptionTest.java | 12 +- .../MockFhirClientSearchParamProvider.java | 13 +-- .../MockFhirClientSubscriptionProvider.java | 14 +-- .../module/config/MockProvider.java | 31 +++++ .../module/config/TestSubscriptionConfig.java | 18 ++- .../config/UnregisterScheduledProcessor.java | 40 +++++++ ...kingQueueSubscribableChannelDstu3Test.java | 10 +- .../standalone/SearchParamLoaderTest.java | 61 ++++++++++ .../SubscriptionLoaderFhirClientTest.java | 3 - .../standalone/SubscriptionLoaderTest.java | 61 ++++++++++ 42 files changed, 647 insertions(+), 327 deletions(-) create mode 100644 hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java create mode 100644 hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/retry/RetrierTest.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockProvider.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/UnregisterScheduledProcessor.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SearchParamLoaderTest.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderTest.java 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 9ad47a9730b..6bb1e235b7c 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 @@ -58,7 +58,6 @@ import org.hibernate.Session; import org.hibernate.internal.SessionImpl; import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.r4.model.Bundle.HTTPVerb; -import org.hl7.fhir.r4.model.InstantType; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; 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 fbe42a91c8c..9cfd6afec1c 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 @@ -35,7 +35,9 @@ import ca.uhn.fhir.jpa.util.ExpungeOptions; import ca.uhn.fhir.jpa.util.ExpungeOutcome; import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils; import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils; -import ca.uhn.fhir.model.api.*; +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.*; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -60,7 +62,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java index 638643d6413..961c06021be 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoSearchParamProvider.java @@ -44,12 +44,8 @@ public class DaoSearchParamProvider implements ISearchParamProvider { } @Override - public <SP extends IBaseResource> void refreshCache(BaseSearchParamRegistry<SP> theSearchParamRegistry, long theRefreshInterval) { + public <SP extends IBaseResource> int refreshCache(BaseSearchParamRegistry<SP> theSearchParamRegistry, long theRefreshInterval) { TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.execute(t->{ - theSearchParamRegistry.doRefresh(theRefreshInterval); - return null; - }); - + return txTemplate.execute(t-> theSearchParamRegistry.doRefresh(theRefreshInterval)); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java index f98e0df47be..a39c3f30708 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java @@ -37,7 +37,10 @@ import java.util.Set; public class SubscriptionInterceptorLoader { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionInterceptorLoader.class); - // TODO KHS remove side-effects of autowiring these beans + // TODO KHS these beans are late loaded because we don't want to run their @PostConstruct and @Scheduled method if they're + // not required. Recommend removing @PostConstruct from these classes and instead call those methods in register interceptors below. + // @Schedule will be tricker to resolve + private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; private SubscriptionActivatingInterceptor mySubscriptionActivatingInterceptor; @@ -64,11 +67,6 @@ public class SubscriptionInterceptorLoader { mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class); } ourLog.info("Registering subscription matcher interceptor"); - - if (mySubscriptionMatcherInterceptor == null) { - mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class); - } - myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java index 988814d7349..26894df4a01 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java @@ -45,8 +45,9 @@ import javax.annotation.PreDestroy; public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter { private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class); - static final String SUBSCRIPTION_STATUS = "Subscription.status"; - static final String SUBSCRIPTION_TYPE = "Subscription.channel.type"; + public static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching"; + public static final String SUBSCRIPTION_STATUS = "Subscription.status"; + public static final String SUBSCRIPTION_TYPE = "Subscription.channel.type"; private SubscribableChannel myProcessingChannel; @Autowired @@ -66,9 +67,11 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd @PostConstruct public void start() { if (myProcessingChannel == null) { - myProcessingChannel = mySubscriptionChannelFactory.newMatchingChannel("subscription-matching"); + myProcessingChannel = mySubscriptionChannelFactory.newMatchingChannel(SUBSCRIPTION_MATCHING_CHANNEL_NAME); } myProcessingChannel.subscribe(mySubscriptionMatchingSubscriber); + ourLog.info("Subscription Matching Subscriber subscribed to Matching Channel {} with name {}", myProcessingChannel.getClass().getName(), SUBSCRIPTION_MATCHING_CHANNEL_NAME); + } @SuppressWarnings("unused") diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index d08e7b3b9cc..428bf928507 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -56,7 +56,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Qualifier("myResourceCountsCache") protected ResourceCountCache myResourceCountsCache; @Autowired - protected ISearchParamRegistry mySearchParamRegsitry; + protected ISearchParamRegistry mySearchParamRegistry; @Autowired protected ApplicationContext myAppCtx; @Autowired @@ -181,8 +181,6 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest { @Qualifier("myValueSetDaoDstu2") protected IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt> myValueSetDao; @Autowired - private ISearchParamRegistry mySearchParamRegistry; - @Autowired protected SubscriptionLoader mySubscriptionLoader; @Before 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 90c00800444..6d93836ec5b 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 @@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.dao.dstu2; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; @@ -70,7 +70,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu threadIdSp.setXpathUsage(XPathUsageTypeEnum.NORMAL); threadIdSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(threadIdSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Communication com = new Communication(); com.setStatus(CommunicationStatusEnum.IN_PROGRESS); @@ -163,7 +163,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu sp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Practitioner pract = new Practitioner(); pract.setId("A"); @@ -194,7 +194,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu eyeColourSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(eyeColourSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -215,7 +215,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu attendingSp.addTarget(ResourceTypeEnum.PRACTITIONER); IIdType spId = mySearchParameterDao.create(attendingSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Practitioner p1 = new Practitioner(); p1.getName().addFamily("P1"); @@ -265,7 +265,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu identifierSp.setStatus(ConformanceResourceStatusEnum.RETIRED); mySearchParameterDao.create(identifierSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p = new Patient(); p.addName().addGiven("G"); @@ -307,7 +307,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu identifierSp.setStatus(ConformanceResourceStatusEnum.RETIRED); mySearchParameterDao.create(identifierSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p = new Patient(); p.addName().addGiven("G"); @@ -339,7 +339,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.addTarget(ResourceTypeEnum.ORGANIZATION); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().addFamily("P1"); @@ -382,7 +382,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.addTarget(ResourceTypeEnum.PATIENT); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().addFamily("P1"); @@ -435,7 +435,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().addFamily("P1"); @@ -489,7 +489,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu eyeColourSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(eyeColourSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -522,7 +522,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.addTarget(ResourceTypeEnum.ORGANIZATION); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().addFamily("P2"); @@ -558,7 +558,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.addTarget(ResourceTypeEnum.ORGANIZATION); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().addFamily("P2"); @@ -594,7 +594,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatusEnum.ARRIVED); @@ -635,7 +635,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().addFamily("P2"); @@ -671,7 +671,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().addFamily("P2"); @@ -708,7 +708,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.addTarget(ResourceTypeEnum.APPOINTMENT); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatusEnum.ARRIVED); @@ -747,7 +747,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatusEnum.ARRIVED); @@ -789,7 +789,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.addTarget(ResourceTypeEnum.OBSERVATION); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatusEnum.ARRIVED); @@ -830,7 +830,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().addFamily("P2"); @@ -867,7 +867,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.addIdentifier().setSystem("FOO123").setValue("BAR678"); @@ -912,7 +912,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.addIdentifier().setSystem("http://AAA").setValue("BAR678"); @@ -957,7 +957,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu fooSp.setStatus(ConformanceResourceStatusEnum.ACTIVE); IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGenderEnum.MALE); @@ -988,7 +988,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu // Delete the param mySearchParameterDao.delete(spId, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); myResourceReindexingSvc.forceReindexingPass(); @@ -1016,7 +1016,7 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu fooSp.setStatus(ConformanceResourceStatusEnum.DRAFT); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGenderEnum.MALE); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 25f1dd08211..c01ecfb9d17 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -56,7 +56,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @RunWith(SpringJUnit4ClassRunner.class) @@ -215,7 +215,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Autowired protected ISearchParamPresenceSvc mySearchParamPresenceSvc; @Autowired - protected ISearchParamRegistry mySearchParamRegsitry; + protected ISearchParamRegistry mySearchParamRegistry; @Autowired protected IStaleSearchDeletingSvc myStaleSearchDeletingSvc; @Autowired @@ -256,8 +256,6 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { protected ITermConceptMapGroupElementTargetDao myTermConceptMapGroupElementTargetDao; @Autowired private JpaValidationSupportChainDstu3 myJpaValidationSupportChainDstu3; - @Autowired - protected ISearchParamRegistry mySearchParamRegistry; @After() public void afterCleanupDao() { @@ -306,7 +304,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest { @Before @Transactional() public void beforePurgeDatabase() { - purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegsitry); + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); } @Before diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java index cb52de87033..c9916d62c22 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java @@ -139,7 +139,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu sp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); org.hl7.fhir.dstu3.model.Practitioner pract = new org.hl7.fhir.dstu3.model.Practitioner(); pract.setId("A"); @@ -169,7 +169,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu eyeColourSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(eyeColourSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -191,7 +191,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu attendingSp.getTarget().add(new CodeType("Practitioner")); IIdType spId = mySearchParameterDao.create(attendingSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Practitioner p1 = new Practitioner(); p1.addName().setFamily("P1"); @@ -231,7 +231,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu threadIdSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); threadIdSp.setStatus(Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(threadIdSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Communication com = new Communication(); com.setStatus(Communication.CommunicationStatus.INPROGRESS); @@ -277,7 +277,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu sp.addTarget("Condition"); sp.addTarget("Observation"); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Condition condition = new Condition(); condition.getCode().setText("A condition"); @@ -313,7 +313,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu sp.setType(Enumerations.SearchParamType.TOKEN); sp.setExpression("MedicationRequest.reasonCode | ProcedureRequest.reasonCode"); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); MedicationRequest mr = new MedicationRequest(); mr.addReasonCode().addCoding().setSystem("foo").setCode("bar"); @@ -343,7 +343,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.getTarget().add(new CodeType("Organization")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().setFamily("P1"); @@ -387,7 +387,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.getTarget().add(new CodeType("Patient")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().setFamily("P1"); @@ -441,7 +441,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().setFamily("P1"); @@ -496,7 +496,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu eyeColourSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(eyeColourSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -530,7 +530,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.getTarget().add(new CodeType("Organization")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -568,7 +568,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.getTarget().add(new CodeType("Organization")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -605,7 +605,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -647,7 +647,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -684,7 +684,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -722,7 +722,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.getTarget().add(new CodeType("Appointment")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -764,7 +764,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -807,7 +807,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.getTarget().add(new CodeType("Observation")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -849,7 +849,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu siblingSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -886,7 +886,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu displaySp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(displaySp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); MedicationStatement ms1 = new MedicationStatement(); ms1.setMedication(new CodeableConcept()); @@ -919,7 +919,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Specimen specimen = new Specimen(); specimen.setId("#FOO"); @@ -962,7 +962,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); @@ -993,7 +993,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu // Delete the param mySearchParameterDao.delete(spId, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); myResourceReindexingSvc.forceReindexingPass(); myResourceReindexingSvc.forceReindexingPass(); @@ -1021,7 +1021,7 @@ public class FhirResourceDaoDstu3SearchCustomSearchParamTest extends BaseJpaDstu fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.DRAFT); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java index 2fb1336f5f0..e648d839c7a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UniqueSearchParamTest.java @@ -2,11 +2,10 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.SearchBuilder; import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; -import ca.uhn.fhir.jpa.util.JpaConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.TokenParam; @@ -82,7 +81,7 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } private void createUniqueIndexCoverageBeneficiary() { @@ -121,7 +120,7 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } private void createUniqueNameAndManagingOrganizationSps() { @@ -159,13 +158,13 @@ public class FhirResourceDaoDstu3UniqueSearchParamTest extends BaseJpaDstu3Test .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } @Test public void testDetectUniqueSearchParams() { createUniqueBirthdateAndGenderSps(); - List<JpaRuntimeSearchParam> params = mySearchParamRegsitry.getActiveUniqueSearchParams("Patient"); + List<JpaRuntimeSearchParam> params = mySearchParamRegistry.getActiveUniqueSearchParams("Patient"); assertEquals(1, params.size()); assertEquals(params.get(0).isUnique(), true); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index e020fb031d4..b8ac0fee849 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -14,7 +14,7 @@ import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.term.BaseHapiTerminologySvcImpl; import ca.uhn.fhir.jpa.term.IHapiTerminologySvc; @@ -226,7 +226,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Qualifier("mySearchParameterDaoR4") protected IFhirResourceDao<SearchParameter> mySearchParameterDao; @Autowired - protected ISearchParamRegistry mySearchParamRegsitry; + protected BaseSearchParamRegistry mySearchParamRegistry; @Autowired protected IStaleSearchDeletingSvc myStaleSearchDeletingSvc; @Autowired @@ -326,7 +326,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Transactional() public void beforePurgeDatabase() throws InterruptedException { final EntityManager entityManager = this.myEntityManager; - purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegsitry); + purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry); } @Before 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 612f8256aa2..4b87e0d55e4 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 @@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; @@ -163,7 +163,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test sp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); org.hl7.fhir.r4.model.Practitioner pract = new org.hl7.fhir.r4.model.Practitioner(); pract.setId("A"); @@ -193,7 +193,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test eyeColourSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(eyeColourSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -215,7 +215,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test attendingSp.getTarget().add(new CodeType("Practitioner")); IIdType spId = mySearchParameterDao.create(attendingSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Practitioner p1 = new Practitioner(); p1.addName().setFamily("P1"); @@ -265,7 +265,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test identifierSp.setStatus(Enumerations.PublicationStatus.RETIRED); mySearchParameterDao.create(identifierSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p = new Patient(); p.addName().addGiven("G"); @@ -305,7 +305,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test identifierSp.setStatus(Enumerations.PublicationStatus.RETIRED); mySearchParameterDao.create(identifierSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p = new Patient(); p.addName().addGiven("G"); @@ -342,7 +342,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test sp.addTarget("Condition"); sp.addTarget("Observation"); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Condition condition = new Condition(); condition.getCode().setText("A condition"); @@ -378,7 +378,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test sp.setType(Enumerations.SearchParamType.TOKEN); sp.setExpression("MedicationRequest.reasonCode | ServiceRequest.reasonCode"); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); MedicationRequest mr = new MedicationRequest(); mr.addReasonCode().addCoding().setSystem("foo").setCode("bar"); @@ -425,7 +425,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.getTarget().add(new CodeType("Organization")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().setFamily("P1"); @@ -469,7 +469,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.getTarget().add(new CodeType("Patient")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().setFamily("P1"); @@ -523,7 +523,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.addName().setFamily("P1"); @@ -577,7 +577,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test eyeColourSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(eyeColourSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -611,7 +611,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.getTarget().add(new CodeType("Organization")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -649,7 +649,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.getTarget().add(new CodeType("Organization")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -686,7 +686,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -739,7 +739,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test txTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus theArg0) { - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } }); @@ -783,7 +783,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -821,7 +821,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.getTarget().add(new CodeType("Appointment")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -863,7 +863,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -906,7 +906,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.getTarget().add(new CodeType("Observation")); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Appointment apt = new Appointment(); apt.setStatus(AppointmentStatus.ARRIVED); @@ -948,7 +948,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test siblingSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(siblingSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient patient = new Patient(); patient.addName().setFamily("P2"); @@ -986,7 +986,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.addIdentifier().setSystem("FOO123").setValue("BAR678"); @@ -1030,7 +1030,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.addIdentifier().setSystem("http://AAA").setValue("BAR678"); @@ -1074,7 +1074,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test sp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Specimen specimen = new Specimen(); specimen.setId("#FOO"); @@ -1117,7 +1117,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); IIdType spId = mySearchParameterDao.create(fooSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); @@ -1148,7 +1148,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test // Delete the param mySearchParameterDao.delete(spId, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); myResourceReindexingSvc.forceReindexingPass(); // Try with custom gender SP @@ -1175,7 +1175,7 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.DRAFT); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java index 34a042446d0..c8cbded65c3 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.jpa.util.JpaConstants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.TokenParam; @@ -97,7 +96,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); SearchBuilder.resetLastHandlerMechanismForUnitTest(); } @@ -139,7 +138,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } @@ -168,7 +167,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } @@ -197,7 +196,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } @@ -226,7 +225,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setUrl(SearchParamConstants.EXT_SP_UNIQUE) .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } private void createUniqueNameAndManagingOrganizationSps() { @@ -264,7 +263,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } private void createUniqueObservationSubjectDateCode() { @@ -316,13 +315,13 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { .setValue(new BooleanType(true)); mySearchParameterDao.update(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } @Test public void testDetectUniqueSearchParams() { createUniqueBirthdateAndGenderSps(); - List<JpaRuntimeSearchParam> params = mySearchParamRegsitry.getActiveUniqueSearchParams("Patient"); + List<JpaRuntimeSearchParam> params = mySearchParamRegistry.getActiveUniqueSearchParams("Patient"); assertEquals(1, params.size()); assertTrue(params.get(0).isUnique()); @@ -474,7 +473,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { myResourceIndexedCompositeStringUniqueDao.deleteAll(); - assertEquals(1, mySearchParamRegsitry.getActiveUniqueSearchParams("Observation").size()); + assertEquals(1, mySearchParamRegistry.getActiveUniqueSearchParams("Observation").size()); myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index e34761975e2..3597e4b0b99 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -7,7 +7,6 @@ import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.r4.hapi.ctx.DefaultProfileValidationSupport; @@ -71,11 +70,6 @@ public class SearchParamExtractorR4Test { throw new UnsupportedOperationException(); } - @Override - public void refreshCacheIfNecessary() { - // nothing - } - @Override public void requestRefresh() { // nothing @@ -90,11 +84,6 @@ public class SearchParamExtractorR4Test { public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { return null; } - - @Override - public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) { - // nothing - } }; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java index 1297e57e86e..8346d0afc05 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderCustomSearchParamDstu3Test.java @@ -1,37 +1,43 @@ package ca.uhn.fhir.jpa.provider.dstu3; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.*; - -import java.io.IOException; -import java.util.*; - +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.hl7.fhir.dstu3.model.*; -import org.hl7.fhir.dstu3.model.CapabilityStatement.*; -import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; -import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; -import org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType; -import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.*; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; - -import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestComponent; +import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceComponent; +import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; +import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; +import org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.*; public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProviderDstu3Test { @@ -56,7 +62,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv super.beforeResetConfig(); myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } private Map<String, CapabilityStatementRestResourceSearchParamComponent> extractSearchParams(CapabilityStatement conformance, String resType) { @@ -142,7 +148,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv txTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } }); @@ -198,7 +204,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.RETIRED); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); conformance = ourClient .fetchConformance() @@ -259,7 +265,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv attendingSp.getTarget().add(new CodeType("Practitioner")); IIdType spId = mySearchParameterDao.create(attendingSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Practitioner p1 = new Practitioner(); p1.addName().setFamily("P1"); @@ -309,7 +315,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv .resource(eyeColourSp) .execute(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -351,7 +357,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); @@ -395,7 +401,7 @@ public class ResourceProviderCustomSearchParamDstu3Test extends BaseResourceProv fooSp.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index 7d5ec840386..161a66362b2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -204,7 +204,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { fail("Failed to init subscriptions"); } try { - mySubscriptionLoader.initSubscriptions(); + mySubscriptionLoader.syncSubscriptions(); break; } catch (ResourceVersionConflictException e) { Thread.sleep(250); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java index 953aed5ef72..a1b8c1aabaf 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java @@ -1,37 +1,43 @@ package ca.uhn.fhir.jpa.provider.r4; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.*; - -import java.io.IOException; -import java.util.*; - +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.CapabilityStatement.*; -import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; -import org.hl7.fhir.r4.model.Observation.ObservationStatus; -import org.hl7.fhir.r4.model.SearchParameter.XPathUsageType; -import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.*; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; - -import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent; +import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; +import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.SearchParameter.XPathUsageType; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.*; public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProviderR4Test { @@ -56,7 +62,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide super.beforeResetConfig(); myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } private Map<String, CapabilityStatementRestResourceSearchParamComponent> extractSearchParams(CapabilityStatement conformance, String resType) { @@ -142,7 +148,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide txTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus theStatus) { - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); } }); @@ -198,7 +204,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.RETIRED); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); conformance = ourClient .fetchConformance() @@ -260,7 +266,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide attendingSp.getTarget().add(new CodeType("Practitioner")); IIdType spId = mySearchParameterDao.create(attendingSp, mySrd).getId().toUnqualifiedVersionless(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Practitioner p1 = new Practitioner(); p1.addName().setFamily("P1"); @@ -310,7 +316,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide .resource(eyeColourSp) .execute(); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient p1 = new Patient(); p1.setActive(true); @@ -352,7 +358,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); @@ -396,7 +402,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(fooSp, mySrd); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); Patient pat = new Patient(); pat.setGender(AdministrativeGender.MALE); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java index 6af748eda5d..6385b506216 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/FhirClientSearchParamProviderTest.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSearchParamProvider; import ca.uhn.fhir.rest.api.MethodOutcome; import org.hl7.fhir.r4.model.Coding; @@ -17,8 +16,6 @@ import static org.junit.Assert.assertEquals; public class FhirClientSearchParamProviderTest extends BaseSubscriptionsR4Test { - @Autowired - ISearchParamRegistry mySearchParamRegistry; @Autowired ISearchParamProvider origSearchParamProvider; @@ -44,7 +41,7 @@ public class FhirClientSearchParamProviderTest extends BaseSubscriptionsR4Test { sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); createSubscription(criteria, "application/json"); waitForActivatedSubscriptionCount(1); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java index 8d34fd1f8c2..aa14fad52c7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java @@ -57,7 +57,7 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc @Before public void beforeSetSubscriptionActivatingInterceptor() { SubscriptionActivatingInterceptor.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true); - mySubscriptionLoader.initSubscriptions(); + mySubscriptionLoader.syncSubscriptions(); } @@ -109,7 +109,7 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc createSubscription(criteria2, payload, ourListenerServerBase); mySubscriptionTestUtil.registerRestHookInterceptor(); - mySubscriptionLoader.initSubscriptions(); + mySubscriptionLoader.syncSubscriptions(); sendObservation(code, "SNOMED-CT"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java index 3c9dbf46061..735b051a2ad 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java @@ -206,7 +206,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { waitForActivatedSubscriptionCount(1); for (int i = 0; i < 5; i++) { - int changes = this.mySubscriptionLoader.doInitSubscriptionsForUnitTest(); + int changes = this.mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); assertEquals(0, changes); } } @@ -874,7 +874,7 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); mySearchParameterDao.create(sp); - mySearchParamRegsitry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); createSubscription(criteria, "application/json"); waitForActivatedSubscriptionCount(1); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java index 599ee55a986..40794bb4e88 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test.java @@ -69,7 +69,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B ourCreatedObservations.clear(); ourUpdatedObservations.clear(); - mySubscriptionLoader.initSubscriptions(); + mySubscriptionLoader.syncSubscriptions(); } private void waitForQueueToDrain() throws InterruptedException { diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java index 33ab46ec183..95826b555f4 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.searchparam.registry; * 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. @@ -26,6 +26,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.retry.Retrier; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.util.SearchParameterUtil; import ca.uhn.fhir.util.StopWatch; @@ -44,6 +45,14 @@ import java.util.*; import static org.apache.commons.lang3.StringUtils.isBlank; public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implements ISearchParamRegistry { + + private static final int MAX_MANAGED_PARAM_COUNT = 10000; + private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamRegistry.class); + @VisibleForTesting + public static final int INITIAL_SECONDS_BETWEEN_RETRIES = 5; + private static long REFRESH_INTERVAL = 60 * DateUtils.MILLIS_PER_MINUTE; + private static final int MAX_RETRIES = 60; // 5 minutes + @Autowired private ModelConfig myModelConfig; @Autowired @@ -51,48 +60,37 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen @Autowired private FhirContext myFhirContext; - private static final int MAX_MANAGED_PARAM_COUNT = 10000; - private static final Logger ourLog = LoggerFactory.getLogger(BaseSearchParamRegistry.class); + private volatile int mySecondsBetweenRetries = INITIAL_SECONDS_BETWEEN_RETRIES; private Map<String, Map<String, RuntimeSearchParam>> myBuiltInSearchParams; private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap(); private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap(); private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams; private volatile long myLastRefresh; - @Override - public void requestRefresh() { - synchronized (this) { - myLastRefresh = 0; - } - } - - @Override - public void forceRefresh() { - requestRefresh(); - refreshCacheIfNecessary(); - } - @Override public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { + + requiresActiveSearchParams(); RuntimeSearchParam retVal = null; - Map<String, RuntimeSearchParam> params = getActiveSearchParams().get(theResourceName); + Map<String, RuntimeSearchParam> params = myActiveSearchParams.get(theResourceName); if (params != null) { retVal = params.get(theParamName); } return retVal; } - - @Override - public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() { - return myActiveSearchParams; - } - @Override public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) { + requiresActiveSearchParams(); return myActiveSearchParams.get(theResourceName); } + void requiresActiveSearchParams() { + if (myActiveSearchParams == null) { + refreshCacheWithRetry(); + } + } + @Override public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName) { List<JpaRuntimeSearchParam> retVal = myActiveUniqueSearchParams.get(theResourceName); @@ -223,21 +221,10 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen } myBuiltInSearchParams = Collections.unmodifiableMap(resourceNameToSearchParams); - - refreshCacheIfNecessary(); } - @Override - public void refreshCacheIfNecessary() { - long refreshInterval = 60 * DateUtils.MILLIS_PER_MINUTE; - if (System.currentTimeMillis() - refreshInterval > myLastRefresh) { - synchronized (this) { - mySearchParamProvider.refreshCache(this, refreshInterval); - } - } - } - public void doRefresh(long theRefreshInterval) { + public int doRefresh(long theRefreshInterval) { if (System.currentTimeMillis() - theRefreshInterval > myLastRefresh) { StopWatch sw = new StopWatch(); @@ -317,14 +304,9 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen myLastRefresh = System.currentTimeMillis(); ourLog.info("Refreshed search parameter cache in {}ms", sw.getMillis()); } + return myActiveSearchParams.size(); } - @Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND) - public void refreshCacheOnSchedule() { - refreshCacheIfNecessary(); - } - - protected abstract RuntimeSearchParam toRuntimeSp(SP theNextSp); @Override @@ -338,9 +320,49 @@ public abstract class BaseSearchParamRegistry<SP extends IBaseResource> implemen return getActiveSearchParams(theResourceDef.getName()).values(); } - @VisibleForTesting @Override + public void requestRefresh() { + synchronized (this) { + myLastRefresh = 0; + } + } + + @Override + public void forceRefresh() { + requestRefresh(); + refreshCacheWithRetry(); + } + + @VisibleForTesting public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) { mySearchParamProvider = theSearchParamProvider; } + + synchronized int refreshCacheWithRetry() { + Retrier<Integer> refreshCacheRetrier = new Retrier(() -> mySearchParamProvider.refreshCache(this, REFRESH_INTERVAL), MAX_RETRIES, mySecondsBetweenRetries, "refresh search parameter registry"); + return refreshCacheRetrier.runWithRetry(); + } + + @Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND) + public void refreshCacheOnSchedule() { + refreshCacheIfNecessary(); + } + + public void refreshCacheIfNecessary() { + if (myActiveSearchParams == null || + System.currentTimeMillis() - REFRESH_INTERVAL > myLastRefresh) { + refreshCacheWithRetry(); + } + } + + @VisibleForTesting + public void setSecondsBetweenRetriesForTesting(int theSecondsBetweenRetries) { + mySecondsBetweenRetries = theSecondsBetweenRetries; + } + + @Override + public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() { + requiresActiveSearchParams(); + return Collections.unmodifiableMap(myActiveSearchParams); + } } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java index 6a095e5b85c..6970653b972 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamProvider.java @@ -27,5 +27,5 @@ import org.hl7.fhir.instance.model.api.IBaseResource; public interface ISearchParamProvider { IBundleProvider search(SearchParameterMap theParams); - <SP extends IBaseResource> void refreshCache(BaseSearchParamRegistry<SP> theSPBaseSearchParamRegistry, long theRefreshInterval); + <SP extends IBaseResource> int refreshCache(BaseSearchParamRegistry<SP> theSPBaseSearchParamRegistry, long theRefreshInterval); } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java index 0be19842ba2..57807af7ade 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/ISearchParamRegistry.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.jpa.searchparam.registry; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; -import com.google.common.annotations.VisibleForTesting; import java.util.Collection; import java.util.List; @@ -50,8 +49,6 @@ public interface ISearchParamRegistry { List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName); - void refreshCacheIfNecessary(); - /** * Request that the cache be refreshed at the next convenient time (in a different thread) */ @@ -60,7 +57,4 @@ public interface ISearchParamRegistry { RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName); Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef); - - @VisibleForTesting - void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider); } diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java new file mode 100644 index 00000000000..be7bc738c23 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.jpa.searchparam.retry; + +import org.apache.commons.lang3.time.DateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Supplier; + +public class Retrier<T> { + private static final Logger ourLog = LoggerFactory.getLogger(Retrier.class); + + private final Supplier<T> mySupplier; + private final int myMaxRetries; + private final int mySecondsBetweenRetries; + private final String myDescription; + + public Retrier(Supplier<T> theSupplier, int theMaxRetries, int theSecondsBetweenRetries, String theDescription) { + mySupplier = theSupplier; + myMaxRetries = theMaxRetries; + mySecondsBetweenRetries = theSecondsBetweenRetries; + myDescription = theDescription; + } + + public T runWithRetry() { + RuntimeException lastException = new IllegalStateException("maxRetries must be above zero."); + for (int retryCount = 1; retryCount <= myMaxRetries; ++retryCount) { + try { + return mySupplier.get(); + } catch(RuntimeException e) { + ourLog.info("Failed to {}. Attempt {} / {}: {}", myDescription, retryCount, myMaxRetries, e.getMessage()); + lastException = e; + try { + Thread.sleep(mySecondsBetweenRetries * DateUtils.MILLIS_PER_SECOND); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw lastException; + } + } + } + throw lastException; + } +} diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java index 387f52f2c56..223d0b029e5 100644 --- a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/SearchParamExtractorDstu3Test.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; @@ -68,11 +67,6 @@ public class SearchParamExtractorDstu3Test { throw new UnsupportedOperationException(); } - @Override - public void refreshCacheIfNecessary() { - // nothing - } - @Override public void requestRefresh() { // nothing @@ -87,11 +81,6 @@ public class SearchParamExtractorDstu3Test { public Collection<RuntimeSearchParam> getSearchParamsByResourceType(RuntimeResourceDefinition theResourceDef) { return null; } - - @Override - public void setSearchParamProviderForUnitTest(ISearchParamProvider theSearchParamProvider) { - // nothing - } }; SearchParamExtractorDstu3 extractor = new SearchParamExtractorDstu3(new ModelConfig(), ourCtx, ourValidationSupport, searchParamRegistry); diff --git a/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/retry/RetrierTest.java b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/retry/RetrierTest.java new file mode 100644 index 00000000000..60154a2d02a --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/test/java/ca/uhn/fhir/jpa/searchparam/retry/RetrierTest.java @@ -0,0 +1,87 @@ +package ca.uhn.fhir.jpa.searchparam.retry; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +public class RetrierTest { + @Test + public void happyPath() { + Supplier<Boolean> supplier = () -> true; + Retrier<Boolean> retrier = new Retrier<>(supplier, 5, 0, "test"); + assertTrue(retrier.runWithRetry()); + } + + @Test + public void succeedBeforeMaxRetries() { + AtomicInteger counter = new AtomicInteger(); + Supplier<Boolean> supplier = () -> { + if (counter.incrementAndGet() < 3) throw new RetryRuntimeException("test"); + return true; + }; + Retrier<Boolean> retrier = new Retrier<>(supplier, 5, 0, "test"); + assertTrue(retrier.runWithRetry()); + assertEquals(3, counter.get()); + } + + @Test + public void failMaxRetries() { + AtomicInteger counter = new AtomicInteger(); + Supplier<Boolean> supplier = () -> { + if (counter.incrementAndGet() < 10) throw new RetryRuntimeException("test"); + return true; + }; + Retrier<Boolean> retrier = new Retrier<>(supplier, 5, 0, "test"); + try { + retrier.runWithRetry(); + fail(); + } catch (RetryRuntimeException e) { + assertEquals(5, counter.get()); + } + } + + @Test + public void failMaxRetriesZero() { + AtomicInteger counter = new AtomicInteger(); + Supplier<Boolean> supplier = () -> { + if (counter.incrementAndGet() < 10) throw new RetryRuntimeException("test"); + return true; + }; + Retrier<Boolean> retrier = new Retrier<>(supplier, 0, 0, "test"); + try { + retrier.runWithRetry(); + fail(); + } catch (IllegalStateException e) { + assertEquals(0, counter.get()); + assertEquals("maxRetries must be above zero." ,e.getMessage()); + } + } + + @Test + public void failMaxRetriesNegative() { + AtomicInteger counter = new AtomicInteger(); + Supplier<Boolean> supplier = () -> { + if (counter.incrementAndGet() < 10) throw new RetryRuntimeException("test"); + return true; + }; + Retrier<Boolean> retrier = new Retrier<>(supplier, -1, 0, "test"); + try { + retrier.runWithRetry(); + fail(); + } catch (IllegalStateException e) { + assertEquals(0, counter.get()); + assertEquals("maxRetries must be above zero." ,e.getMessage()); + } + } + + + + class RetryRuntimeException extends RuntimeException { + RetryRuntimeException(String message) { + super(message); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java index ec848cfd0f3..86e45ff8829 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java @@ -200,6 +200,7 @@ public class CanonicalSubscription implements Serializable { b.append(myTrigger, that.myTrigger); b.append(myEmailDetails, that.myEmailDetails); b.append(myRestHookDetails, that.myRestHookDetails); + b.append(myChannelExtensions, that.myChannelExtensions); return b.isEquals(); } @@ -216,6 +217,7 @@ public class CanonicalSubscription implements Serializable { .append(myTrigger) .append(myEmailDetails) .append(myRestHookDetails) + .append(myChannelExtensions) .toHashCode(); } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java index 5fa11049fa9..b82b9254137 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionLoader.java @@ -21,10 +21,12 @@ package ca.uhn.fhir.jpa.subscription.module.cache; */ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.searchparam.retry.Retrier; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Subscription; import org.slf4j.Logger; @@ -34,7 +36,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import javax.annotation.PostConstruct; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -45,46 +46,52 @@ import java.util.concurrent.Semaphore; @Lazy public class SubscriptionLoader { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionLoader.class); + @VisibleForTesting + public static final int INITIAL_SECONDS_BETWEEN_RETRIES = 5; + private static final int MAX_RETRIES = 60; // 60 * 5 seconds = 5 minutes @Autowired private ISubscriptionProvider mySubscriptionProvidor; @Autowired private SubscriptionRegistry mySubscriptionRegistry; - private final Object myInitSubscriptionsLock = new Object(); - private Semaphore myInitSubscriptionsSemaphore = new Semaphore(1); + private final Object mySyncSubscriptionsLock = new Object(); + private Semaphore mySyncSubscriptionsSemaphore = new Semaphore(1); - @PostConstruct - public void start() { - initSubscriptions(); - } + private volatile int mySecondsBetweenRetries = INITIAL_SECONDS_BETWEEN_RETRIES; /** * Read the existing subscriptions from the database */ @SuppressWarnings("unused") - @Scheduled(fixedDelay = 60000) - public void initSubscriptions() { - if (!myInitSubscriptionsSemaphore.tryAcquire()) { + @Scheduled(fixedDelay = DateUtils.MILLIS_PER_MINUTE) + public void syncSubscriptions() { + if (!mySyncSubscriptionsSemaphore.tryAcquire()) { return; } try { - doInitSubscriptions(); + doSyncSubscriptionsWithRetry(); } finally { - myInitSubscriptionsSemaphore.release(); + mySyncSubscriptionsSemaphore.release(); } } @VisibleForTesting - public int doInitSubscriptionsForUnitTest() { - return doInitSubscriptions(); + public int doSyncSubscriptionsForUnitTest() { + return doSyncSubscriptionsWithRetry(); } - private int doInitSubscriptions() { - synchronized (myInitSubscriptionsLock) { - ourLog.debug("Starting init subscriptions"); + synchronized int doSyncSubscriptionsWithRetry() { + Retrier<Integer> syncSubscriptionRetrier = new Retrier(() -> doSyncSubscriptions(), MAX_RETRIES, mySecondsBetweenRetries, "sync subscriptions"); + return syncSubscriptionRetrier.runWithRetry(); + } + + private int doSyncSubscriptions() { + synchronized (mySyncSubscriptionsLock) { + ourLog.debug("Starting sync subscriptions"); SearchParameterMap map = new SearchParameterMap(); map.add(Subscription.SP_STATUS, new TokenOrListParam() + // TODO KHS perhaps we should only be requesting ACTIVE subscriptions here?... .addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode())) .addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode()))); map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS); @@ -109,7 +116,7 @@ public class SubscriptionLoader { } mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds); - ourLog.trace("Finished init subscriptions - found {}", resourceList.size()); + ourLog.debug("Finished sync subscriptions - found {}", resourceList.size()); return changesCount; } @@ -119,5 +126,10 @@ public class SubscriptionLoader { public void setSubscriptionProviderForUnitTest(ISubscriptionProvider theSubscriptionProvider) { mySubscriptionProvidor = theSubscriptionProvider; } + + @VisibleForTesting + public void setSecondsBetweenRetriesForTesting(int theSecondsBetweenRetries) { + mySecondsBetweenRetries = theSecondsBetweenRetries; + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index 8c55ad2d924..6d4fc7e0a35 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -82,10 +82,10 @@ public class SubscriptionRegistry { Optional<MessageHandler> deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(canonicalized); ActiveSubscription activeSubscription = new ActiveSubscription(canonicalized, deliveryChannel); - myActiveSubscriptionCache.put(subscriptionId, activeSubscription); - deliveryHandler.ifPresent(handler -> activeSubscription.register(handler)); + myActiveSubscriptionCache.put(subscriptionId, activeSubscription); + return canonicalized; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java index c0a07f871ad..b484886ab18 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java @@ -26,8 +26,10 @@ import ca.uhn.fhir.jpa.subscription.module.cache.LinkedBlockingQueueSubscribable import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; @Configuration +@EnableScheduling @ComponentScan(basePackages = {"ca.uhn.fhir.jpa.searchparam", "ca.uhn.fhir.jpa.subscription.module"}) public abstract class BaseSubscriptionConfig { public abstract FhirContext fhirContext(); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java index 3a527eefe73..6e1ae21ac67 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/standalone/FhirClientSearchParamProvider.java @@ -62,7 +62,7 @@ public class FhirClientSearchParamProvider implements ISearchParamProvider { } @Override - public <SP extends IBaseResource> void refreshCache(BaseSearchParamRegistry<SP> theSearchParamRegistry, long theRefreshInterval) { - theSearchParamRegistry.doRefresh(theRefreshInterval); + public <SP extends IBaseResource> int refreshCache(BaseSearchParamRegistry<SP> theSearchParamRegistry, long theRefreshInterval) { + return theSearchParamRegistry.doRefresh(theRefreshInterval); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java index e81033463ca..1d2dac2d498 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java @@ -1,8 +1,6 @@ package ca.uhn.fhir.jpa.subscription.module; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; -import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader; import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSearchParamProvider; import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSubscriptionProvider; @@ -15,25 +13,25 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; public abstract class BaseSubscriptionTest { @Autowired - ISearchParamProvider mySearchParamProvider; + MockFhirClientSubscriptionProvider myMockFhirClientSubscriptionProvider; @Autowired ISearchParamRegistry mySearchParamRegistry; @Autowired - ISubscriptionProvider mySubscriptionProvider; + MockFhirClientSearchParamProvider myMockFhirClientSearchParamProvider; @Autowired SubscriptionLoader mySubscriptionLoader; public void initSearchParamRegistry(IBundleProvider theBundleProvider) { - ((MockFhirClientSearchParamProvider)mySearchParamProvider).setBundleProvider(theBundleProvider); + myMockFhirClientSearchParamProvider.setBundleProvider(theBundleProvider); mySearchParamRegistry.forceRefresh(); } public void initSubscriptionLoader(IBundleProvider theBundleProvider) { - ((MockFhirClientSubscriptionProvider)mySubscriptionProvider).setBundleProvider(theBundleProvider); - mySubscriptionLoader.doInitSubscriptionsForUnitTest(); + myMockFhirClientSubscriptionProvider.setBundleProvider(theBundleProvider); + mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java index cbc47ae2e23..14e0f8834d6 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSearchParamProvider.java @@ -3,21 +3,18 @@ package ca.uhn.fhir.jpa.subscription.module.config; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSearchParamProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.server.SimpleBundleProvider; public class MockFhirClientSearchParamProvider extends FhirClientSearchParamProvider { - private IBundleProvider myBundleProvider = new SimpleBundleProvider(); + private final MockProvider myMockProvider = new MockProvider(); public MockFhirClientSearchParamProvider() { super(null); } - public void setBundleProvider(IBundleProvider theBundleProvider) { - myBundleProvider = theBundleProvider; - } + public void setBundleProvider(IBundleProvider theBundleProvider) { myMockProvider.setBundleProvider(theBundleProvider); } + public void setFailCount(int theFailCount) { myMockProvider.setFailCount(theFailCount); } + public int getFailCount() { return myMockProvider.getFailCount(); } @Override - public IBundleProvider search(SearchParameterMap theParams) { - return myBundleProvider; - } + public IBundleProvider search(SearchParameterMap theParams) { return myMockProvider.search(theParams); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java index efe5326da84..fba2470251d 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockFhirClientSubscriptionProvider.java @@ -3,22 +3,18 @@ package ca.uhn.fhir.jpa.subscription.module.config; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.module.standalone.FhirClientSubscriptionProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.server.SimpleBundleProvider; public class MockFhirClientSubscriptionProvider extends FhirClientSubscriptionProvider { - private IBundleProvider myBundleProvider = new SimpleBundleProvider(); - + private final MockProvider myMockProvider = new MockProvider(); public MockFhirClientSubscriptionProvider() { super(null); } - public void setBundleProvider(IBundleProvider theBundleProvider) { - myBundleProvider = theBundleProvider; - } + public void setBundleProvider(IBundleProvider theBundleProvider) { myMockProvider.setBundleProvider(theBundleProvider); } + public void setFailCount(int theFailCount) { myMockProvider.setFailCount(theFailCount); } + public int getFailCount() { return myMockProvider.getFailCount(); } @Override - public IBundleProvider search(SearchParameterMap theParams) { - return myBundleProvider; - } + public IBundleProvider search(SearchParameterMap theParams) { return myMockProvider.search(theParams); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockProvider.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockProvider.java new file mode 100644 index 00000000000..2ce9230b9f1 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/MockProvider.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.jpa.subscription.module.config; + +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; + +public class MockProvider { + private IBundleProvider myBundleProvider = new SimpleBundleProvider(); + private int myFailCount = 0; + + public void setBundleProvider(IBundleProvider theBundleProvider) { + myBundleProvider = theBundleProvider; + } + + public IBundleProvider search(SearchParameterMap theParams) { + if (myFailCount > 0) { + --myFailCount; + throw new RuntimeException("Mock Search Failed"); + } + return myBundleProvider; + } + + public void setFailCount(int theFailCount) { + myFailCount = theFailCount; + } + + public int getFailCount() { + return myFailCount; + } + +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java index 2c3a51bec1d..d63ce2861ea 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionConfig.java @@ -6,15 +6,16 @@ import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.util.PortUtil; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.context.TestPropertySource; @Configuration +@TestPropertySource(properties = { + "scheduling_disabled=true" +}) public class TestSubscriptionConfig { - - @Autowired - FhirContext myFhirContext; private static int ourPort; private static String ourServerBase; @@ -24,15 +25,20 @@ public class TestSubscriptionConfig { } @Bean - public IGenericClient fhirClient() { + public IGenericClient fhirClient(FhirContext theFhirContext) { ourPort = PortUtil.findFreePort(); ourServerBase = "http://localhost:" + ourPort + "/fhir/context"; - return myFhirContext.newRestfulGenericClient(ourServerBase); + return theFhirContext.newRestfulGenericClient(ourServerBase); }; @Bean public ISubscriptionMatcher inMemorySubscriptionMatcher() { return new InMemorySubscriptionMatcher(); } + + @Bean + public UnregisterScheduledProcessor unregisterScheduledProcessor(Environment theEnv) { + return new UnregisterScheduledProcessor(theEnv); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/UnregisterScheduledProcessor.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/UnregisterScheduledProcessor.java new file mode 100644 index 00000000000..9e2f4b4ca76 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/UnregisterScheduledProcessor.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.jpa.subscription.module.config; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ExecutorConfigurationSupport; + +/** + * This bean postprocessor disables all scheduled tasks. It is intended + * only to be used in unit tests in circumstances where scheduled + * tasks cause issues. + */ +// TODO KHS duplicated from jpaserver-base +public class UnregisterScheduledProcessor implements BeanFactoryPostProcessor { + + private final Environment myEnvironment; + + public UnregisterScheduledProcessor(Environment theEnv) { + myEnvironment = theEnv; + } + + @Override + public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException { + String schedulingDisabled = myEnvironment.getProperty("scheduling_disabled"); + if ("true".equals(schedulingDisabled)) { + for (String beanName : beanFactory.getBeanNamesForType(ScheduledAnnotationBeanPostProcessor.class)) { + ((DefaultListableBeanFactory) beanFactory).removeBeanDefinition(beanName); + } + + for (String beanName : beanFactory.getBeanNamesForType(ExecutorConfigurationSupport.class)) { + ExecutorConfigurationSupport executorConfigSupport = ((DefaultListableBeanFactory) beanFactory).getBean(beanName, ExecutorConfigurationSupport.class); + executorConfigSupport.shutdown(); + } + } + + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index c7eb9d8cef4..8b3186ea951 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -21,7 +21,6 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -45,6 +44,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base @Autowired SubscriptionChannelFactory mySubscriptionChannelFactory; + protected String myCode = "1000000050"; + private static int ourListenerPort; private static RestfulServer ourListenerRestServer; private static Server ourListenerServer; @@ -56,11 +57,6 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); private long idCounter = 0; - @After - public void afterUnregisterRestHookListener() { - mySubscriptionIds.clear(); - } - @Before public void beforeReset() { ourCreatedObservations.clear(); @@ -169,7 +165,5 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base ourLog.info("Received Listener Update (now have {} updates)", ourUpdatedObservations.size()); return new MethodOutcome(new IdType("Observation/1"), false); } - } - } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SearchParamLoaderTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SearchParamLoaderTest.java new file mode 100644 index 00000000000..753fb291302 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SearchParamLoaderTest.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry; +import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSearchParamProvider; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; +import org.hl7.fhir.dstu3.model.Enumerations; +import org.hl7.fhir.dstu3.model.SearchParameter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; + +public class SearchParamLoaderTest extends BaseBlockingQueueSubscribableChannelDstu3Test { + private static final int MOCK_FHIR_CLIENT_FAILURES = 5; + @Autowired + private MockFhirClientSearchParamProvider myMockFhirClientSearchParamProvider; + @Autowired + private BaseSearchParamRegistry mySearchParamRegistry; + + @Before + public void setFailCount() { + myMockFhirClientSearchParamProvider.setFailCount(MOCK_FHIR_CLIENT_FAILURES); + } + + @After + public void restoreFailCount() { + myMockFhirClientSearchParamProvider.setFailCount(0); + } + + @Before + public void zeroRetryDelay() { + mySearchParamRegistry.setSecondsBetweenRetriesForTesting(0); + } + + @After + public void restoreRetryDelay() { + mySearchParamRegistry.setSecondsBetweenRetriesForTesting(mySearchParamRegistry.INITIAL_SECONDS_BETWEEN_RETRIES); + } + + @Test + public void testSubscriptionLoaderFhirClientDown() throws Exception { + String criteria = "BodySite?accessType=Catheter,PD%20Catheter"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("BodySite"); + sp.setCode("accessType"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("BodySite.extension('BodySite#accessType')"); + sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + + IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + initSearchParamRegistry(bundle); + assertEquals(0, myMockFhirClientSearchParamProvider.getFailCount()); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java index ce667d0eb51..b6c2ae73037 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java @@ -4,7 +4,6 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import org.hl7.fhir.dstu3.model.Subscription; -import org.junit.Before; import org.junit.Test; import java.util.ArrayList; @@ -13,8 +12,6 @@ import java.util.List; import static org.junit.Assert.assertEquals; public class SubscriptionLoaderFhirClientTest extends BaseBlockingQueueSubscribableChannelDstu3Test { - private String myCode = "1000000050"; - @Test public void testSubscriptionLoaderFhirClient() throws Exception { String payload = "application/fhir+json"; diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderTest.java new file mode 100644 index 00000000000..ff675029fe0 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderTest.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.jpa.subscription.module.standalone; + +import ca.uhn.fhir.jpa.searchparam.registry.BaseSearchParamRegistry; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader; +import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSubscriptionProvider; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.server.SimpleBundleProvider; +import org.hl7.fhir.dstu3.model.Subscription; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class SubscriptionLoaderTest extends BaseBlockingQueueSubscribableChannelDstu3Test { + private static final int MOCK_FHIR_CLIENT_FAILURES = 5; + @Autowired + private MockFhirClientSubscriptionProvider myMockFhirClientSubscriptionProvider; + @Autowired + private SubscriptionLoader mySubscriptionLoader; + + @Before + public void setFailCount() { + myMockFhirClientSubscriptionProvider.setFailCount(MOCK_FHIR_CLIENT_FAILURES); + } + + @After + public void restoreFailCount() { + myMockFhirClientSubscriptionProvider.setFailCount(0); + } + + @Before + public void zeroRetryDelay() { + mySubscriptionLoader.setSecondsBetweenRetriesForTesting(0); + } + + @After + public void restoreRetryDelay() { + mySubscriptionLoader.setSecondsBetweenRetriesForTesting(BaseSearchParamRegistry.INITIAL_SECONDS_BETWEEN_RETRIES); + } + + @Test + public void testSubscriptionLoaderFhirClientDown() throws Exception { + String payload = "application/fhir+json"; + + String criteria1 = "Observation?code=SNOMED-CT|" + myCode + "&_format=xml"; + String criteria2 = "Observation?code=SNOMED-CT|" + myCode + "111&_format=xml"; + + List<Subscription> subs = new ArrayList<>(); + subs.add(returnedActiveSubscription(criteria1, payload, ourListenerServerBase)); + subs.add(returnedActiveSubscription(criteria2, payload, ourListenerServerBase)); + + IBundleProvider bundle = new SimpleBundleProvider(new ArrayList<>(subs), "uuid"); + initSubscriptionLoader(bundle); + assertEquals(0, myMockFhirClientSubscriptionProvider.getFailCount()); + } +} From b8755615b2cb30448d1f82d6b8f187ac0c457799 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Mon, 14 Jan 2019 13:51:52 -0600 Subject: [PATCH 16/56] Allow AuthorizationInterceptor to read patients if the user has read access to the individual instance --- .../auth/IAuthRuleBuilderRuleOp.java | 33 +++-- .../server/interceptor/auth/RuleBuilder.java | 125 +++++++++++------- .../server/interceptor/auth/RuleImplOp.java | 41 ++++-- .../interceptor/auth/RuleBuilderTest.java | 39 +++++- .../AuthorizationInterceptorDstu3Test.java | 94 +++++++------ src/changes/changes.xml | 4 + 6 files changed, 224 insertions(+), 112 deletions(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java index ce1b1702fdf..70aae37c005 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java @@ -13,9 +13,9 @@ import java.util.Collection; * 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. @@ -32,14 +32,14 @@ public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthR * See the following examples which show how theId is interpreted: * </p> * <ul> - * <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li> + * <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li> * <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li> * <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li> * </ul> - * + * * @param theId The ID of the resource to apply (e.g. <code>Patient/123</code>) * @throws IllegalArgumentException If theId does not contain an ID with at least an ID part - * @throws NullPointerException If theId is null + * @throws NullPointerException If theId is null */ IAuthRuleFinished instance(String theId); @@ -49,16 +49,31 @@ public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthR * See the following examples which show how theId is interpreted: * </p> * <ul> - * <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li> + * <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li> * <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li> * <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li> * </ul> - * + * * @param theId The ID of the resource to apply (e.g. <code>Patient/123</code>) * @throws IllegalArgumentException If theId does not contain an ID with at least an ID part - * @throws NullPointerException If theId is null + * @throws NullPointerException If theId is null */ IAuthRuleFinished instance(IIdType theId); - IAuthRuleFinished instances(Collection<IIdType> theInstances); + /** + * Rule applies to the resource with the given ID (e.g. <code>Patient/123</code>) + * <p> + * See the following examples which show how theId is interpreted: + * </p> + * <ul> + * <li><b><code>http://example.com/Patient/123</code></b> - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)</li> + * <li><b><code>Patient/123</code></b> - Any Patient resource with the ID "123" will be matched</li> + * <li><b><code>123</code></b> - Any resource of any type with the ID "123" will be matched</li> + * </ul> + * + * @param theIds The IDs of the resource to apply (e.g. <code>Patient/123</code>) + * @throws IllegalArgumentException If theId does not contain an ID with at least an ID part + * @throws NullPointerException If theId is null + */ + IAuthRuleFinished instances(Collection<IIdType> theIds); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 0557989e55f..1d205c0613c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * 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. @@ -23,6 +23,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; +import com.google.common.collect.Lists; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -35,6 +36,8 @@ public class RuleBuilder implements IAuthRuleBuilder { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private ArrayList<IAuthRule> myRules; + private IAuthRuleBuilderRule myAllow; + private IAuthRuleBuilderRule myDeny; public RuleBuilder() { myRules = new ArrayList<>(); @@ -42,7 +45,10 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRule allow() { - return allow(null); + if (myAllow == null) { + myAllow = allow(null); + } + return myAllow; } @Override @@ -69,7 +75,10 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRule deny() { - return deny(null); + if (myDeny == null) { + myDeny = deny(null); + } + return myDeny; } @Override @@ -153,7 +162,7 @@ public class RuleBuilder implements IAuthRuleBuilder { private void setTenantApplicabilityChecker(ITenantApplicabilityChecker theTenantApplicabilityChecker) { myTenantApplicabilityChecker = theTenantApplicabilityChecker; - myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker); + myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker); } @Override @@ -172,6 +181,8 @@ public class RuleBuilder implements IAuthRuleBuilder { private PolicyEnum myRuleMode; private String myRuleName; + private RuleBuilderRuleOp myReadRuleBuilder; + private RuleBuilderRuleOp myWriteRuleBuilder; RuleBuilderRule(PolicyEnum theRuleMode, String theRuleName) { myRuleMode = theRuleMode; @@ -214,7 +225,10 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOp read() { - return new RuleBuilderRuleOp(RuleOpEnum.READ); + if (myReadRuleBuilder == null) { + myReadRuleBuilder = new RuleBuilderRuleOp(RuleOpEnum.READ); + } + return myReadRuleBuilder; } @Override @@ -229,7 +243,10 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOp write() { - return new RuleBuilderRuleOp(RuleOpEnum.WRITE); + if (myWriteRuleBuilder == null) { + myWriteRuleBuilder = new RuleBuilderRuleOp(RuleOpEnum.WRITE); + } + return myWriteRuleBuilder; } @Override @@ -286,6 +303,7 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleOp implements IAuthRuleBuilderRuleOp { private final RuleOpEnum myRuleOp; + private RuleBuilderRuleOpClassifier myInstancesBuilder; public RuleBuilderRuleOp(RuleOpEnum theRuleOp) { myRuleOp = theRuleOp; @@ -308,16 +326,22 @@ public class RuleBuilder implements IAuthRuleBuilder { Validate.notBlank(theId.getValue(), "theId.getValue() must not be null or empty"); Validate.notBlank(theId.getIdPart(), "theId must contain an ID part"); - List<IIdType> instances = Collections.singletonList(theId); + List<IIdType> instances = Lists.newArrayList(theId); return instances(instances); } @Override - public IAuthRuleFinished instances(Collection<IIdType> theInstances) { + public RuleBuilderFinished instances(Collection<IIdType> theInstances) { Validate.notNull(theInstances, "theInstances must not be null"); Validate.notEmpty(theInstances, "theInstances must not be empty"); - return new RuleBuilderRuleOpClassifier(theInstances).finished(); + if (myInstancesBuilder == null) { + RuleBuilderRuleOpClassifier instancesBuilder = new RuleBuilderRuleOpClassifier(theInstances); + myInstancesBuilder = instancesBuilder; + return instancesBuilder.finished(); + } else { + return myInstancesBuilder.addInstances(theInstances); + } } @Override @@ -328,12 +352,13 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleOpClassifier implements IAuthRuleBuilderRuleOpClassifier { + private final AppliesTypeEnum myAppliesTo; + private final Set<?> myAppliesToTypes; private ClassifierTypeEnum myClassifierType; private String myInCompartmentName; private Collection<? extends IIdType> myInCompartmentOwners; private Collection<IIdType> myAppliesToInstances; - private final AppliesTypeEnum myAppliesTo; - private final Set<?> myAppliesToTypes; + private RuleImplOp myRule; /** * Constructor @@ -341,7 +366,7 @@ public class RuleBuilder implements IAuthRuleBuilder { RuleBuilderRuleOpClassifier(AppliesTypeEnum theAppliesTo, Set<Class<? extends IBaseResource>> theAppliesToTypes) { super(); myAppliesTo = theAppliesTo; - myAppliesToTypes=theAppliesToTypes; + myAppliesToTypes = theAppliesToTypes; } /** @@ -353,20 +378,20 @@ public class RuleBuilder implements IAuthRuleBuilder { myAppliesToTypes = null; } - private IAuthRuleBuilderRuleOpClassifierFinished finished() { + private RuleBuilderFinished finished() { + Validate.isTrue(myRule == null, "Can not call finished() twice"); + myRule = new RuleImplOp(myRuleName); + myRule.setMode(myRuleMode); + myRule.setOp(myRuleOp); + myRule.setAppliesTo(myAppliesTo); + myRule.setAppliesToTypes(myAppliesToTypes); + myRule.setAppliesToInstances(myAppliesToInstances); + myRule.setClassifierType(myClassifierType); + myRule.setClassifierCompartmentName(myInCompartmentName); + myRule.setClassifierCompartmentOwners(myInCompartmentOwners); + myRules.add(myRule); - RuleImplOp rule = new RuleImplOp(myRuleName); - rule.setMode(myRuleMode); - rule.setOp(myRuleOp); - rule.setAppliesTo(myAppliesTo); - rule.setAppliesToTypes(myAppliesToTypes); - rule.setAppliesToInstances(myAppliesToInstances); - rule.setClassifierType(myClassifierType); - rule.setClassifierCompartmentName(myInCompartmentName); - rule.setClassifierCompartmentOwners(myInCompartmentOwners); - myRules.add(rule); - - return new RuleBuilderFinished(rule); + return new RuleBuilderFinished(myRule); } @Override @@ -405,6 +430,10 @@ public class RuleBuilder implements IAuthRuleBuilder { return finished(); } + RuleBuilderFinished addInstances(Collection<IIdType> theInstances) { + myAppliesToInstances.addAll(theInstances); + return new RuleBuilderFinished(myRule); + } } } @@ -424,28 +453,6 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed { - private class RuleBuilderOperationNamedAndScoped implements IAuthRuleBuilderOperationNamedAndScoped { - - private final OperationRule myRule; - - public RuleBuilderOperationNamedAndScoped(OperationRule theRule) { - myRule = theRule; - } - - @Override - public IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses() { - myRule.allowAllResponses(); - myRules.add(myRule); - return new RuleBuilderFinished(myRule); - } - - @Override - public IAuthRuleBuilderRuleOpClassifierFinished andRequireExplicitResponseAuthorization() { - myRules.add(myRule); - return new RuleBuilderFinished(myRule); - } - } - private String myOperationName; RuleBuilderRuleOperationNamed(String theOperationName) { @@ -532,6 +539,28 @@ public class RuleBuilder implements IAuthRuleBuilder { Validate.notNull(theType, "theType must not be null"); } + private class RuleBuilderOperationNamedAndScoped implements IAuthRuleBuilderOperationNamedAndScoped { + + private final OperationRule myRule; + + public RuleBuilderOperationNamedAndScoped(OperationRule theRule) { + myRule = theRule; + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished andAllowAllResponses() { + myRule.allowAllResponses(); + myRules.add(myRule); + return new RuleBuilderFinished(myRule); + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished andRequireExplicitResponseAuthorization() { + myRules.add(myRule); + return new RuleBuilderFinished(myRule); + } + } + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index c818140ba3d..d91ec3bdc80 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -3,15 +3,18 @@ package ca.uhn.fhir.rest.server.interceptor.auth; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.rest.api.QualifiedParamList; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict; import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.BundleUtil.BundleEntryParts; import ca.uhn.fhir.util.FhirTerser; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -34,9 +37,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -64,6 +67,15 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { super(theRuleName); } + @VisibleForTesting + Collection<IIdType> getAppliesToInstances() { + return myAppliesToInstances; + } + + public void setAppliesToInstances(Collection<IIdType> theAppliesToInstances) { + myAppliesToInstances = theAppliesToInstances; + } + @Override public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) { @@ -111,14 +123,18 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { if (theRequestDetails.getParameters().containsKey("_id")) { String[] idValues = theRequestDetails.getParameters().get("_id"); appliesToResourceId = new ArrayList<>(); - for (String next : idValues) { - IIdType nextId = ctx.getVersion().newIdType().setValue(next); - if (nextId.hasIdPart()){ - if (!nextId.hasResourceType()) { - nextId = nextId.withResourceType(appliesToResourceType); - } - if (nextId.getResourceType().equals(appliesToResourceType)) { - appliesToResourceId.add(nextId); + + for (String nextIdValue : idValues) { + QualifiedParamList orParamList = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextIdValue); + for (String next : orParamList) { + IIdType nextId = ctx.getVersion().newIdType().setValue(next); + if (nextId.hasIdPart()) { + if (!nextId.hasResourceType()) { + nextId = nextId.withResourceType(appliesToResourceType); + } + if (nextId.getResourceType().equals(appliesToResourceType)) { + appliesToResourceId.add(nextId); + } } } } @@ -340,6 +356,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { return newVerdict(); } } + return null; case ALL_RESOURCES: if (appliesToResourceType != null) { @@ -531,10 +548,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { myAppliesTo = theAppliesTo; } - public void setAppliesToInstances(Collection<IIdType> theAppliesToInstances) { - myAppliesToInstances = theAppliesToInstances; - } - public void setAppliesToTypes(Set<?> theAppliesToTypes) { myAppliesToTypes = theAppliesToTypes; } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java index 70f9afabd61..243d7704d43 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java @@ -1,14 +1,51 @@ package ca.uhn.fhir.rest.server.interceptor.auth; +import ca.uhn.fhir.model.primitive.IdDt; +import com.google.common.collect.Lists; import org.junit.Test; -import static org.junit.Assert.*; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class RuleBuilderTest { + /** + * If the user creates multiple rules that allow read/write of individual + * instances, we will collapse these into a single rule for performance + */ @Test public void testCollapseReadInstancesIntoSingleRule() { + RuleBuilder builder = new RuleBuilder(); + builder.allow().read().instance(new IdDt("Patient/READ-1")); + builder.allow().write().instance(new IdDt("Patient/WRITE-1")); + builder.allow().read().instance(new IdDt("Patient/READ-2")); + builder.allow().write().instance(new IdDt("Patient/WRITE-2")); + builder.allow().read().instances(Lists.newArrayList(new IdDt("Patient/READ-3"), new IdDt("Patient/READ-4"))); + builder.allow().write().instances(Lists.newArrayList(new IdDt("Patient/WRITE-3"), new IdDt("Patient/WRITE-4"))); + List<IAuthRule> list = builder.build(); + assertEquals(2, list.size()); + + assertEquals(RuleImplOp.class, list.get(0).getClass()); + RuleImplOp allowRead = (RuleImplOp) list.get(0); + assertThat(allowRead.getAppliesToInstances(), contains( + new IdDt("Patient/READ-1"), + new IdDt("Patient/READ-2"), + new IdDt("Patient/READ-3"), + new IdDt("Patient/READ-4") + )); + + assertEquals(RuleImplOp.class, list.get(1).getClass()); + RuleImplOp allowWrite = (RuleImplOp) list.get(1); + assertThat(allowWrite.getAppliesToInstances(), contains( + new IdDt("Patient/WRITE-1"), + new IdDt("Patient/WRITE-2"), + new IdDt("Patient/WRITE-3"), + new IdDt("Patient/WRITE-4") + )); } } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java index 5fb6b005d8b..59c18cebf32 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/interceptor/AuthorizationInterceptorDstu3Test.java @@ -2507,10 +2507,10 @@ public class AuthorizationInterceptorDstu3Test { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @Override public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) { - return new RuleBuilder() - .allow("Rule 1").read().instance("Patient/900").andThen() - .allow("Rule 1").read().instance("Patient/700").andThen() - .build(); + RuleBuilder ruleBuilder = new RuleBuilder(); + ruleBuilder.allow().read().instance("Patient/900").andThen(); + ruleBuilder.allow().read().instance("Patient/700").andThen(); + return ruleBuilder.build(); } }); @@ -2519,51 +2519,65 @@ public class AuthorizationInterceptorDstu3Test { HttpGet httpGet; ourReturn = Collections.singletonList(createPatient(900)); +// ourHitMethod = false; +// httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=900"); +// status = ourClient.execute(httpGet); +// extractResponseAndClose(status); +// assertEquals(200, status.getStatusLine().getStatusCode()); +// assertTrue(ourHitMethod); +// +// ourHitMethod = false; +// httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/900"); +// status = ourClient.execute(httpGet); +// extractResponseAndClose(status); +// assertEquals(200, status.getStatusLine().getStatusCode()); +// assertTrue(ourHitMethod); +// +// ourHitMethod = false; +// httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=901"); +// status = ourClient.execute(httpGet); +// response = extractResponseAndClose(status); +// assertEquals(403, status.getStatusLine().getStatusCode()); +// assertEquals(ERR403, response); +// assertFalse(ourHitMethod); +// +// ourHitMethod = false; +// httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/901"); +// status = ourClient.execute(httpGet); +// response = extractResponseAndClose(status); +// assertEquals(403, status.getStatusLine().getStatusCode()); +// assertEquals(ERR403, response); +// assertFalse(ourHitMethod); +// +// ourHitMethod = false; +// // technically this is invalid, but just in case.. +// httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_id=Patient/901"); +// status = ourClient.execute(httpGet); +// response = extractResponseAndClose(status); +// assertEquals(403, status.getStatusLine().getStatusCode()); +// assertEquals(ERR403, response); +// assertFalse(ourHitMethod); +// +// ourHitMethod = false; +// httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_id=901"); +// status = ourClient.execute(httpGet); +// response = extractResponseAndClose(status); +// assertEquals(403, status.getStatusLine().getStatusCode()); +// assertEquals(ERR403, response); +// assertFalse(ourHitMethod); + ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=900"); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/900,Patient/700"); status = ourClient.execute(httpGet); extractResponseAndClose(status); assertEquals(200, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/900"); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=900,777"); status = ourClient.execute(httpGet); extractResponseAndClose(status); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=901"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); assertEquals(403, status.getStatusLine().getStatusCode()); - assertEquals(ERR403, response); - assertFalse(ourHitMethod); - - ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=Patient/901"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertEquals(ERR403, response); - assertFalse(ourHitMethod); - - ourHitMethod = false; - // technically this is invalid, but just in case.. - httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_id=Patient/901"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertEquals(ERR403, response); - assertFalse(ourHitMethod); - - ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_id=901"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertEquals(ERR403, response); assertFalse(ourHitMethod); } @@ -3560,7 +3574,7 @@ public class AuthorizationInterceptorDstu3Test { } @Search() - public List<Resource> search(@OptionalParam(name = "_id") IdType theIdParam) { + public List<Resource> search(@OptionalParam(name = "_id") TokenAndListParam theIdParam) { ourHitMethod = true; return ourReturn; } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index db1c2e5c937..16a6c4a3417 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -299,6 +299,10 @@ A wrapper script for Maven has been added, enabling new users to use Maven without having to install it beforehand. Thanks to Ari Ruotsalainen for the Pull Request! </action> + <action type="add"> + AuthorizationInterceptor can now allow a user to perform a search that is scoped to a particular + resource (e.g. Patient?_id=123) if the user has read access for that specific instance. + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From a584e15251336b674c2f4b188d003cf94545b0b1 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Mon, 14 Jan 2019 14:58:27 -0500 Subject: [PATCH 17/56] Add headers --- .../jpa/term/BaseHapiTerminologySvcImpl.java | 4 ++-- .../registry/BaseSearchParamRegistry.java | 4 ++-- .../fhir/jpa/searchparam/retry/Retrier.java | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java index 5f24e9d61d1..a606b38c2a4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvcImpl.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term; * 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. diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java index 95826b555f4..8a2d06f59d8 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.searchparam.registry; * 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. diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java index be7bc738c23..cc11e2c8a43 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.searchparam.retry; +/*- + * #%L + * HAPI FHIR Search Parameters + * %% + * Copyright (C) 2014 - 2019 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 org.apache.commons.lang3.time.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From a2d01686714caaa0b3d2c1518f976a5a137d157e Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Tue, 15 Jan 2019 11:30:43 -0600 Subject: [PATCH 18/56] Fix handling of sequences in migrator tool --- .../migrate/taskdef/AddIdGeneratorTask.java | 9 ++++- .../migrate/taskdef/AddTableRawSqlTask.java | 12 +++++-- .../migrate/tasks/api/BaseMigrationTasks.java | 4 +++ .../migrate/taskdef/ArbitrarySqlTaskTest.java | 33 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTask.java index b498db1faeb..3930baa42b4 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddIdGeneratorTask.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import java.sql.SQLException; import java.util.Set; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -78,7 +79,13 @@ public class AddIdGeneratorTask extends BaseTask<AddIdGeneratorTask> { } if (isNotBlank(sql)) { - if (JdbcUtils.getSequenceNames(getConnectionProperties()).contains(myGeneratorName)) { + Set<String> sequenceNames = + JdbcUtils.getSequenceNames(getConnectionProperties()) + .stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + ourLog.debug("Currently have sequences: {}", sequenceNames); + if (sequenceNames.contains(myGeneratorName.toLowerCase())) { ourLog.info("Sequence {} already exists - No action performed", myGeneratorName); return; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java index 1da758bae16..b273992fae4 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/AddTableRawSqlTask.java @@ -35,6 +35,7 @@ public class AddTableRawSqlTask extends BaseTableTask<AddTableRawSqlTask> { private static final Logger ourLog = LoggerFactory.getLogger(AddTableRawSqlTask.class); private Map<DriverTypeEnum, List<String>> myDriverToSqls = new HashMap<>(); + private List<String> myDriverNeutralSqls = new ArrayList<>(); public void addSql(DriverTypeEnum theDriverType, @Language("SQL") String theSql) { Validate.notNull(theDriverType); @@ -52,9 +53,11 @@ public class AddTableRawSqlTask extends BaseTableTask<AddTableRawSqlTask> { return; } - List<String> sqlStatements = myDriverToSqls.get(getDriverType()); + List<String> sqlStatements = myDriverToSqls.computeIfAbsent(getDriverType(), t -> new ArrayList<>()); + sqlStatements.addAll(myDriverNeutralSqls); + ourLog.info("Going to create table {} using {} SQL statements", getTableName(), sqlStatements.size()); - getConnectionProperties().getTxTemplate().execute(t->{ + getConnectionProperties().getTxTemplate().execute(t -> { JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate(); for (String nextSql : sqlStatements) { @@ -65,4 +68,9 @@ public class AddTableRawSqlTask extends BaseTableTask<AddTableRawSqlTask> { }); } + + public void addSql(String theSql) { + Validate.notBlank("theSql must not be null", theSql); + myDriverNeutralSqls.add(theSql); + } } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java index abc4d262037..d041155d70f 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/api/BaseMigrationTasks.java @@ -329,6 +329,10 @@ public class BaseMigrationTasks<T extends Enum> { myTask.addSql(theDriverTypeEnum, theSql); return this; } + + public void addSql(@Language("SQL") String theSql) { + myTask.addSql(theSql); + } } public class BuilderAddTableByColumns implements IAcceptsTasks { diff --git a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java index 9dbce75c8c7..da5da3f13d5 100644 --- a/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java +++ b/hapi-fhir-jpaserver-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ArbitrarySqlTaskTest.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.jpa.migrate.taskdef; +import ca.uhn.fhir.jpa.migrate.tasks.api.BaseMigrationTasks; import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; +import ca.uhn.fhir.util.VersionEnum; import org.junit.Test; import java.util.List; @@ -66,4 +68,35 @@ public class ArbitrarySqlTaskTest extends BaseTest { getMigrator().migrate(); } + + private static class TestUpdateTasks extends BaseMigrationTasks<VersionEnum> { + + public TestUpdateTasks() { + Builder v = forVersion(VersionEnum.V3_5_0); + v + .addTableRawSql("A") + .addSql("delete from TEST_UPDATE_TASK where RES_TYPE = 'Patient'"); + } + + + } + + + @Test + public void testUpdateTask() { + executeSql("create table TEST_UPDATE_TASK (PID bigint not null, RES_TYPE varchar(255), PARAM_NAME varchar(255))"); + executeSql("insert into TEST_UPDATE_TASK (PID, RES_TYPE, PARAM_NAME) values (1, 'Patient', 'identifier')"); + + List<Map<String, Object>> rows = executeQuery("select * from TEST_UPDATE_TASK"); + assertEquals(1, rows.size()); + + TestUpdateTasks migrator = new TestUpdateTasks(); + getMigrator().addTasks(migrator.getTasks(VersionEnum.V3_3_0, VersionEnum.V3_6_0)); + getMigrator().migrate(); + + rows = executeQuery("select * from TEST_UPDATE_TASK"); + assertEquals(0, rows.size()); + + } + } From 19afcb7e098615bc89884b31a219ea8ddc9bd98f Mon Sep 17 00:00:00 2001 From: Ken Stevens <khstevens@gmail.com> Date: Tue, 15 Jan 2019 18:22:08 -0500 Subject: [PATCH 19/56] all tests pass --- .../ca/uhn/fhir/jpa/config/BaseConfig.java | 2 + .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 11 ++-- .../IResourceModifiedConsumer.java | 7 +++ .../SubscriptionInterceptorLoader.java | 23 +++----- .../SubscriptionMatcherInterceptor.java | 20 ++++--- .../SubscriptionTriggeringSvcImpl.java | 55 ++++++++----------- .../r4/BaseResourceProviderR4Test.java | 9 +-- .../fhir/jpa/model/entity/ModelConfig.java | 26 ++++++++- .../module/cache/SubscriptionRegistry.java | 22 ++++++-- .../autoconfigure/FhirAutoConfiguration.java | 4 -- 10 files changed, 102 insertions(+), 77 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java 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 5e6377848cb..472ba63e4f3 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 @@ -8,6 +8,8 @@ import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl; +import ca.uhn.fhir.jpa.subscription.ISubscriptionTriggeringSvc; +import ca.uhn.fhir.jpa.subscription.SubscriptionTriggeringSvcImpl; import ca.uhn.fhir.jpa.subscription.dbmatcher.CompositeInMemoryDaoSubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.dbmatcher.DaoSubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.cache.ISubscribableChannelFactory; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index a0e0766db0d..383df71a802 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -143,7 +143,6 @@ public class DaoConfig { private boolean myDisableHashBasedSearches; private boolean myEnableInMemorySubscriptionMatching = true; private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC; - private boolean mySubscriptionMatchingEnabled = true; /** * Constructor @@ -530,7 +529,7 @@ public class DaoConfig { * This may be used to optionally register server interceptors directly against the DAOs. */ public void setInterceptors(IServerInterceptor... theInterceptor) { - setInterceptors(new ArrayList<IServerInterceptor>()); + setInterceptors(new ArrayList<>()); if (theInterceptor != null && theInterceptor.length != 0) { getInterceptors().addAll(Arrays.asList(theInterceptor)); } @@ -1308,8 +1307,7 @@ public class DaoConfig { public void setSearchPreFetchThresholds(List<Integer> thePreFetchThresholds) { Validate.isTrue(thePreFetchThresholds.size() > 0, "thePreFetchThresholds must not be empty"); int last = 0; - for (Integer nextInteger : thePreFetchThresholds) { - int nextInt = nextInteger.intValue(); + for (Integer nextInt : thePreFetchThresholds) { Validate.isTrue(nextInt > 0 || nextInt == -1, nextInt + " is not a valid prefetch threshold"); Validate.isTrue(nextInt != last, "Prefetch thresholds must be sequential"); Validate.isTrue(nextInt > last || nextInt == -1, "Prefetch thresholds must be sequential"); @@ -1398,7 +1396,7 @@ public class DaoConfig { */ public boolean isSubscriptionMatchingEnabled() { - return mySubscriptionMatchingEnabled; + return myModelConfig.isSubscriptionMatchingEnabled(); } /** @@ -1407,9 +1405,8 @@ public class DaoConfig { * @since 3.7.0 */ - public void setSubscriptionMatchingEnabled(boolean theSubscriptionMatchingEnabled) { - mySubscriptionMatchingEnabled = theSubscriptionMatchingEnabled; + myModelConfig.setSubscriptionMatchingEnabled(theSubscriptionMatchingEnabled); } public ModelConfig getModelConfig() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java new file mode 100644 index 00000000000..0c2171c2eb3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.jpa.subscription; + +import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; + +public interface IResourceModifiedConsumer { + void submitResourceModified(ResourceModifiedMessage theMsg); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java index a39c3f30708..feaad036450 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java @@ -37,50 +37,41 @@ import java.util.Set; public class SubscriptionInterceptorLoader { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionInterceptorLoader.class); - // TODO KHS these beans are late loaded because we don't want to run their @PostConstruct and @Scheduled method if they're - // not required. Recommend removing @PostConstruct from these classes and instead call those methods in register interceptors below. - // @Schedule will be tricker to resolve - + @Autowired private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + @Autowired private SubscriptionActivatingInterceptor mySubscriptionActivatingInterceptor; - @Autowired DaoConfig myDaoConfig; @Autowired - private ApplicationContext myAppicationContext; - @Autowired private SubscriptionRegistry mySubscriptionRegistry; + @Autowired + private ApplicationContext myAppicationContext; public void registerInterceptors() { Set<Subscription.SubscriptionChannelType> supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes(); if (!supportedSubscriptionTypes.isEmpty()) { loadSubscriptions(); - if (mySubscriptionActivatingInterceptor == null) { - mySubscriptionActivatingInterceptor = myAppicationContext.getBean(SubscriptionActivatingInterceptor.class); - } ourLog.info("Registering subscription activating interceptor"); myDaoConfig.registerInterceptor(mySubscriptionActivatingInterceptor); } if (myDaoConfig.isSubscriptionMatchingEnabled()) { - if (mySubscriptionMatcherInterceptor == null) { - mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class); - } + mySubscriptionMatcherInterceptor.start(); ourLog.info("Registering subscription matcher interceptor"); myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); - } } private void loadSubscriptions() { ourLog.info("Loading subscriptions into the SubscriptionRegistry..."); - // Load subscriptions into the SubscriptionRegistry + // Activate scheduled subscription loads into the SubscriptionRegistry myAppicationContext.getBean(SubscriptionLoader.class); ourLog.info("...{} subscriptions loaded", mySubscriptionRegistry.size()); } @VisibleForTesting - public void unregisterInterceptorsForUnitTest() { + void unregisterInterceptorsForUnitTest() { myDaoConfig.unregisterInterceptor(mySubscriptionActivatingInterceptor); myDaoConfig.unregisterInterceptor(mySubscriptionMatcherInterceptor); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java index 26894df4a01..2d6a421c623 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscr import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,12 +43,12 @@ import javax.annotation.PreDestroy; @Component @Lazy -public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter { +public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter implements IResourceModifiedConsumer { private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class); - public static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching"; - public static final String SUBSCRIPTION_STATUS = "Subscription.status"; - public static final String SUBSCRIPTION_TYPE = "Subscription.channel.type"; + private static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching"; + static final String SUBSCRIPTION_STATUS = "Subscription.status"; + static final String SUBSCRIPTION_TYPE = "Subscription.channel.type"; private SubscribableChannel myProcessingChannel; @Autowired @@ -64,7 +65,6 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd super(); } - @PostConstruct public void start() { if (myProcessingChannel == null) { myProcessingChannel = mySubscriptionChannelFactory.newMatchingChannel(SUBSCRIPTION_MATCHING_CHANNEL_NAME); @@ -77,7 +77,10 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd @SuppressWarnings("unused") @PreDestroy public void preDestroy() { - myProcessingChannel.unsubscribe(mySubscriptionMatchingSubscriber); + + if (myProcessingChannel != null) { + myProcessingChannel.unsubscribe(mySubscriptionMatchingSubscriber); + } } @Override @@ -100,8 +103,9 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd submitResourceModified(msg); } - protected void sendToProcessingChannel(final ResourceModifiedMessage theMessage) { + private void sendToProcessingChannel(final ResourceModifiedMessage theMessage) { ourLog.trace("Sending resource modified message to processing channel"); + Validate.notNull(myProcessingChannel, "A SubscriptionMatcherInterceptor has been registered without calling start() on it."); myProcessingChannel.send(new ResourceModifiedJsonMessage(theMessage)); } @@ -117,7 +121,7 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd } @VisibleForTesting - public LinkedBlockingQueueSubscribableChannel getProcessingChannelForUnitTest() { + LinkedBlockingQueueSubscribableChannel getProcessingChannelForUnitTest() { return (LinkedBlockingQueueSubscribableChannel) myProcessingChannel; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java index bda9ec74f4b..f876ef1750b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java @@ -72,10 +72,10 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @Service -public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc, ApplicationContextAware { +public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTriggeringProvider.class); - public static final int DEFAULT_MAX_SUBMIT = 10000; + private static final int DEFAULT_MAX_SUBMIT = 10000; @Autowired private FhirContext myFhirContext; @@ -88,11 +88,10 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc @Autowired private MatchUrlService myMatchUrlService; @Autowired - private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + private IResourceModifiedConsumer myResourceModifiedConsumer; private final List<SubscriptionTriggeringJobDetails> myActiveJobs = new ArrayList<>(); private int myMaxSubmitPerPass = DEFAULT_MAX_SUBMIT; - private ApplicationContext myAppCtx; private ExecutorService myExecutorService; @Override @@ -105,7 +104,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc if (theSubscriptionId != null) { IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao(); IIdType subscriptionId = theSubscriptionId; - if (subscriptionId.hasResourceType() == false) { + if (!subscriptionId.hasResourceType()) { subscriptionId = subscriptionId.withResourceType(ResourceTypeEnum.SUBSCRIPTION.getCode()); } subscriptionDao.read(subscriptionId); @@ -128,7 +127,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc // Search URLs must be valid for (StringParam next : searchUrls) { - if (next.getValue().contains("?") == false) { + if (!next.getValue().contains("?")) { throw new InvalidRequestException("Search URL is not valid (must be in the form \"[resource type]?[optional params]\")"); } } @@ -163,7 +162,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc return; } - String activeJobIds = myActiveJobs.stream().map(t -> t.getJobId()).collect(Collectors.joining(", ")); + String activeJobIds = myActiveJobs.stream().map(SubscriptionTriggeringJobDetails::getJobId).collect(Collectors.joining(", ")); ourLog.info("Starting pass: currently have {} active job IDs: {}", myActiveJobs.size(), activeJobIds); SubscriptionTriggeringJobDetails activeJob = myActiveJobs.get(0); @@ -290,7 +289,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc private Future<Void> submitResource(String theSubscriptionId, String theResourceIdToTrigger) { org.hl7.fhir.r4.model.IdType resourceId = new org.hl7.fhir.r4.model.IdType(theResourceIdToTrigger); - IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceId.getResourceType()); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceId.getResourceType()); IBaseResource resourceToTrigger = dao.read(resourceId); return submitResource(theSubscriptionId, resourceToTrigger); @@ -306,7 +305,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc return myExecutorService.submit(() -> { for (int i = 0; ; i++) { try { - mySubscriptionMatcherInterceptor.submitResourceModified(msg); + myResourceModifiedConsumer.submitResourceModified(msg); break; } catch (Exception e) { if (i >= 3) { @@ -329,11 +328,6 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc } } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - myAppCtx = applicationContext; - } - /** * Sets the maximum number of resources that will be submitted in a single pass */ @@ -346,7 +340,6 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc myMaxSubmitPerPass = maxSubmitPerPass; } - @SuppressWarnings("unchecked") @PostConstruct public void start() { LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000); @@ -393,67 +386,67 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc private String myCurrentSearchResourceType; private int myCurrentSearchLastUploadedIndex; - public Integer getCurrentSearchCount() { + Integer getCurrentSearchCount() { return myCurrentSearchCount; } - public void setCurrentSearchCount(Integer theCurrentSearchCount) { + void setCurrentSearchCount(Integer theCurrentSearchCount) { myCurrentSearchCount = theCurrentSearchCount; } - public String getCurrentSearchResourceType() { + String getCurrentSearchResourceType() { return myCurrentSearchResourceType; } - public void setCurrentSearchResourceType(String theCurrentSearchResourceType) { + void setCurrentSearchResourceType(String theCurrentSearchResourceType) { myCurrentSearchResourceType = theCurrentSearchResourceType; } - public String getJobId() { + String getJobId() { return myJobId; } - public void setJobId(String theJobId) { + void setJobId(String theJobId) { myJobId = theJobId; } - public String getSubscriptionId() { + String getSubscriptionId() { return mySubscriptionId; } - public void setSubscriptionId(String theSubscriptionId) { + void setSubscriptionId(String theSubscriptionId) { mySubscriptionId = theSubscriptionId; } - public List<String> getRemainingResourceIds() { + List<String> getRemainingResourceIds() { return myRemainingResourceIds; } - public void setRemainingResourceIds(List<String> theRemainingResourceIds) { + void setRemainingResourceIds(List<String> theRemainingResourceIds) { myRemainingResourceIds = theRemainingResourceIds; } - public List<String> getRemainingSearchUrls() { + List<String> getRemainingSearchUrls() { return myRemainingSearchUrls; } - public void setRemainingSearchUrls(List<String> theRemainingSearchUrls) { + void setRemainingSearchUrls(List<String> theRemainingSearchUrls) { myRemainingSearchUrls = theRemainingSearchUrls; } - public String getCurrentSearchUuid() { + String getCurrentSearchUuid() { return myCurrentSearchUuid; } - public void setCurrentSearchUuid(String theCurrentSearchUuid) { + void setCurrentSearchUuid(String theCurrentSearchUuid) { myCurrentSearchUuid = theCurrentSearchUuid; } - public int getCurrentSearchLastUploadedIndex() { + int getCurrentSearchLastUploadedIndex() { return myCurrentSearchLastUploadedIndex; } - public void setCurrentSearchLastUploadedIndex(int theCurrentSearchLastUploadedIndex) { + void setCurrentSearchLastUploadedIndex(int theCurrentSearchLastUploadedIndex) { myCurrentSearchLastUploadedIndex = theCurrentSearchLastUploadedIndex; } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java index 161a66362b2..b08bafd86a8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java @@ -59,14 +59,14 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { protected static RestfulServer ourRestServer; protected static String ourServerBase; protected static SearchParamRegistryR4 ourSearchParamRegistry; - protected static DatabaseBackedPagingProvider ourPagingProvider; + private static DatabaseBackedPagingProvider ourPagingProvider; protected static ISearchDao mySearchEntityDao; protected static ISearchCoordinatorSvc mySearchCoordinatorSvc; - protected static GenericWebApplicationContext ourWebApplicationContext; - protected static SubscriptionMatcherInterceptor ourSubscriptionMatcherInterceptor; + private static GenericWebApplicationContext ourWebApplicationContext; + private static SubscriptionMatcherInterceptor ourSubscriptionMatcherInterceptor; private static Server ourServer; protected IGenericClient ourClient; - protected ResourceCountCache ourResourceCountsCache; + ResourceCountCache ourResourceCountsCache; private TerminologyUploaderProviderR4 myTerminologyUploaderProvider; private Object ourGraphQLProvider; private boolean ourRestHookSubscriptionInterceptorRequested; @@ -162,6 +162,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { mySearchEntityDao = wac.getBean(ISearchDao.class); ourSearchParamRegistry = wac.getBean(SearchParamRegistryR4.class); ourSubscriptionMatcherInterceptor = wac.getBean(SubscriptionMatcherInterceptor.class); + ourSubscriptionMatcherInterceptor.start(); myFhirCtx.getRestfulClientFactory().setSocketTimeout(5000000); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 9b1851a94a6..22a2e6b90c7 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -40,7 +40,7 @@ public class ModelConfig { * <li><code>"http://hl7.org/fhir/StructureDefinition/*"</code></li> * </ul> */ - public static final Set<String> DEFAULT_LOGICAL_BASE_URLS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( + public static final Set<String> DEFAULT_LOGICAL_BASE_URLS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "http://hl7.org/fhir/ValueSet/*", "http://hl7.org/fhir/CodeSystem/*", "http://hl7.org/fhir/valueset-*", @@ -57,6 +57,7 @@ public class ModelConfig { private boolean myDefaultSearchParamsCanBeOverridden = false; private Set<Subscription.SubscriptionChannelType> mySupportedSubscriptionTypes = new HashSet<>(); private String myEmailFromAddress = "noreply@unknown.com"; + private boolean mySubscriptionMatchingEnabled = true; /** * If set to {@code true} the default search params (i.e. the search parameters that are @@ -225,7 +226,7 @@ public class ModelConfig { } } - HashSet<String> treatBaseUrlsAsLocal = new HashSet<String>(); + HashSet<String> treatBaseUrlsAsLocal = new HashSet<>(); for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet<String>())) { while (next.endsWith("/")) { next = next.substring(0, next.length() - 1); @@ -320,6 +321,27 @@ public class ModelConfig { return Collections.unmodifiableSet(mySupportedSubscriptionTypes); } + /** + * If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions + * and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs. + * @since 3.7.0 + */ + + public boolean isSubscriptionMatchingEnabled() { + return mySubscriptionMatchingEnabled; + } + + /** + * If set to <code>true</code> (default is true) the server will match incoming resources against active subscriptions + * and send them to the subscription channel. If set to <code>false</code> no matching or sending occurs. + * @since 3.7.0 + */ + + + public void setSubscriptionMatchingEnabled(boolean theSubscriptionMatchingEnabled) { + mySubscriptionMatchingEnabled = theSubscriptionMatchingEnabled; + } + @VisibleForTesting public void clearSupportedSubscriptionTypesForUnitTest() { mySupportedSubscriptionTypes.clear(); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index 6d4fc7e0a35..363a7bcea9b 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -42,16 +43,19 @@ import java.util.Optional; * handlers are all caches in this registry so they can be removed it the subscription is deleted. */ +// TODO KHS Does jpa need a subscription registry if matching is disabled? @Component public class SubscriptionRegistry { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionRegistry.class); @Autowired - SubscriptionCanonicalizer mySubscriptionCanonicalizer; + SubscriptionCanonicalizer<IBaseResource> mySubscriptionCanonicalizer; @Autowired SubscriptionDeliveryHandlerFactory mySubscriptionDeliveryHandlerFactory; @Autowired SubscriptionChannelFactory mySubscriptionDeliveryChannelFactory; + @Autowired + ModelConfig myModelConfig; private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); @@ -71,18 +75,26 @@ public class SubscriptionRegistry { } @SuppressWarnings("UnusedReturnValue") - public CanonicalSubscription registerSubscription(IIdType theId, IBaseResource theSubscription) { + private CanonicalSubscription registerSubscription(IIdType theId, IBaseResource theSubscription) { Validate.notNull(theId); String subscriptionId = theId.getIdPart(); Validate.notBlank(subscriptionId); Validate.notNull(theSubscription); CanonicalSubscription canonicalized = mySubscriptionCanonicalizer.canonicalize(theSubscription); - SubscribableChannel deliveryChannel = mySubscriptionDeliveryChannelFactory.newDeliveryChannel(subscriptionId, canonicalized.getChannelType().toCode().toLowerCase()); - Optional<MessageHandler> deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(canonicalized); + SubscribableChannel deliveryChannel; + Optional<MessageHandler> deliveryHandler; + + if (myModelConfig.isSubscriptionMatchingEnabled()) { + deliveryChannel = mySubscriptionDeliveryChannelFactory.newDeliveryChannel(subscriptionId, canonicalized.getChannelType().toCode().toLowerCase()); + deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(canonicalized); + } else { + deliveryChannel = null; + deliveryHandler = Optional.empty(); + } ActiveSubscription activeSubscription = new ActiveSubscription(canonicalized, deliveryChannel); - deliveryHandler.ifPresent(handler -> activeSubscription.register(handler)); + deliveryHandler.ifPresent(activeSubscription::register); myActiveSubscriptionCache.put(subscriptionId, activeSubscription); diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java index b55756d22e4..0516da71584 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java @@ -112,8 +112,6 @@ public class FhirAutoConfiguration { private final IPagingProvider pagingProvider; - private final List<IServerInterceptor> interceptors; - private final List<FhirRestfulServerCustomizer> customizers; public FhirRestfulServerConfiguration( @@ -127,7 +125,6 @@ public class FhirAutoConfiguration { this.fhirContext = fhirContext; this.resourceProviders = resourceProviders.getIfAvailable(); this.pagingProvider = pagingProvider.getIfAvailable(); - this.interceptors = interceptors.getIfAvailable(); this.customizers = customizers.getIfAvailable(); } @@ -154,7 +151,6 @@ public class FhirAutoConfiguration { setFhirContext(this.fhirContext); setResourceProviders(this.resourceProviders); setPagingProvider(this.pagingProvider); - setInterceptors(this.interceptors); setServerAddressStrategy(new HardcodedServerAddressStrategy(this.properties.getServer().getPath())); From 66dc7f82d584b1051030be9c8293008f88f8cc5a Mon Sep 17 00:00:00 2001 From: Ken Stevens <khstevens@gmail.com> Date: Tue, 15 Jan 2019 20:55:00 -0500 Subject: [PATCH 20/56] organize imports --- .../src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java | 2 -- .../jpa/subscription/SubscriptionMatcherInterceptor.java | 1 - .../jpa/subscription/SubscriptionTriggeringSvcImpl.java | 8 ++++---- .../spring/boot/autoconfigure/FhirAutoConfiguration.java | 7 ------- 4 files changed, 4 insertions(+), 14 deletions(-) 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 472ba63e4f3..5e6377848cb 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 @@ -8,8 +8,6 @@ import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl; -import ca.uhn.fhir.jpa.subscription.ISubscriptionTriggeringSvc; -import ca.uhn.fhir.jpa.subscription.SubscriptionTriggeringSvcImpl; import ca.uhn.fhir.jpa.subscription.dbmatcher.CompositeInMemoryDaoSubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.dbmatcher.DaoSubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.cache.ISubscribableChannelFactory; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java index 2d6a421c623..c6f34c017c0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java @@ -18,7 +18,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.messaging.SubscribableChannel; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; /*- diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java index f876ef1750b..a3ee7aa5358 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java @@ -55,15 +55,15 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; import java.util.concurrent.*; import java.util.stream.Collectors; diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java index 0516da71584..98bfc7e706e 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/src/main/java/ca/uhn/fhir/spring/boot/autoconfigure/FhirAutoConfiguration.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.spring.boot.autoconfigure; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; -import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2; import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; import ca.uhn.fhir.jpa.config.BaseJavaConfigR4; @@ -31,7 +30,6 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.provider.BaseJpaProvider; import ca.uhn.fhir.jpa.provider.BaseJpaSystemProvider; -import ca.uhn.fhir.model.dstu2.resource.AuditEvent; import ca.uhn.fhir.okhttp.client.OkHttpRestfulClientFactory; import ca.uhn.fhir.rest.client.apache.ApacheRestfulClientFactory; import ca.uhn.fhir.rest.client.api.IClientInterceptor; @@ -61,11 +59,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; -import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean; import org.springframework.util.CollectionUtils; import javax.servlet.ServletException; From 7ffbe9505b83de548675ad79757bfecda44a525d Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Wed, 16 Jan 2019 23:35:14 -0500 Subject: [PATCH 21/56] Prototype. Not working yet. --- .../interceptor/InterceptorRegistry.java | 34 +++++++++ .../module/cache/SubscriptionRegistry.java | 5 ++ .../SubscriptionMatchingSubscriber.java | 13 +++- .../subscription/module/LatchedService.java | 69 +++++++++++++++++++ ...kingQueueSubscribableChannelDstu3Test.java | 39 +++++++++-- .../SubscriptionCheckingSubscriberTest.java | 15 ++-- .../SubscriptionMatchingSubscriberTest.java | 17 +++-- 7 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/interceptor/InterceptorRegistry.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/interceptor/InterceptorRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/interceptor/InterceptorRegistry.java new file mode 100644 index 00000000000..99651e462ba --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/interceptor/InterceptorRegistry.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jpa.searchparam.interceptor; + +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.Predicate; + +import static javolution.testing.TestContext.assertTrue; + +@Component +public class InterceptorRegistry { + private final Map<String, List<Predicate<Object>>> interceptorMap = new HashMap<>(); + + public void addInterceptor(String key, Predicate<Object> interceptor) { + interceptorMap.computeIfAbsent(key, entry -> new ArrayList<>()).add(interceptor); + } + + public void removeInterceptor(String key, Predicate<Object> interceptor) { + assertTrue(interceptorMap.get(key).remove(interceptor)); + } + + // TODO KHS this feels like it should be a one-line lambda + public Boolean trigger(String key, Object object) { + List<Predicate<Object>> predicates = interceptorMap.get(key); + if (predicates != null) { + for (Predicate<Object> predicate : predicates) { + if (!predicate.test(object)) { + return false; + } + } + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index 363a7bcea9b..f746995bd8f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription.module.cache; */ import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -47,6 +48,7 @@ import java.util.Optional; @Component public class SubscriptionRegistry { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionRegistry.class); + public static final String INTERCEPTOR_POST_ACTIVATED = "SubscriptionRegistry.postActivated"; @Autowired SubscriptionCanonicalizer<IBaseResource> mySubscriptionCanonicalizer; @@ -56,6 +58,8 @@ public class SubscriptionRegistry { SubscriptionChannelFactory mySubscriptionDeliveryChannelFactory; @Autowired ModelConfig myModelConfig; + @Autowired + InterceptorRegistry myInterceptorRegistry; private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); @@ -97,6 +101,7 @@ public class SubscriptionRegistry { deliveryHandler.ifPresent(activeSubscription::register); myActiveSubscriptionCache.put(subscriptionId, activeSubscription); + myInterceptorRegistry.trigger(INTERCEPTOR_POST_ACTIVATED, theSubscription); return canonicalized; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index 1acf9bbe662..c540936a98f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; @@ -30,9 +31,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -44,6 +45,8 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; @Service public class SubscriptionMatchingSubscriber implements MessageHandler { private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatchingSubscriber.class); + public static final String INTERCEPTOR_PRE_PROCESSED = "SubscriptionMatchingSubscriber.preProcessed"; + public static final String INTERCEPTOR_POST_PROCESSED = "SubscriptionMatchingSubscriber.postProcessed"; @Autowired private ISubscriptionMatcher mySubscriptionMatcher; @@ -51,6 +54,8 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { private FhirContext myFhirContext; @Autowired private SubscriptionRegistry mySubscriptionRegistry; + @Autowired + private InterceptorRegistry myInterceptorRegistry; @Override public void handleMessage(Message<?> theMessage) throws MessagingException { @@ -66,6 +71,9 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { } public void matchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { + if (!myInterceptorRegistry.trigger(INTERCEPTOR_PRE_PROCESSED, theMsg)) { + return; + } switch (theMsg.getOperationType()) { case CREATE: case UPDATE: @@ -135,5 +143,6 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { ourLog.warn("Do not have delivery channel for subscription {}", nextActiveSubscription.getIdElement(myFhirContext)); } } + myInterceptorRegistry.trigger(INTERCEPTOR_POST_PROCESSED, theMsg); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java new file mode 100644 index 00000000000..359621338c9 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java @@ -0,0 +1,69 @@ +package ca.uhn.fhir.jpa.subscription.module; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class LatchedService implements Predicate<Object> { + private static final Logger ourLog = LoggerFactory.getLogger(LatchedService.class); + private final String name; + + private CountDownLatch myCountdownLatch; + private AtomicReference<String> myFailure; + private AtomicReference<List<Object>> myCalledWith; + + public LatchedService(String name) { + this.name = name; + } + + public void countdown() { + if (myCountdownLatch == null) { + myFailure.set(name + " latch countdown() called before expectedCount set."); + } else if (myCountdownLatch.getCount() <= 0) { + myFailure.set(name + " latch countdown() called "+ (1 - myCountdownLatch.getCount()) + " more times than expected."); + } + ourLog.info("{} counting down {}", name, myCountdownLatch); + myCountdownLatch.countDown(); + } + + public void setExpectedCount(int count) { + myFailure = new AtomicReference<>(); + myCalledWith = new AtomicReference<>(new ArrayList<>()); + myCountdownLatch = new CountDownLatch(count); + } + + public void awaitExpected() throws InterruptedException { + awaitExpectedWithTimeout(10); + } + + public void awaitExpectedWithTimeout(int timeoutSecond) throws InterruptedException { + assertTrue(name+" latch timed out waiting "+timeoutSecond+" seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); + + if (myFailure.get() != null) { + String error = myFailure.get(); + error += "\nLatch called with values: "+myCalledWith.get().stream().map(Object::toString).collect(Collectors.joining(", ")); + throw new AssertionError(error); + } + } + + @Override + public boolean test(Object object) { + this.countdown(); + if (myCalledWith.get() != null) { + myCalledWith.get().add(object); + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index 8b3186ea951..b061ece857e 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -1,10 +1,14 @@ package ca.uhn.fhir.jpa.subscription.module.standalone; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; +import ca.uhn.fhir.jpa.subscription.module.LatchedService; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage; +import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriber; import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriberTest; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.ResourceParam; @@ -21,6 +25,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -36,6 +41,7 @@ import java.util.List; public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends BaseSubscriptionDstu3Test { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatchingSubscriberTest.class); + protected static ObservationListener ourObservationListener; @Autowired FhirContext myFhirContext; @@ -43,6 +49,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base StandaloneSubscriptionMessageHandler myStandaloneSubscriptionMessageHandler; @Autowired SubscriptionChannelFactory mySubscriptionChannelFactory; + @Autowired + InterceptorRegistry myInterceptorRegistry; protected String myCode = "1000000050"; @@ -56,6 +64,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base private static SubscribableChannel ourSubscribableChannel; private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); private long idCounter = 0; + protected LatchedService mySubscriptionMatchingPost = new LatchedService(SubscriptionMatchingSubscriber.INTERCEPTOR_POST_PROCESSED); + protected LatchedService mySubscriptionActivatedPost = new LatchedService(SubscriptionRegistry.INTERCEPTOR_POST_ACTIVATED); @Before public void beforeReset() { @@ -66,6 +76,14 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base ourSubscribableChannel = mySubscriptionChannelFactory.newDeliveryChannel("test", Subscription.SubscriptionChannelType.RESTHOOK.toCode().toLowerCase()); ourSubscribableChannel.subscribe(myStandaloneSubscriptionMessageHandler); } + myInterceptorRegistry.addInterceptor(SubscriptionMatchingSubscriber.INTERCEPTOR_POST_PROCESSED, mySubscriptionMatchingPost); + myInterceptorRegistry.addInterceptor(SubscriptionRegistry.INTERCEPTOR_POST_ACTIVATED, mySubscriptionActivatedPost); + } + + @After + public void cleanup() { + myInterceptorRegistry.removeInterceptor(SubscriptionRegistry.INTERCEPTOR_POST_ACTIVATED, mySubscriptionActivatedPost); + myInterceptorRegistry.removeInterceptor(SubscriptionMatchingSubscriber.INTERCEPTOR_POST_PROCESSED, mySubscriptionMatchingPost); } public <T extends IBaseResource> T sendResource(T theResource) { @@ -77,8 +95,10 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base protected Subscription sendSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException { Subscription subscription = returnedActiveSubscription(theCriteria, thePayload, theEndpoint); - - return sendResource(subscription); + mySubscriptionActivatedPost.setExpectedCount(1); + Subscription retval = sendResource(subscription); + mySubscriptionActivatedPost.awaitExpected(); + return retval; } protected Subscription returnedActiveSubscription(String theCriteria, String thePayload, String theEndpoint) { @@ -122,8 +142,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base ourListenerRestServer = new RestfulServer(FhirContext.forDstu3()); ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; - ObservationListener obsListener = new ObservationListener(); - ourListenerRestServer.setResourceProviders(obsListener); + ourObservationListener = new ObservationListener(); + ourListenerRestServer.setResourceProviders(ourObservationListener); ourListenerServer = new Server(ourListenerPort); @@ -145,6 +165,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base public static class ObservationListener implements IResourceProvider { + private LatchedService updateLatch = new LatchedService("Observation Update"); + @Create public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { ourLog.info("Received Listener Create"); @@ -162,8 +184,17 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); ourUpdatedObservations.add(theObservation); + updateLatch.countdown(); ourLog.info("Received Listener Update (now have {} updates)", ourUpdatedObservations.size()); return new MethodOutcome(new IdType("Observation/1"), false); } + + public void setExpectedCount(int count) { + updateLatch.setExpectedCount(count); + } + + public void awaitExpected() throws InterruptedException { + updateLatch.awaitExpected(); + } } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java index 2b656d1f79d..58e5f696f38 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java @@ -25,10 +25,10 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); } @@ -43,10 +43,10 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); } @@ -61,9 +61,10 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + ourObservationListener.setExpectedCount(0); + mySubscriptionMatchingPost.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - - waitForSize(0, ourCreatedObservations); - waitForSize(0, ourUpdatedObservations); + mySubscriptionMatchingPost.awaitExpected(); + ourObservationListener.awaitExpected(); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java index 2a8e605a577..5eb8e73db12 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java @@ -1,10 +1,12 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; +import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.standalone.BaseBlockingQueueSubscribableChannelDstu3Test; import ca.uhn.fhir.rest.api.Constants; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import static org.junit.Assert.assertEquals; @@ -25,10 +27,10 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); } @@ -43,10 +45,10 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); } @@ -61,9 +63,10 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + ourObservationListener.setExpectedCount(0); + mySubscriptionMatchingPost.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - - waitForSize(0, ourCreatedObservations); - waitForSize(0, ourUpdatedObservations); + mySubscriptionMatchingPost.awaitExpected(); + ourObservationListener.awaitExpected(); } } From 4367e340ae038e0732341d7149268297f48fca81 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Thu, 17 Jan 2019 16:05:54 -0500 Subject: [PATCH 22/56] Log the version number and commit on startup --- hapi-fhir-base/pom.xml | 33 +++++++++++++++++++ .../java/ca/uhn/fhir/util/VersionUtil.java | 32 ++++++++++++++---- .../ca/uhn/fhir/util/VersionUtilTest.java | 19 +++++++++++ pom.xml | 5 +++ src/changes/changes.xml | 4 +++ 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionUtilTest.java diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 4056f44e884..ef32c50de30 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -162,12 +162,45 @@ </instructions> </configuration> </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>buildnumber-maven-plugin</artifactId> + <inherited>true</inherited> + <executions> + <execution> + <id>standard</id> + <phase>validate</phase> + <goals> + <goal>create</goal> + </goals> + </execution> + <execution> + <id>downstream</id> + <phase>validate</phase> + <goals> + <goal>create-metadata</goal> + </goals> + <configuration> + <outputDirectory>${project.build.directory}/generated-sources/properties</outputDirectory> + <outputName>ca/uhn/fhir/hapi-fhir-base-build.properties</outputName> + <revisionPropertyName>hapifhir.buildnumber</revisionPropertyName> + <timestampPropertyName>hapifhir.timestamp</timestampPropertyName> + <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SXXX</timestampFormat> + <versionPropertyName>hapifhir.version</versionPropertyName> + </configuration> + </execution> + </executions> + </plugin> </plugins> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> + <resource> + <directory>${project.build.directory}/generated-sources/properties</directory> + <filtering>false</filtering> + </resource> </resources> </build> diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java index f0852a252df..4853f98a8dc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.util; * 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. @@ -20,9 +20,13 @@ package ca.uhn.fhir.util; * #L% */ +import org.apache.commons.lang3.StringUtils; + import java.io.InputStream; import java.util.Properties; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; + /** * Used internally by HAPI to log the version of the HAPI FHIR framework * once, when the framework is first loaded by the classloader. @@ -31,24 +35,40 @@ public class VersionUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(VersionUtil.class); private static String ourVersion; + private static String ourBuildNumber; + private static String ourBuildTime; static { initialize(); } + public static String getBuildNumber() { + return ourBuildNumber; + } + + public static String getBuildTime() { + return ourBuildTime; + } + public static String getVersion() { return ourVersion; } private static void initialize() { - try (InputStream is = VersionUtil.class.getResourceAsStream("/ca/uhn/fhir/hapi-version.properties")) { + try (InputStream is = VersionUtil.class.getResourceAsStream("/ca/uhn/fhir/hapi-fhir-base-build.properties")) { + Properties p = new Properties(); p.load(is); - ourVersion = p.getProperty("version"); - ourLog.info("HAPI FHIR version is: " + ourVersion); + + ourVersion = p.getProperty("hapifhir.version"); + ourVersion = defaultIfBlank(ourVersion, "(unknown)"); + + ourBuildNumber = p.getProperty("hapifhir.buildnumber"); + ourBuildTime = p.getProperty("hapifhir.timestamp"); + ourLog.info("HAPI FHIR version {} - Rev {}", ourVersion, StringUtils.right(ourBuildNumber, 10)); } catch (Exception e) { ourLog.warn("Unable to determine HAPI version information", e); } } - + } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionUtilTest.java new file mode 100644 index 00000000000..c66151b6d3e --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/VersionUtilTest.java @@ -0,0 +1,19 @@ +package ca.uhn.fhir.util; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.blankOrNullString; +import static org.junit.Assert.*; + +public class VersionUtilTest { + + @Test + public void testProperties() { + assertThat(VersionUtil.getVersion(), not(blankOrNullString())); + assertThat(VersionUtil.getBuildNumber(), not(blankOrNullString())); + assertThat(VersionUtil.getBuildTime(), not(blankOrNullString())); + } + + +} diff --git a/pom.xml b/pom.xml index 9e3ef32c75b..e99d52fdaf3 100644 --- a/pom.xml +++ b/pom.xml @@ -1485,6 +1485,11 @@ <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>buildnumber-maven-plugin</artifactId> + <version>1.4</version> + </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>animal-sniffer-maven-plugin</artifactId> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 16a6c4a3417..6ea40d80996 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -303,6 +303,10 @@ AuthorizationInterceptor can now allow a user to perform a search that is scoped to a particular resource (e.g. Patient?_id=123) if the user has read access for that specific instance. </action> + <action type="add"> + HAPI FHIR will now log the Git revision when it first starts up (on the ame line as the version number + that it already logs). + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From 432ad8e5bcc90bf6bbc0835b0f526270e9b62c24 Mon Sep 17 00:00:00 2001 From: Volker Schmidt <volker@volsch.eu> Date: Thu, 27 Sep 2018 13:46:51 +0200 Subject: [PATCH 23/56] Subscription without Payload did not contain header. --- .../jpa/subscription/NotificationServlet.java | 39 +++++++++++ .../resthook/RestHookTestDstu3Test.java | 70 ++++++++++++++++++- ...scriptionDeliveringRestHookSubscriber.java | 23 ++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/NotificationServlet.java diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/NotificationServlet.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/NotificationServlet.java new file mode 100644 index 00000000000..7abb32a3831 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/NotificationServlet.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.subscription; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Receives subscription notification without payloads. + */ +public class NotificationServlet extends HttpServlet { + private static final long serialVersionUID = 5957950857980374719L; + + private final AtomicLong receivedNotificationCount = new AtomicLong(); + + private final List<String> receivedAuthorizationHeaders = Collections.synchronizedList(new ArrayList<>()); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + receivedNotificationCount.incrementAndGet(); + receivedAuthorizationHeaders.add(req.getHeader("Authorization")); + } + + public long getReceivedNotificationCount() { + return receivedNotificationCount.get(); + } + + public List<String> getReceivedAuthorizationHeaders() { + return receivedAuthorizationHeaders; + } + + public void reset() { + receivedNotificationCount.set(0); + receivedAuthorizationHeaders.clear(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java index 97716aac54f..a0d71ca92a5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java @@ -49,6 +49,11 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { @Autowired private SubscriptionTestUtil mySubscriptionTestUtil; + private static NotificationServlet ourNotificationServlet; + private static String ourNotificationListenerServer; + private static List<Observation> ourUpdatedObservations = Lists.newArrayList(); + private static List<String> ourContentTypes = new ArrayList<>(); + private List<IIdType> mySubscriptionIds = new ArrayList<>(); @After public void afterUnregisterRestHookListener() { @@ -79,9 +84,15 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { ourCreatedObservations.clear(); ourUpdatedObservations.clear(); ourContentTypes.clear(); + ourNotificationServlet.reset(); } - private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint) throws InterruptedException { + private Subscription createSubscription(String criteria, String payload, String endpoint) throws InterruptedException { + return createSubscription(criteria, payload, endpoint, null); + } + + private Subscription createSubscription(String theCriteria, String thePayload, String theEndpoint, + List<StringType> headers) throws InterruptedException { Subscription subscription = new Subscription(); subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); @@ -91,6 +102,9 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { channel.setType(Subscription.SubscriptionChannelType.RESTHOOK); channel.setPayload(thePayload); channel.setEndpoint(theEndpoint); + if (headers != null) { + channel.setHeader(headers); + } subscription.setChannel(channel); MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); @@ -120,6 +134,55 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { return observation; } + @Test + public void testRestHookSubscription() throws Exception { + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; + + createSubscription(criteria1, null, ourNotificationListenerServer, + Collections.singletonList(new StringType("Authorization: abc-def"))); + createSubscription(criteria2, null, ourNotificationListenerServer); + + sendObservation(code, "SNOMED-CT"); + + // Should see 1 subscription notification with authorization header + waitForSize(1, ourNotificationServlet.getReceivedAuthorizationHeaders()); + Assert.assertEquals(1, ourNotificationServlet.getReceivedNotificationCount()); + Assert.assertEquals("abc-def", ourNotificationServlet.getReceivedAuthorizationHeaders().get(0)); + ourNotificationServlet.reset(); + + sendObservation(code, "SNOMED-CT"); + + // Should see 1 subscription notification with authorization header + waitForSize(1, ourNotificationServlet.getReceivedAuthorizationHeaders()); + Assert.assertEquals(1, ourNotificationServlet.getReceivedNotificationCount()); + Assert.assertEquals("abc-def", ourNotificationServlet.getReceivedAuthorizationHeaders().get(0)); + ourNotificationServlet.reset(); + + Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); + + /// Should see 1 subscription notification with authorization header + waitForSize(1, ourNotificationServlet.getReceivedAuthorizationHeaders()); + Assert.assertEquals(1, ourNotificationServlet.getReceivedNotificationCount()); + Assert.assertEquals("abc-def", ourNotificationServlet.getReceivedAuthorizationHeaders().get(0)); + ourNotificationServlet.reset(); + + Observation observation3 = ourClient.read(Observation.class, observationTemp3.getId()); + CodeableConcept codeableConcept = new CodeableConcept(); + observation3.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(code + "111"); + coding.setSystem("SNOMED-CT"); + ourClient.update().resource(observation3).withId(observation3.getIdElement()).execute(); + + // Should see 2 subscription notifications with and without authorization header + waitForSize(1, ourNotificationServlet.getReceivedAuthorizationHeaders()); + Assert.assertEquals(1, ourNotificationServlet.getReceivedNotificationCount()); + Assert.assertNull(ourNotificationServlet.getReceivedAuthorizationHeaders().get(0)); + ourNotificationServlet.reset(); + } + @Test public void testRestHookSubscriptionApplicationFhirJson() throws Exception { String payload = "application/fhir+json"; @@ -358,11 +421,13 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { ourListenerPort = PortUtil.findFreePort(); ourListenerRestServer = new RestfulServer(FhirContext.forDstu3()); ourListenerServerBase = "http://localhost:" + ourListenerPort + "/fhir/context"; + ourNotificationListenerServer = "http://localhost:" + ourListenerPort + "/fhir/subscription"; ObservationListener obsListener = new ObservationListener(); ourListenerRestServer.setResourceProviders(obsListener); ourListenerServer = new Server(ourListenerPort); + ourNotificationServlet = new NotificationServlet(); ServletContextHandler proxyHandler = new ServletContextHandler(); proxyHandler.setContextPath("/"); @@ -370,6 +435,9 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { ServletHolder servletHolder = new ServletHolder(); servletHolder.setServlet(ourListenerRestServer); proxyHandler.addServlet(servletHolder, "/fhir/context/*"); + servletHolder = new ServletHolder(); + servletHolder.setServlet(ourNotificationServlet); + proxyHandler.addServlet(servletHolder, "/fhir/subscription"); ourListenerServer.setHandler(proxyHandler); ourListenerServer.start(); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index b2d9e803053..383a0ce7272 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -21,6 +21,12 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; */ import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; +import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.*; @@ -28,6 +34,7 @@ import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; import ca.uhn.fhir.rest.gclient.IClientExecutable; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; @@ -183,12 +190,28 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe */ protected void sendNotification(ResourceDeliveryMessage theMsg) { Map<String, List<String>> params = new HashMap(); + List<Header> headers = new ArrayList<>(); + if (theMsg.getSubscription().getHeaders() != null) { + theMsg.getSubscription().getHeaders().stream().filter(Objects::nonNull).forEach(h -> { + final int sep = h.indexOf(':'); + if (sep > 0) { + final String name = h.substring(0, sep); + final String value = h.substring(sep + 1); + if (StringUtils.isNotBlank(name)) { + headers.add(new Header(name.trim(), value.trim())); + } + } + }); + } + StringBuilder url = new StringBuilder(theMsg.getSubscription().getEndpointUrl()); IHttpClient client = myFhirContext.getRestfulClientFactory().getHttpClient(url, params, "", RequestTypeEnum.POST, headers); IHttpRequest request = client.createParamRequest(myFhirContext, params, null); try { IHttpResponse response = request.execute(); + // close connection in order to return a possible cached connection to the connection pool + response.close(); } catch (IOException e) { ourLog.error("Error trying to reach "+ theMsg.getSubscription().getEndpointUrl()); e.printStackTrace(); From 5fd084d7cf02f745ca7c22ddc3c4b435aff80773 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Fri, 18 Jan 2019 05:47:37 -0500 Subject: [PATCH 24/56] Fix a couple of merge conflicts --- .../subscription/resthook/RestHookTestDstu3Test.java | 4 +--- .../SubscriptionDeliveringRestHookSubscriber.java | 11 +---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java index a0d71ca92a5..871934e7692 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java @@ -4,6 +4,7 @@ package ca.uhn.fhir.jpa.subscription.resthook; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; +import ca.uhn.fhir.jpa.subscription.NotificationServlet; import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.ResourceParam; @@ -51,9 +52,6 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { private SubscriptionTestUtil mySubscriptionTestUtil; private static NotificationServlet ourNotificationServlet; private static String ourNotificationListenerServer; - private static List<Observation> ourUpdatedObservations = Lists.newArrayList(); - private static List<String> ourContentTypes = new ArrayList<>(); - private List<IIdType> mySubscriptionIds = new ArrayList<>(); @After public void afterUnregisterRestHookListener() { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index 383a0ce7272..d251aabc1cf 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -21,12 +21,6 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; */ import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionDeliverySubscriber; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor; -import ca.uhn.fhir.jpa.subscription.CanonicalSubscription; -import ca.uhn.fhir.jpa.subscription.ResourceDeliveryMessage; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.*; @@ -45,10 +39,7 @@ import org.springframework.messaging.MessagingException; import org.springframework.stereotype.Component; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.apache.commons.lang3.StringUtils.isNotBlank; From 3d941ed7e303d02f9085ae6d651f94f1a15fed0a Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Fri, 18 Jan 2019 05:50:30 -0500 Subject: [PATCH 25/56] Credit for #1084 --- src/changes/changes.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 16a6c4a3417..53863c7e209 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -303,6 +303,12 @@ AuthorizationInterceptor can now allow a user to perform a search that is scoped to a particular resource (e.g. Patient?_id=123) if the user has read access for that specific instance. </action> + <action type="fix" issue="1084"> + In JPA Server REST Hook Subscriptions, any Headers defined in the + Subscription resource are now applied to the outgoing HTTP + request. + Thanks to Volker Schmidt for the pull request! + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From 5ce9d444e9979af3211edc911e2a558fd1980850 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Fri, 18 Jan 2019 09:04:45 -0500 Subject: [PATCH 26/56] Work on interceptors --- .../ca/uhn/fhir/hapi-version.properties | 1 - .../ca/uhn/fhir/jpa/config/TestR4Config.java | 3 +- .../provider/r4/ResourceProviderR4Test.java | 13 ++++ .../module/CanonicalSubscription.java | 67 ++++++++++++++++++- ...scriptionDeliveringRestHookSubscriber.java | 23 ++----- 5 files changed, 85 insertions(+), 22 deletions(-) delete mode 100644 hapi-fhir-base/src/main/resources/ca/uhn/fhir/hapi-version.properties diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/hapi-version.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/hapi-version.properties deleted file mode 100644 index e5683df88cb..00000000000 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/hapi-version.properties +++ /dev/null @@ -1 +0,0 @@ -version=${project.version} \ No newline at end of file diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index d9fcc5773cf..fc67daa6729 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.config; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; +import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.apache.commons.dbcp2.BasicDataSource; import org.springframework.context.annotation.Bean; @@ -96,7 +97,7 @@ public class TestR4Config extends BaseJavaConfigR4 { DataSource dataSource = ProxyDataSourceBuilder .create(retVal) -// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") + .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") // .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) // .countQuery(new ThreadQueryCountHolder()) .beforeQuery(new BlockLargeNumbersOfParamsListener()) 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 ec5c4fcd125..72f422b8a8b 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 @@ -945,6 +945,19 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { } } + + @Test + @Ignore + public void testQuery() throws IOException { + ourLog.info("** Performing Search"); + HttpGet read = new HttpGet(ourServerBase + "/MedicationRequest?category=community&identifier=urn:oid:2.16.840.1.113883.3.7418.12.3%7C&intent=order&medication.code:text=calcitriol,hectorol,Zemplar,rocaltrol,vectical,vitamin%20D,doxercalciferol,paricalcitol&status=active,completed"); + try (CloseableHttpResponse response = ourHttpClient.execute(read)) { + ourLog.info(response.toString()); + } + ourLog.info("** DONE Performing Search"); + + } + @Test public void testDeleteResourceConditional1() throws IOException { String methodName = "testDeleteResourceConditional1"; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java index 86e45ff8829..7b30f1abe15 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java @@ -31,6 +31,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.EventDefinition; import org.hl7.fhir.r4.model.Subscription; +import javax.annotation.Nonnull; import java.io.Serializable; import java.util.*; @@ -65,6 +66,39 @@ public class CanonicalSubscription implements Serializable { @JsonProperty("extensions") private Map<String, String> myChannelExtensions; + /** + * Constructor + */ + public CanonicalSubscription() { + super(); + } + + /** + * Copy Constructor + */ + public CanonicalSubscription(CanonicalSubscription theSubscription) { + myChannelExtensions = new HashMap<>(theSubscription.getChannelExtensions()); + myChannelType = theSubscription.getChannelType(); + myCriteriaString = theSubscription.getCriteriaString(); + if (theSubscription.getEmailDetails() != null) { + myEmailDetails = new EmailDetails(theSubscription.getEmailDetails()); + } + if (theSubscription.getRestHookDetails() != null) { + myRestHookDetails = new RestHookDetails(theSubscription.getRestHookDetails()); + } + myHeaders = new ArrayList<>(theSubscription.getHeaders()); + myEndpointUrl = theSubscription.getEndpointUrl(); + myIdElement = theSubscription.getIdElementString(); + myPayloadString = theSubscription.getPayloadString(); + myStatus = theSubscription.getStatus(); + // FIXME: is this constructor needed +// my + } + + private Map<String, String> getChannelExtensions() { + return myChannelExtensions != null ? myChannelExtensions : Collections.emptyMap(); + } + /** * For now we're using the R4 TriggerDefinition, but this * may change in the future when things stabilize @@ -105,8 +139,9 @@ public class CanonicalSubscription implements Serializable { myEndpointUrl = theEndpointUrl; } + @Nonnull public List<String> getHeaders() { - return Collections.unmodifiableList(myHeaders); + return myHeaders != null ? Collections.unmodifiableList(myHeaders) : Collections.emptyList(); } public void setHeaders(List<? extends IPrimitiveType<String>> theHeader) { @@ -236,6 +271,21 @@ public class CanonicalSubscription implements Serializable { @JsonProperty("subjectTemplate") private String mySubjectTemplate; + /** + * Construcor + */ + public EmailDetails() { + super(); + } + + /** + * Copy Constructor + */ + public EmailDetails(EmailDetails theEmailDetails) { + myFrom = theEmailDetails.getFrom(); + mySubjectTemplate = theEmailDetails.getSubjectTemplate(); + } + public String getFrom() { return myFrom; } @@ -261,6 +311,21 @@ public class CanonicalSubscription implements Serializable { @JsonProperty("deliverLatestVersion") private boolean myDeliverLatestVersion; + /** + * Constructor + */ + public RestHookDetails() { + super(); + } + + /** + * Copy constructor + */ + public RestHookDetails(RestHookDetails theRestHookDetails) { + myDeliverLatestVersion = theRestHookDetails.isDeliverLatestVersion(); + myStripVersionId = theRestHookDetails.isStripVersionId(); + } + public boolean isDeliverLatestVersion() { return myDeliverLatestVersion; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index b2d9e803053..cd945e8f845 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -55,7 +55,9 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) { IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription); - if (payloadResource == null) return; + if (payloadResource == null) { + return; + } doDelivery(theMsg, theSubscription, thePayloadType, theClient, payloadResource); } @@ -64,22 +66,6 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe IClientExecutable<?, ?> operation; switch (theMsg.getOperationType()) { case CREATE: - if (thePayloadResource == null || thePayloadResource.isEmpty()) { - if (thePayloadType != null ) { - operation = theClient.create().resource(thePayloadResource); - } else { - sendNotification(theMsg); - return; - } - } else { - if (thePayloadType != null ) { - operation = theClient.update().resource(thePayloadResource); - } else { - sendNotification(theMsg); - return; - } - } - break; case UPDATE: if (thePayloadResource == null || thePayloadResource.isEmpty()) { if (thePayloadType != null ) { @@ -179,10 +165,9 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe /** * Sends a POST notification without a payload - * @param theMsg */ protected void sendNotification(ResourceDeliveryMessage theMsg) { - Map<String, List<String>> params = new HashMap(); + Map<String, List<String>> params = new HashMap<>(); List<Header> headers = new ArrayList<>(); StringBuilder url = new StringBuilder(theMsg.getSubscription().getEndpointUrl()); IHttpClient client = myFhirContext.getRestfulClientFactory().getHttpClient(url, params, "", RequestTypeEnum.POST, headers); From 3d07fc1c22660d5504e76dffcbfce40ad581c06c Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Fri, 18 Jan 2019 10:39:35 -0500 Subject: [PATCH 27/56] fixed null subscription id (#1170) * fixed null subscription id --- ...mpositeInMemoryDaoSubscriptionMatcher.java | 11 ++-- .../dbmatcher/DaoSubscriptionMatcher.java | 6 ++- .../InMemorySubscriptionMatcherTestR4.java | 6 ++- .../matcher/CriteriaResourceMatcher.java | 4 +- .../module/matcher/ISubscriptionMatcher.java | 3 +- .../matcher/InMemorySubscriptionMatcher.java | 7 +-- .../matcher/SubscriptionMatchResult.java | 8 --- .../SubscriptionMatchingSubscriber.java | 54 ++++++++++++------- 8 files changed, 57 insertions(+), 42 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java index eec2462fd2c..b5cf2c969b8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription.dbmatcher; */ import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; @@ -43,16 +44,16 @@ public class CompositeInMemoryDaoSubscriptionMatcher implements ISubscriptionMat } @Override - public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) { + public SubscriptionMatchResult match(CanonicalSubscription theSubscription, ResourceModifiedMessage theMsg) { SubscriptionMatchResult result; if (myDaoConfig.isEnableInMemorySubscriptionMatching()) { - result = myInMemorySubscriptionMatcher.match(criteria, msg); + result = myInMemorySubscriptionMatcher.match(theSubscription, theMsg); if (!result.supported()) { - ourLog.info("Criteria {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", criteria, result.getUnsupportedReason()); - result = myDaoSubscriptionMatcher.match(criteria, msg); + ourLog.info("Criteria {} for Subscription {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", theSubscription.getCriteriaString(), theSubscription.getIdElementString(), result.getUnsupportedReason()); + result = myDaoSubscriptionMatcher.match(theSubscription, theMsg); } } else { - result = myDaoSubscriptionMatcher.match(criteria, msg); + result = myDaoSubscriptionMatcher.match(theSubscription, theMsg); } return result; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java index 24c41e9c560..ad8f7acfaac 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; @@ -49,10 +50,11 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher { MatchUrlService myMatchUrlService; @Override - public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) { - IIdType id = msg.getId(myCtx); + public SubscriptionMatchResult match(CanonicalSubscription theSubscription, ResourceModifiedMessage theMsg) { + IIdType id = theMsg.getId(myCtx); String resourceType = id.getResourceType(); String resourceId = id.getIdPart(); + String criteria = theSubscription.getCriteriaString(); // run the subscriptions query and look for matches, add the id as part of the criteria to avoid getting matches of previous resources rather than the recent resource criteria += "&_id=" + resourceType + "/" + resourceId; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java index 65d3daa91c1..054670e9607 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -380,10 +381,13 @@ public class InMemorySubscriptionMatcherTestR4 { params.add(Patient.SP_FAMILY, new StringParam("testSearchNameParam01Fam")); try { String criteria = params.toNormalizedQueryString(myContext); + CanonicalSubscription subscription = new CanonicalSubscription(); + subscription.setCriteriaString(criteria); + subscription.setIdElement(new IdType("Subscription", 123L)); ResourceModifiedMessage msg = new ResourceModifiedMessage(myContext, patient, ResourceModifiedMessage.OperationTypeEnum.CREATE); msg.setSubscriptionId("Subscription/123"); msg.setId(new IdType("Patient/ABC")); - SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, msg); + SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(subscription, msg); fail(); } catch (InternalErrorException e){ assertEquals("Failure processing resource ID[Patient/ABC] for subscription ID[Subscription/123]: Invalid resource reference found at path[Patient.managingOrganization] - Does not contain resource type - urn:uuid:13720262-b392-465f-913e-54fb198ff954", e.getMessage()); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java index faad12db014..3e8fabb9aba 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java @@ -57,7 +57,7 @@ public class CriteriaResourceMatcher { } searchParameterMap.clean(); if (searchParameterMap.getLastUpdated() != null) { - return new SubscriptionMatchResult(Constants.PARAM_LASTUPDATED, "Qualifiers not supported"); + return new SubscriptionMatchResult(Constants.PARAM_LASTUPDATED, "Standard Parameters not supported"); } for (Map.Entry<String, List<List<? extends IQueryParameterType>>> entry : searchParameterMap.entrySet()) { @@ -79,7 +79,7 @@ public class CriteriaResourceMatcher { if (hasQualifiers(theAndOrParams)) { - return new SubscriptionMatchResult(theParamName, "Qualifiers not supported."); + return new SubscriptionMatchResult(theParamName, "Standard Parameters not supported."); } if (hasPrefixes(theAndOrParams)) { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java index 26044e7139f..cf012682bed 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/ISubscriptionMatcher.java @@ -20,8 +20,9 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; * #L% */ +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; public interface ISubscriptionMatcher { - SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg); + SubscriptionMatchResult match(CanonicalSubscription subscription, ResourceModifiedMessage msg); } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java index 6c11e139641..414cac544b0 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -45,11 +46,11 @@ public class InMemorySubscriptionMatcher implements ISubscriptionMatcher { private InlineResourceLinkResolver myInlineResourceLinkResolver; @Override - public SubscriptionMatchResult match(String criteria, ResourceModifiedMessage msg) { + public SubscriptionMatchResult match(CanonicalSubscription theSubscription, ResourceModifiedMessage theMsg) { try { - return match(criteria, msg.getNewPayload(myContext)); + return match(theSubscription.getCriteriaString(), theMsg.getNewPayload(myContext)); } catch (Exception e) { - throw new InternalErrorException("Failure processing resource ID[" + msg.getId(myContext) + "] for subscription ID[" + msg.getSubscriptionId() + "]: " + e.getMessage(), e); + throw new InternalErrorException("Failure processing resource ID[" + theMsg.getId(myContext) + "] for subscription ID[" + theSubscription.getIdElementString() + "]: " + e.getMessage(), e); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java index 074a9df81b6..4ed696989cb 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java @@ -43,14 +43,6 @@ public class SubscriptionMatchResult { this.myMatcherShortName = theMatcherShortName; } - public SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason, String theMatcherShortName) { - this.myMatch = false; - this.mySupported = false; - this.myUnsupportedParameter = theUnsupportedParameter; - this.myUnsupportedReason = theUnsupportedReason; - this.myMatcherShortName = theMatcherShortName; - } - public boolean supported() { return mySupported; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index 1acf9bbe662..8bb7a7cc7ec 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -78,8 +78,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { return; } - IIdType id = theMsg.getId(myFhirContext); - String resourceType = id.getResourceType(); + IIdType resourceId = theMsg.getId(myFhirContext); Collection<ActiveSubscription> subscriptions = mySubscriptionRegistry.getAll(); @@ -87,8 +86,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { for (ActiveSubscription nextActiveSubscription : subscriptions) { - String nextSubscriptionId = nextActiveSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue(); - String nextCriteriaString = nextActiveSubscription.getCriteriaString(); + String nextSubscriptionId = getId(nextActiveSubscription); if (isNotBlank(theMsg.getSubscriptionId())) { if (!theMsg.getSubscriptionId().equals(nextSubscriptionId)) { @@ -97,29 +95,16 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { } } - if (StringUtils.isBlank(nextCriteriaString)) { + if (!validCriteria(nextActiveSubscription, resourceId)) { continue; } - // see if the criteria matches the created object - ourLog.trace("Checking subscription {} for {} with criteria {}", nextSubscriptionId, resourceType, nextCriteriaString); - String criteriaResource = nextCriteriaString; - int index = criteriaResource.indexOf("?"); - if (index != -1) { - criteriaResource = criteriaResource.substring(0, criteriaResource.indexOf("?")); - } - - if (resourceType != null && nextCriteriaString != null && !criteriaResource.equals(resourceType)) { - ourLog.trace("Skipping subscription search for {} because it does not match the criteria {}", resourceType, nextCriteriaString); - continue; - } - - SubscriptionMatchResult matchResult = mySubscriptionMatcher.match(nextCriteriaString, theMsg); + SubscriptionMatchResult matchResult = mySubscriptionMatcher.match(nextActiveSubscription.getSubscription(), theMsg); if (!matchResult.matched()) { continue; } - ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), id.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName()); + ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), resourceId.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName()); ResourceDeliveryMessage deliveryMsg = new ResourceDeliveryMessage(); deliveryMsg.setPayload(myFhirContext, theMsg.getNewPayload(myFhirContext)); @@ -136,4 +121,33 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { } } } + + private String getId(ActiveSubscription theActiveSubscription) { + return theActiveSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue(); + } + + private boolean validCriteria(ActiveSubscription theActiveSubscription, IIdType theResourceId) { + String criteriaString = theActiveSubscription.getCriteriaString(); + String subscriptionId = getId(theActiveSubscription); + String resourceType = theResourceId.getResourceType(); + + if (StringUtils.isBlank(criteriaString)) { + return false; + } + + // see if the criteria matches the created object + ourLog.trace("Checking subscription {} for {} with criteria {}", subscriptionId, resourceType, criteriaString); + String criteriaResource = criteriaString; + int index = criteriaResource.indexOf("?"); + if (index != -1) { + criteriaResource = criteriaResource.substring(0, criteriaResource.indexOf("?")); + } + + if (resourceType != null && criteriaString != null && !criteriaResource.equals(resourceType)) { + ourLog.trace("Skipping subscription search for {} because it does not match the criteria {}", resourceType, criteriaString); + return false; + } + + return true; + } } From b0cbd52ae923cdccdff9dc2ca8bbd4b18801cd3b Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sat, 19 Jan 2019 18:01:00 -0500 Subject: [PATCH 28/56] Interceptor cleanup --- .../java/ca/uhn/fhir/util/VersionUtil.java | 4 +- .../ca/uhn/fhir/jpa/config/BaseConfig.java | 14 ++- .../test/InterceptorRegistryTest.java | 91 +++++++------- .../RestHookWithInterceptorR4Test.java | 24 ++-- hapi-fhir-jpaserver-model/pom.xml | 5 + .../api/Hook.java} | 12 +- .../jpa/model/interceptor/api/HookParams.java | 56 +++++++++ .../interceptor/api/IAnonymousLambdaHook.java | 16 +++ .../interceptor/api/IInterceptorRegistry.java | 23 ++++ .../api/Interceptor.java} | 4 +- .../jpa/model/interceptor/api/Pointcut.java | 63 ++++++++++ .../executor/InterceptorRegistry.java} | 118 ++++++++++++++---- .../interceptor/api/Pointcut.java | 28 ----- .../interceptor/executor/HookParams.java | 48 ------- .../ISubscriptionInterceptorRegistry.java | 17 --- .../module/ResourceModifiedMessage.java | 10 +- ...scriptionDeliveringRestHookSubscriber.java | 14 +-- .../SubscriptionMatchingSubscriber.java | 17 ++- .../module/BaseSubscriptionTest.java | 11 ++ .../config/TestSubscriptionDstu3Config.java | 12 +- .../SubscriptionLoaderFhirClientTest.java | 17 ++- 21 files changed, 403 insertions(+), 201 deletions(-) rename hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistryTest.java => hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java (56%) rename hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/{subscription/interceptor/api/SubscriptionHook.java => interceptor/api/Hook.java} (58%) create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java rename hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/{subscription/interceptor/api/SubscriptionInterceptor.java => interceptor/api/Interceptor.java} (74%) create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java rename hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/{subscription/interceptor/executor/SubscriptionInterceptorRegistry.java => interceptor/executor/InterceptorRegistry.java} (52%) delete mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/Pointcut.java delete mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/HookParams.java delete mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/ISubscriptionInterceptorRegistry.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java index 9124fcf99dc..c7cb7ad7956 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java @@ -58,7 +58,9 @@ public class VersionUtil { try (InputStream is = VersionUtil.class.getResourceAsStream("/ca/uhn/fhir/hapi-fhir-base-build.properties")) { Properties p = new Properties(); - p.load(is); + if (is != null) { + p.load(is); + } ourVersion = p.getProperty("hapifhir.version"); ourVersion = defaultIfBlank(ourVersion, "(unknown)"); 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 5e6377848cb..28a55f6d5bb 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 @@ -42,9 +42,9 @@ import javax.annotation.Nonnull; * 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. @@ -57,10 +57,12 @@ import javax.annotation.Nonnull; @Configuration @EnableScheduling @EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data") -@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters={ - @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=BaseConfig.class), - @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value=WebSocketConfigurer.class), - @ComponentScan.Filter(type=FilterType.REGEX, pattern="ca.uhn.fhir.jpa.subscription.module.standalone.*")}) +@ComponentScan(basePackages = "ca.uhn.fhir.jpa", excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BaseConfig.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebSocketConfigurer.class), + @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*\\.test\\..*"), + @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"), + @ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.subscription.module.standalone.*")}) public abstract class BaseConfig implements SchedulingConfigurer { diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java similarity index 56% rename from hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistryTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java index 413eb8f1617..d1f6c0d9ead 100644 --- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java @@ -1,8 +1,10 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.executor; +package ca.uhn.fhir.jpa.interceptor.test; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.SubscriptionHook; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.SubscriptionInterceptor; +import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; +import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.model.interceptor.api.Hook; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -21,24 +23,25 @@ import static org.hamcrest.Matchers.contains; import static org.junit.Assert.*; @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {SubscriptionInterceptorRegistryTest.MyCtxConfig.class}) -public class SubscriptionInterceptorRegistryTest { +@ContextConfiguration(classes = {InterceptorRegistryTest.InterceptorRegistryTestCtxConfig.class}) +public class InterceptorRegistryTest { private static boolean ourNext_beforeRestHookDelivery_Return2; private static boolean ourNext_beforeRestHookDelivery_Return1; private static List<String> ourInvocations = new ArrayList<>(); private static CanonicalSubscription ourLastCanonicalSubscription; - private static ResourceDeliveryMessage ourLastResourceDeliveryMessage; + private static ResourceDeliveryMessage ourLastResourceDeliveryMessage0; + private static ResourceDeliveryMessage ourLastResourceDeliveryMessage1; @Autowired - private SubscriptionInterceptorRegistry mySubscriptionInterceptorRegistry; + private InterceptorRegistry myInterceptorRegistry; @Test public void testGlobalInterceptorsAreFound() { - List<Object> globalInterceptors = mySubscriptionInterceptorRegistry.getGlobalInterceptors(); + List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest(); assertEquals(2, globalInterceptors.size()); - assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyInterceptorOne); - assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyInterceptorTwo); + assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne); + assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo); } @Test @@ -46,11 +49,12 @@ public class SubscriptionInterceptorRegistryTest { ResourceDeliveryMessage msg = new ResourceDeliveryMessage(); CanonicalSubscription subs = new CanonicalSubscription(); HookParams params = new HookParams(msg, subs); - boolean outcome = mySubscriptionInterceptorRegistry.callHooks(Pointcut.BEFORE_REST_HOOK_DELIVERY, params); + boolean outcome = myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params); assertTrue(outcome); - assertThat(ourInvocations, contains("MyInterceptorOne.beforeRestHookDelivery", "MyInterceptorTwo.beforeRestHookDelivery")); - assertSame(msg, ourLastResourceDeliveryMessage); + assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery", "MyTestInterceptorTwo.beforeRestHookDelivery")); + assertSame(msg, ourLastResourceDeliveryMessage0); + assertNull(ourLastResourceDeliveryMessage1); assertSame(subs, ourLastCanonicalSubscription); } @@ -61,10 +65,10 @@ public class SubscriptionInterceptorRegistryTest { ResourceDeliveryMessage msg = new ResourceDeliveryMessage(); CanonicalSubscription subs = new CanonicalSubscription(); HookParams params = new HookParams(msg, subs); - boolean outcome = mySubscriptionInterceptorRegistry.callHooks(Pointcut.BEFORE_REST_HOOK_DELIVERY, params); + boolean outcome = myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params); assertFalse(outcome); - assertThat(ourInvocations, contains("MyInterceptorOne.beforeRestHookDelivery")); + assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery")); } @Test @@ -73,10 +77,10 @@ public class SubscriptionInterceptorRegistryTest { CanonicalSubscription subs = new CanonicalSubscription(); HookParams params = new HookParams(msg, subs); try { - mySubscriptionInterceptorRegistry.callHooks(Pointcut.BEFORE_REST_HOOK_DELIVERY, params); + myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params); fail(); } catch (AssertionError e) { - // good + assertEquals("Wrong hook parameters, wanted [CanonicalSubscription, ResourceDeliveryMessage] and found [CanonicalSubscription, Integer]", e.getMessage()); } } @@ -86,17 +90,22 @@ public class SubscriptionInterceptorRegistryTest { ourNext_beforeRestHookDelivery_Return1 = true; ourNext_beforeRestHookDelivery_Return2 = true; ourLastCanonicalSubscription = null; - ourLastResourceDeliveryMessage = null; + ourLastResourceDeliveryMessage0 = null; + ourLastResourceDeliveryMessage1 = null; ourInvocations.clear(); } @Configuration @ComponentScan(basePackages = "ca.uhn.fhir.jpa.model") - public static class MyCtxConfig { + static class InterceptorRegistryTestCtxConfig { + /** + * Note: Orders are deliberately reversed to make sure we get the orders right + * using the @Order annotation + */ @Bean - public SubscriptionInterceptorRegistry subscriptionInterceptorRegistry() { - return new SubscriptionInterceptorRegistry(); + public MyTestInterceptorTwo interceptor1() { + return new MyTestInterceptorTwo(); } /** @@ -104,41 +113,37 @@ public class SubscriptionInterceptorRegistryTest { * using the @Order annotation */ @Bean - public MyInterceptorTwo interceptor1() { - return new MyInterceptorTwo(); - } - - /** - * Note: Orders are deliberately reversed to make sure we get the orders right - * using the @Order annotation - */ - @Bean - public MyInterceptorOne interceptor2() { - return new MyInterceptorOne(); + public MyTestInterceptorOne interceptor2() { + return new MyTestInterceptorOne(); } } - @SubscriptionInterceptor + @Interceptor @Order(100) - public static class MyInterceptorOne { + public static class MyTestInterceptorOne { - @SubscriptionHook(Pointcut.BEFORE_REST_HOOK_DELIVERY) + public MyTestInterceptorOne() { + super(); + } + + @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) public boolean beforeRestHookDelivery(CanonicalSubscription theCanonicalSubscription) { ourLastCanonicalSubscription = theCanonicalSubscription; - ourInvocations.add("MyInterceptorOne.beforeRestHookDelivery"); + ourInvocations.add("MyTestInterceptorOne.beforeRestHookDelivery"); return ourNext_beforeRestHookDelivery_Return1; } } - @SubscriptionInterceptor + @Interceptor @Order(200) - public static class MyInterceptorTwo { - @SubscriptionHook(Pointcut.BEFORE_REST_HOOK_DELIVERY) - public boolean beforeRestHookDelivery(ResourceDeliveryMessage theResourceDeliveryMessage) { - ourLastResourceDeliveryMessage = theResourceDeliveryMessage; - ourInvocations.add("MyInterceptorTwo.beforeRestHookDelivery"); + public static class MyTestInterceptorTwo { + @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) + public boolean beforeRestHookDelivery(ResourceDeliveryMessage theResourceDeliveryMessage0, ResourceDeliveryMessage theResourceDeliveryMessage1) { + ourLastResourceDeliveryMessage0 = theResourceDeliveryMessage0; + ourLastResourceDeliveryMessage1 = theResourceDeliveryMessage1; + ourInvocations.add("MyTestInterceptorTwo.beforeRestHookDelivery"); return ourNext_beforeRestHookDelivery_Return2; } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java index bef8ce7df35..d64876b428d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java @@ -2,11 +2,11 @@ package ca.uhn.fhir.jpa.subscription.resthook; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.StoppableSubscriptionDeliveringRestHookSubscriber; +import ca.uhn.fhir.jpa.model.interceptor.api.Hook; +import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.SubscriptionHook; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.SubscriptionInterceptor; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; @@ -28,7 +28,7 @@ import static org.junit.Assert.*; /** * Test the rest-hook subscriptions */ -@ContextConfiguration(classes = {RestHookWithInterceptorR4Test.MyCtxConfig.class}) +@ContextConfiguration(classes = {RestHookWithInterceptorR4Test.MyTestCtxConfig.class}) public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { private static final Logger ourLog = LoggerFactory.getLogger(RestHookWithInterceptorR4Test.class); @@ -118,11 +118,11 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { } @Configuration - public static class MyCtxConfig { + static class MyTestCtxConfig { @Bean - public MyInterceptor interceptor() { - return new MyInterceptor(); + public MyTestInterceptor interceptor() { + return new MyTestInterceptor(); } } @@ -130,17 +130,17 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { /** * Interceptor class */ - @SubscriptionInterceptor - public static class MyInterceptor { + @Interceptor + public static class MyTestInterceptor { /** * Constructor */ - public MyInterceptor() { + public MyTestInterceptor() { ourLog.info("Creating interceptor"); } - @SubscriptionHook(Pointcut.BEFORE_REST_HOOK_DELIVERY) + @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) public boolean beforeRestHookDelivery(ResourceDeliveryMessage theDeliveryMessage, CanonicalSubscription theSubscription) { if (ourNextModifyResourceId) { theDeliveryMessage.getPayload(ourCtx).setId(new IdType("Observation/A")); @@ -153,7 +153,7 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { return ourNextBeforeRestHookDeliveryReturn; } - @SubscriptionHook(Pointcut.AFTER_REST_HOOK_DELIVERY) + @Hook(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY) public boolean afterRestHookDelivery(ResourceDeliveryMessage theDeliveryMessage, CanonicalSubscription theSubscription) { ourHitAfterRestHookDelivery = true; return ourNextAfterRestHookDeliveryReturn; diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index ca4362aa8ed..74e84bfa687 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -97,6 +97,11 @@ <artifactId>jscience</artifactId> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + </dependency> + <!-- Java --> <dependency> <groupId>javax.annotation</groupId> diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/SubscriptionHook.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java similarity index 58% rename from hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/SubscriptionHook.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java index 82b5e3ea9fa..364aedec949 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/SubscriptionHook.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.api; +package ca.uhn.fhir.jpa.model.interceptor.api; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -7,18 +7,18 @@ import java.lang.annotation.Target; /** * This annotation should be placed on - * {@link SubscriptionInterceptor Subscription Interceptor} + * {@link Interceptor Subscription Interceptor} * bean methods. * <p> - * Methods with this annotation are invoked immediately before a REST HOOK - * subscription delivery + * Methods with this annotation are invoked immediately before a REST HOOK + * subscription delivery * </p> * - * @see SubscriptionInterceptor + * @see Interceptor */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -public @interface SubscriptionHook { +public @interface Hook { /** * Provides the specific point where this method should be invoked diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java new file mode 100644 index 00000000000..517ef4a233f --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.jpa.model.interceptor.api; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +import java.util.List; +import java.util.stream.Collectors; + +public class HookParams { + + private ListMultimap<Class<?>, Object> myParams = ArrayListMultimap.create(); + + /** + * Constructor + */ + public HookParams() { + } + + /** + * Constructor + */ + public HookParams(Object... theParams) { + for (Object next : theParams) { + add(next); + } + } + + @SuppressWarnings("unchecked") + private <T> void add(T theNext) { + Class<T> nextClass = (Class<T>) theNext.getClass(); + add(nextClass, theNext); + } + + public <T> HookParams add(Class<T> theType, T theParam) { + myParams.put(theType, theParam); + return this; + } + + @SuppressWarnings("unchecked") + public <T> T get(Class<T> theParamType, int theIndex) { + List<T> objects = (List<T>) myParams.get(theParamType); + T retVal = null; + if (objects.size() > theIndex) { + retVal = objects.get(theIndex); + } + return retVal; + } + + /** + * Multivalued parameters will be returned twice in this list + */ + public List<String> getTypesAsSimpleName() { + return myParams.values().stream().map(t -> t.getClass().getSimpleName()).collect(Collectors.toList()); + } + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java new file mode 100644 index 00000000000..dbe80ae5960 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.jpa.model.interceptor.api; + +import com.google.common.annotations.VisibleForTesting; + +/** + * This is currently only here for unit tests! + * + * DO NOT USE IN NON-TEST CODE. Maybe this will change in the future? + */ +@FunctionalInterface +@VisibleForTesting +public interface IAnonymousLambdaHook { + + void invoke(HookParams theArgs); + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java new file mode 100644 index 00000000000..3bda9d09f93 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.jpa.model.interceptor.api; + +import com.google.common.annotations.VisibleForTesting; + +public interface IInterceptorRegistry { + + @VisibleForTesting + void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook); + + @VisibleForTesting + void clearAnonymousHookForUnitTest(); + + /** + * Invoke the interceptor methods + */ + boolean callHooks(Pointcut thePointcut, HookParams theParams); + + /** + * Invoke the interceptor methods + */ + boolean callHooks(Pointcut thePointcut, Object... theParams); + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/SubscriptionInterceptor.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java similarity index 74% rename from hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/SubscriptionInterceptor.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java index 3bb82d10e12..e5fe8cd5474 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/SubscriptionInterceptor.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.api; +package ca.uhn.fhir.jpa.model.interceptor.api; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -10,5 +10,5 @@ import java.lang.annotation.Target; */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -public @interface SubscriptionInterceptor { +public @interface Interceptor { } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java new file mode 100644 index 00000000000..3aa8a6e2a66 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.jpa.model.interceptor.api; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Value for {@link Hook#value()} + */ +public enum Pointcut { + + /** + * Invoked immediately after the delivery of a REST HOOK subscription. + * <p> + * When this hook is called, all processing is complete so this hook should not + * make any changes to the parameters. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription</li> + * <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li> + * </ul> + */ + SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY("CanonicalSubscription", "ResourceDeliveryMessage"), + + /** + * Invoked immediately before the delivery of a REST HOOK subscription. + * <p> + * Hooks may make changes to the delivery payload, or make changes to the + * canonical subscription such as adding headers, modifying the channel + * endpoint, etc. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription</li> + * <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li> + * </ul> + */ + SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY("CanonicalSubscription", "ResourceDeliveryMessage"), + + /** + * Invoked whenever a persisted resource (a resource that has just been stored in the + * database via a create/update/patch/etc.) has been checked for whether any subscriptions + * were triggered as a result of the operation + * Hooks may accept the following parameters: + * <ul> + * <li>ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage</li> + * </ul> + */ + SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage") + + ; + + private final List<String> myParameterTypes; + + Pointcut(String... theParameterTypes) { + myParameterTypes = Collections.unmodifiableList(Arrays.asList(theParameterTypes)); + } + + public List<String> getParameterTypes() { + return myParameterTypes; + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java similarity index 52% rename from hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistry.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index 36554c230ae..baaafc2aba2 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/SubscriptionInterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -1,10 +1,11 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.executor; +package ca.uhn.fhir.jpa.model.interceptor.executor; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.SubscriptionHook; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.SubscriptionInterceptor; +import ca.uhn.fhir.jpa.model.interceptor.api.*; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,27 +19,50 @@ import org.springframework.stereotype.Component; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.IdentityHashMap; -import java.util.List; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; @Component -public class SubscriptionInterceptorRegistry implements ISubscriptionInterceptorRegistry, ApplicationContextAware { - private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionInterceptorRegistry.class); +public class InterceptorRegistry implements IInterceptorRegistry, ApplicationContextAware { + private static final Logger ourLog = LoggerFactory.getLogger(InterceptorRegistry.class); private ApplicationContext myAppCtx; private List<Object> myGlobalInterceptors = new ArrayList<>(); - private ListMultimap<Pointcut, Invoker> myInvokers = ArrayListMultimap.create(); + private ListMultimap<Pointcut, BaseInvoker> myInvokers = ArrayListMultimap.create(); + private ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - List<Object> getGlobalInterceptors() { + /** + * Constructor + */ + public InterceptorRegistry() { + super(); + } + + @VisibleForTesting + public List<Object> getGlobalInterceptorsForUnitTest() { return myGlobalInterceptors; } + + @Override + @VisibleForTesting + public void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook) { + Validate.notNull(thePointcut); + Validate.notNull(theHook); + + myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(theHook)); + } + + @Override + @VisibleForTesting + public void clearAnonymousHookForUnitTest() { + myAnonymousInvokers.clear(); + } + @PostConstruct public void start() { // Grab the global interceptors - String[] globalInterceptorNames = myAppCtx.getBeanNamesForAnnotation(SubscriptionInterceptor.class); + String[] globalInterceptorNames = myAppCtx.getBeanNamesForAnnotation(Interceptor.class); for (String nextName : globalInterceptorNames) { Object nextGlobalInterceptor = myAppCtx.getBean(nextName); myGlobalInterceptors.add(nextGlobalInterceptor); @@ -50,9 +74,9 @@ public class SubscriptionInterceptorRegistry implements ISubscriptionInterceptor // Pull out the hook methods for (Object nextInterceptor : myGlobalInterceptors) { for (Method nextMethod : nextInterceptor.getClass().getDeclaredMethods()) { - SubscriptionHook hook = AnnotationUtils.findAnnotation(nextMethod, SubscriptionHook.class); + Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); if (hook != null) { - Invoker invoker = new Invoker(nextInterceptor, nextMethod); + HookInvoker invoker = new HookInvoker(hook, nextInterceptor, nextMethod); for (Pointcut nextPointcut : hook.value()) { myInvokers.put(nextPointcut, invoker); } @@ -84,12 +108,17 @@ public class SubscriptionInterceptorRegistry implements ISubscriptionInterceptor @Override public boolean callHooks(Pointcut thePointcut, HookParams theParams) { + assert haveAppropriateParams(thePointcut, theParams); + + // Anonymous hooks first + List<BaseInvoker> invokers = ListUtils.union( + myAnonymousInvokers.get(thePointcut), + myInvokers.get(thePointcut)); /* * Call each hook in order */ - List<Invoker> invokers = myInvokers.get(thePointcut); - for (Invoker nextInvoker : invokers) { + for (BaseInvoker nextInvoker : invokers) { boolean shouldContinue = nextInvoker.invoke(theParams); if (!shouldContinue) { return false; @@ -99,22 +128,55 @@ public class SubscriptionInterceptorRegistry implements ISubscriptionInterceptor return true; } + /** + * Only call this when assertions are enabled, it's expensive + */ + private boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) { + List<String> givenTypes = theParams.getTypesAsSimpleName(); + List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); + givenTypes.sort(Comparator.naturalOrder()); + wantedTypes.sort(Comparator.naturalOrder()); + if (!givenTypes.equals(wantedTypes)) { + throw new AssertionError("Wrong hook parameters, wanted " + wantedTypes + " and found " + givenTypes); + } + return true; + } + @Override public boolean callHooks(Pointcut thePointcut, Object... theParams) { return callHooks(thePointcut, new HookParams(theParams)); } - private class Invoker { + private abstract class BaseInvoker { + abstract boolean invoke(HookParams theParams); + } + + private class AnonymousLambdaInvoker extends BaseInvoker { + private final IAnonymousLambdaHook myHook; + + public AnonymousLambdaInvoker(IAnonymousLambdaHook theHook) { + myHook = theHook; + } + + @Override + boolean invoke(HookParams theParams) { + myHook.invoke(theParams); + return true; + } + } + + private class HookInvoker extends BaseInvoker { private final Object myInterceptor; private final boolean myReturnsBoolean; private final Method myMethod; private final Class<?>[] myParameterTypes; + private final int[] myParameterIndexes; /** * Constructor */ - private Invoker(@Nonnull Object theInterceptor, @Nonnull Method theHookMethod) { + private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod) { myInterceptor = theInterceptor; myParameterTypes = theHookMethod.getParameterTypes(); myMethod = theHookMethod; @@ -126,13 +188,26 @@ public class SubscriptionInterceptorRegistry implements ISubscriptionInterceptor Validate.isTrue(Void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); myReturnsBoolean = false; } + + myParameterIndexes = new int[myParameterTypes.length]; + Map<Class<?>, AtomicInteger> typeToCount = new HashMap<>(); + for (int i = 0; i < myParameterTypes.length; i++) { + AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0)); + myParameterIndexes[i] = counter.getAndIncrement(); + } } + /** + * @return Returns true/false if the hook method returns a boolean, returns true otherwise + */ + @Override boolean invoke(HookParams theParams) { + Object[] args = new Object[myParameterTypes.length]; for (int i = 0; i < myParameterTypes.length; i++) { Class<?> nextParamType = myParameterTypes[i]; - Object nextParamValue = theParams.get(nextParamType); + int nextParamIndex = myParameterIndexes[i]; + Object nextParamValue = theParams.get(nextParamType, nextParamIndex); args[i] = nextParamValue; } @@ -153,7 +228,4 @@ public class SubscriptionInterceptorRegistry implements ISubscriptionInterceptor } - private static <T> boolean equals(Collection<T> theLhs, Collection<T> theRhs) { - return theLhs.size() == theRhs.size() && theLhs.containsAll(theRhs) && theRhs.containsAll(theLhs); - } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/Pointcut.java deleted file mode 100644 index 46be9fad070..00000000000 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/api/Pointcut.java +++ /dev/null @@ -1,28 +0,0 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.api; - -/** - * Value for {@link SubscriptionHook#value()} - */ -public enum Pointcut { - - /** - * Invoked immediately after the delivery of a REST HOOK subscription. - * <p> - * When this hook is called, all processing is complete so this hook should not - * make any changes to the parameters. - * </p> - */ - AFTER_REST_HOOK_DELIVERY, - - /** - * Invoked immediately before the delivery of a REST HOOK subscription. - * <p> - * Hooks may make changes to the delivery payload, or make changes to the - * canonical subscription such as adding headers, modifying the channel - * endpoint, etc. - * </p> - */ - BEFORE_REST_HOOK_DELIVERY; - - -} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/HookParams.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/HookParams.java deleted file mode 100644 index 5f517ed3320..00000000000 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/HookParams.java +++ /dev/null @@ -1,48 +0,0 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.executor; - -import org.apache.commons.lang3.Validate; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class HookParams { - - private Map<Class<?>, Object> myParams = new HashMap<>(); - - /** - * Constructor - */ - public HookParams() { - } - - /** - * Constructor - */ - public HookParams(Object... theParams) { - for (Object next : theParams) { - add(next); - } - } - - @SuppressWarnings("unchecked") - private <T> void add(T theNext) { - Class<T> nextClass = (Class<T>) theNext.getClass(); - add(nextClass, theNext); - } - - public <T> HookParams add(Class<T> theType, T theParam) { - Validate.isTrue(myParams.containsKey(theType) == false, "Already have param of type %s", theType); - myParams.put(theType, theParam); - return this; - } - - @SuppressWarnings("unchecked") - public <T> T get(Class<T> theParamType) { - return (T) myParams.get(theParamType); - } - - Set<Class<?>> getTypes() { - return myParams.keySet(); - } -} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/ISubscriptionInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/ISubscriptionInterceptorRegistry.java deleted file mode 100644 index 02e1e797aa5..00000000000 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/subscription/interceptor/executor/ISubscriptionInterceptorRegistry.java +++ /dev/null @@ -1,17 +0,0 @@ -package ca.uhn.fhir.jpa.model.subscription.interceptor.executor; - -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.Pointcut; - -public interface ISubscriptionInterceptorRegistry { - - /** - * Invoke the interceptor methods - */ - boolean callHooks(Pointcut thePointcut, HookParams theParams); - - /** - * Invoke the interceptor methods - */ - boolean callHooks(Pointcut thePointcut, Object... theParams); - -} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java index 4edba56626a..86283119f7f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module; * 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. @@ -52,11 +52,15 @@ public class ResourceModifiedMessage implements IResourceMessage { @JsonIgnore private transient IBaseResource myPayloadDecoded; - // For JSON + /** + * Constructor + */ public ResourceModifiedMessage() { + super(); } public ResourceModifiedMessage(FhirContext theFhirContext, IBaseResource theResource, OperationTypeEnum theOperationType) { + this(); setId(theResource.getIdElement()); setOperationType(theOperationType); if (theOperationType != OperationTypeEnum.DELETE) { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index cfeed058456..536e1172a02 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -21,8 +21,8 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; */ import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; -import ca.uhn.fhir.jpa.model.subscription.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.model.subscription.interceptor.executor.ISubscriptionInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.*; @@ -52,7 +52,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe IResourceRetriever myResourceRetriever; private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); @Autowired - private ISubscriptionInterceptorRegistry mySubscriptionInterceptorRegistry; + private IInterceptorRegistry myInterceptorRegistry; protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) { IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription); @@ -132,8 +132,8 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe public void handleMessage(ResourceDeliveryMessage theMessage) throws MessagingException { CanonicalSubscription subscription = theMessage.getSubscription(); - // Interceptor call: BEFORE_REST_HOOK_DELIVERY - if (!mySubscriptionInterceptorRegistry.callHooks(Pointcut.BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) { + // Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY + if (!myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) { return; } @@ -168,8 +168,8 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe deliverPayload(theMessage, subscription, payloadType, client); - // Interceptor call: AFTER_REST_HOOK_DELIVERY - if (!mySubscriptionInterceptorRegistry.callHooks(Pointcut.AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) { + // Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY + if (!myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) { //noinspection UnnecessaryReturnStatement return; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index d5df31109b5..d5cc437e8a2 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -1,6 +1,8 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; @@ -31,9 +33,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -52,6 +54,8 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { private FhirContext myFhirContext; @Autowired private SubscriptionRegistry mySubscriptionRegistry; + @Autowired + private IInterceptorRegistry myInterceptorRegistry; @Override public void handleMessage(Message<?> theMessage) throws MessagingException { @@ -64,9 +68,18 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload(); matchActiveSubscriptionsAndDeliver(msg); + } public void matchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { + try { + doMatchActiveSubscriptionsAndDeliver(theMsg); + } finally { + myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, theMsg); + } + } + + private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { switch (theMsg.getOperationType()) { case CREATE: case UPDATE: diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java index 1d2dac2d498..9a1996a127e 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/BaseSubscriptionTest.java @@ -1,10 +1,12 @@ package ca.uhn.fhir.jpa.subscription.module; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader; import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSearchParamProvider; import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSubscriptionProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import org.junit.After; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -24,6 +26,15 @@ public abstract class BaseSubscriptionTest { @Autowired SubscriptionLoader mySubscriptionLoader; + @Autowired + protected + IInterceptorRegistry myInterceptorRegistry; + + @After + public void afterClearAnonymousLambdas() { + myInterceptorRegistry.clearAnonymousHookForUnitTest(); + } + public void initSearchParamRegistry(IBundleProvider theBundleProvider) { myMockFhirClientSearchParamProvider.setBundleProvider(theBundleProvider); mySearchParamRegistry.forceRefresh(); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java index 883fa7c5233..3605ae2c30f 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.jpa.subscription.module.config; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider; import org.springframework.context.annotation.Bean; @@ -18,5 +20,13 @@ public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config { @Bean @Primary - public ISubscriptionProvider subsriptionProvider() { return new MockFhirClientSubscriptionProvider();} + public ISubscriptionProvider subsriptionProvider() { + return new MockFhirClientSubscriptionProvider(); + } + + @Bean + public IInterceptorRegistry interceptorRegistry() { + return new InterceptorRegistry(); + } + } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java index b6c2ae73037..3d57b5e9011 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/SubscriptionLoaderFhirClientTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.subscription.module.standalone; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider; @@ -8,12 +9,19 @@ import org.junit.Test; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class SubscriptionLoaderFhirClientTest extends BaseBlockingQueueSubscribableChannelDstu3Test { @Test - public void testSubscriptionLoaderFhirClient() throws Exception { + public void testSubscriptionLoaderFhirClient() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, t-> latch.countDown()); + String payload = "application/fhir+json"; String criteria1 = "Observation?code=SNOMED-CT|" + myCode + "&_format=xml"; @@ -27,6 +35,7 @@ public class SubscriptionLoaderFhirClientTest extends BaseBlockingQueueSubscriba initSubscriptionLoader(bundle); sendObservation(myCode, "SNOMED-CT"); + latch.await(10, TimeUnit.SECONDS); waitForSize(0, ourCreatedObservations); waitForSize(1, ourUpdatedObservations); @@ -34,7 +43,10 @@ public class SubscriptionLoaderFhirClientTest extends BaseBlockingQueueSubscriba } @Test - public void testSubscriptionLoaderFhirClientSubscriptionNotActive() throws Exception { + public void testSubscriptionLoaderFhirClientSubscriptionNotActive() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, t-> latch.countDown()); + String payload = "application/fhir+json"; String criteria1 = "Observation?code=SNOMED-CT|" + myCode + "&_format=xml"; @@ -48,6 +60,7 @@ public class SubscriptionLoaderFhirClientTest extends BaseBlockingQueueSubscriba initSubscriptionLoader(bundle); sendObservation(myCode, "SNOMED-CT"); + latch.await(10, TimeUnit.SECONDS); waitForSize(0, ourCreatedObservations); waitForSize(0, ourUpdatedObservations); From cd1e0e881cdc1c5acede548cbd633e3f0a42bf43 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sat, 19 Jan 2019 18:31:17 -0500 Subject: [PATCH 29/56] Add headers --- .../java/ca/uhn/fhir/util/VersionUtil.java | 4 ++-- .../ca/uhn/fhir/jpa/config/BaseConfig.java | 4 ++-- .../IResourceModifiedConsumer.java | 20 +++++++++++++++++++ .../fhir/jpa/model/interceptor/api/Hook.java | 20 +++++++++++++++++++ .../jpa/model/interceptor/api/HookParams.java | 20 +++++++++++++++++++ .../interceptor/api/IAnonymousLambdaHook.java | 20 +++++++++++++++++++ .../interceptor/api/IInterceptorRegistry.java | 20 +++++++++++++++++++ .../model/interceptor/api/Interceptor.java | 20 +++++++++++++++++++ .../jpa/model/interceptor/api/Pointcut.java | 20 +++++++++++++++++++ .../executor/InterceptorRegistry.java | 20 +++++++++++++++++++ .../module/CanonicalSubscription.java | 4 ++-- .../module/ResourceModifiedMessage.java | 4 ++-- .../subscriber/ResourceDeliveryMessage.java | 4 ++-- ...scriptionDeliveringRestHookSubscriber.java | 4 ++-- .../SubscriptionMatchingSubscriber.java | 4 ++-- .../auth/IAuthRuleBuilderRuleOp.java | 4 ++-- .../server/interceptor/auth/RuleBuilder.java | 4 ++-- .../server/interceptor/auth/RuleImplOp.java | 4 ++-- 18 files changed, 180 insertions(+), 20 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java index c7cb7ad7956..c2208fd3c60 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionUtil.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.util; * 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. 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 28a55f6d5bb..4880c08c8c9 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 @@ -42,9 +42,9 @@ import javax.annotation.Nonnull; * 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. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java index 0c2171c2eb3..20c31732dbd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.subscription; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2019 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.jpa.subscription.module.ResourceModifiedMessage; public interface IResourceModifiedConsumer { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java index 364aedec949..ea89c412852 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.api; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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 java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java index 517ef4a233f..5c97f1ae081 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.api; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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 com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java index dbe80ae5960..5ac153fe0ab 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.api; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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 com.google.common.annotations.VisibleForTesting; /** diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java index 3bda9d09f93..f23efbfc7bb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.api; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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 com.google.common.annotations.VisibleForTesting; public interface IInterceptorRegistry { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java index e5fe8cd5474..669e6833e82 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.api; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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 java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java index 3aa8a6e2a66..462d864c454 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.api; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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 java.util.Arrays; import java.util.Collections; import java.util.List; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index baaafc2aba2..818b44b3d6d 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.model.interceptor.executor; +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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.jpa.model.interceptor.api.*; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java index 7fa84858557..f240b810453 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java index 86283119f7f..42a2122b403 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java index fa660020f6d..f19c577b5d2 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/ResourceDeliveryMessage.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index 536e1172a02..3358e8ab0cd 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index d5cc437e8a2..20a64112d0a 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -33,9 +33,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java index 70aae37c005..bd5dae2f5fc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java @@ -13,9 +13,9 @@ import java.util.Collection; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 1d205c0613c..8988b94dd82 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index d91ec3bdc80..e536f67b2c0 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -37,9 +37,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. From b878925884213b43287baf4a6fcef1a2e42472cf Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sat, 19 Jan 2019 18:44:09 -0500 Subject: [PATCH 30/56] Try to avoid intermittent test failure --- .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 15 +++++++++++++++ .../resthook/RestHookWithInterceptorR4Test.java | 13 ++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index eb040fcba84..3ef8a954096 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -52,6 +52,7 @@ import java.sql.SQLException; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.stream.Collectors; import static ca.uhn.fhir.util.TestUtil.randomizeLocale; @@ -431,6 +432,20 @@ public abstract class BaseJpaTest { } } + public static void waitForTrue(Supplier<Boolean> theList) { + StopWatch sw = new StopWatch(); + while (!theList.get() && sw.getMillis() <= 16000) { + try { + Thread.sleep(50); + } catch (InterruptedException theE) { + throw new Error(theE); + } + } + if (sw.getMillis() >= 16000) { + fail("Waited " + sw.toString() + " and is still false"); + } + } + public static void waitForSize(int theTarget, Callable<Number> theCallable) throws Exception { waitForSize(theTarget, 10000, theCallable); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java index d64876b428d..202c715e472 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java @@ -23,7 +23,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; /** * Test the rest-hook subscriptions @@ -74,8 +75,9 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { waitForSize(1, ourUpdatedObservations); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); assertEquals("Observation/A", ourUpdatedObservations.get(0).getId()); - assertTrue(ourHitBeforeRestHookDelivery); - assertTrue(ourHitAfterRestHookDelivery); + // TODO: JA a latch would be even better but we'd need to allow customizable orders since the ad-hoc ones run first + waitForTrue(() -> ourHitBeforeRestHookDelivery); + waitForTrue(() -> ourHitAfterRestHookDelivery); } @Test @@ -90,8 +92,9 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { waitForSize(0, ourCreatedObservations); waitForSize(1, ourUpdatedObservations); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); - assertTrue(ourHitBeforeRestHookDelivery); - assertTrue(ourHitAfterRestHookDelivery); + // TODO: JA a latch would be even better but we'd need to allow customizable orders since the ad-hoc ones run first + waitForTrue(() -> ourHitBeforeRestHookDelivery); + waitForTrue(() -> ourHitAfterRestHookDelivery); assertThat(ourHeaders, hasItem("X-Foo: Bar")); } From b3bdbea19c61fc068be356cb529c4352f6f5b726 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Sun, 20 Jan 2019 09:39:34 -0500 Subject: [PATCH 31/56] Merge branch 'master' into windows-fixes quickly update tests to new interceptor --- .../jpa/model/interceptor/api/Pointcut.java | 7 ++++-- .../module/cache/SubscriptionRegistry.java | 6 +++-- .../module/config/BaseSubscriptionConfig.java | 2 +- .../subscription/module/LatchedService.java | 24 +++++++++++-------- ...kingQueueSubscribableChannelDstu3Test.java | 14 +++++------ .../SubscriptionMatchingSubscriberTest.java | 2 -- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java index 462d864c454..1a251114b51 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -67,9 +67,12 @@ public enum Pointcut { * <li>ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage</li> * </ul> */ - SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage") + SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage"), - ; + // FIXME KHS + SUBSCRIPTION_AFTER_SUBSCRIPTION_MATCHING("ResourceModifiedMessage"), + // FIXME KHS + SUBSCRIPTION_AFTER_SUBSCRIPTION_ACTIVATED("CanonicalSubscription"); private final List<String> myParameterTypes; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index f746995bd8f..72d9249b4ce 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -21,7 +21,8 @@ package ca.uhn.fhir.jpa.subscription.module.cache; */ import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -101,7 +102,8 @@ public class SubscriptionRegistry { deliveryHandler.ifPresent(activeSubscription::register); myActiveSubscriptionCache.put(subscriptionId, activeSubscription); - myInterceptorRegistry.trigger(INTERCEPTOR_POST_ACTIVATED, theSubscription); + + myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_ACTIVATED, canonicalized); return canonicalized; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java index b484886ab18..a89870c7b0e 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java @@ -30,7 +30,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @Configuration @EnableScheduling -@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.searchparam", "ca.uhn.fhir.jpa.subscription.module"}) +@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.searchparam", "ca.uhn.fhir.jpa.subscription.module", "ca.uhn.fhir.jpa.model.interceptor.executor"}) public abstract class BaseSubscriptionConfig { public abstract FhirContext fhirContext(); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java index 359621338c9..cca0c509d4b 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java @@ -1,5 +1,8 @@ package ca.uhn.fhir.jpa.subscription.module; +import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; +import ca.uhn.fhir.jpa.model.interceptor.api.IAnonymousLambdaHook; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,24 +11,26 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -public class LatchedService implements Predicate<Object> { +public class LatchedService implements IAnonymousLambdaHook { private static final Logger ourLog = LoggerFactory.getLogger(LatchedService.class); private final String name; private CountDownLatch myCountdownLatch; private AtomicReference<String> myFailure; - private AtomicReference<List<Object>> myCalledWith; + private AtomicReference<List<HookParams>> myCalledWith; - public LatchedService(String name) { - this.name = name; + public LatchedService(Pointcut thePointcut) { + this.name = thePointcut.name(); + } + + public LatchedService(String theName) { + this.name = theName; } public void countdown() { @@ -49,7 +54,7 @@ public class LatchedService implements Predicate<Object> { } public void awaitExpectedWithTimeout(int timeoutSecond) throws InterruptedException { - assertTrue(name+" latch timed out waiting "+timeoutSecond+" seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); + assertTrue(name +" latch timed out waiting "+timeoutSecond+" seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); if (myFailure.get() != null) { String error = myFailure.get(); @@ -59,11 +64,10 @@ public class LatchedService implements Predicate<Object> { } @Override - public boolean test(Object object) { + public void invoke(HookParams theArgs) { this.countdown(); if (myCalledWith.get() != null) { - myCalledWith.get().add(object); + myCalledWith.get().add(theArgs); } - return true; } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index b061ece857e..88a16c83ee6 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -1,7 +1,8 @@ package ca.uhn.fhir.jpa.subscription.module.standalone; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; import ca.uhn.fhir.jpa.subscription.module.LatchedService; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; @@ -64,8 +65,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base private static SubscribableChannel ourSubscribableChannel; private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); private long idCounter = 0; - protected LatchedService mySubscriptionMatchingPost = new LatchedService(SubscriptionMatchingSubscriber.INTERCEPTOR_POST_PROCESSED); - protected LatchedService mySubscriptionActivatedPost = new LatchedService(SubscriptionRegistry.INTERCEPTOR_POST_ACTIVATED); + protected LatchedService mySubscriptionMatchingPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_MATCHING); + protected LatchedService mySubscriptionActivatedPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_ACTIVATED); @Before public void beforeReset() { @@ -76,14 +77,13 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base ourSubscribableChannel = mySubscriptionChannelFactory.newDeliveryChannel("test", Subscription.SubscriptionChannelType.RESTHOOK.toCode().toLowerCase()); ourSubscribableChannel.subscribe(myStandaloneSubscriptionMessageHandler); } - myInterceptorRegistry.addInterceptor(SubscriptionMatchingSubscriber.INTERCEPTOR_POST_PROCESSED, mySubscriptionMatchingPost); - myInterceptorRegistry.addInterceptor(SubscriptionRegistry.INTERCEPTOR_POST_ACTIVATED, mySubscriptionActivatedPost); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_MATCHING, mySubscriptionMatchingPost); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_ACTIVATED, mySubscriptionActivatedPost); } @After public void cleanup() { - myInterceptorRegistry.removeInterceptor(SubscriptionRegistry.INTERCEPTOR_POST_ACTIVATED, mySubscriptionActivatedPost); - myInterceptorRegistry.removeInterceptor(SubscriptionMatchingSubscriber.INTERCEPTOR_POST_PROCESSED, mySubscriptionMatchingPost); + myInterceptorRegistry.clearAnonymousHookForUnitTest(); } public <T extends IBaseResource> T sendResource(T theResource) { diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java index 5eb8e73db12..0bd4ac035c1 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java @@ -1,12 +1,10 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; -import ca.uhn.fhir.jpa.searchparam.interceptor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.standalone.BaseBlockingQueueSubscribableChannelDstu3Test; import ca.uhn.fhir.rest.api.Constants; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import static org.junit.Assert.assertEquals; From c3c7d156e9017a1b9395dc50c404ba5d95ef95c9 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sun, 20 Jan 2019 10:16:18 -0500 Subject: [PATCH 32/56] Interceptor cleanup --- .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 11 +++ .../RestHookWithInterceptorR4Test.java | 43 +++++++---- .../interceptor/api/IInterceptorRegistry.java | 5 ++ .../jpa/model/interceptor/api/Pointcut.java | 18 ++++- .../executor/InterceptorRegistry.java | 71 +++++++++++++++---- .../module/cache/SubscriptionRegistry.java | 16 +++-- 6 files changed, 127 insertions(+), 37 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 3ef8a954096..dacf263e0ac 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -2,6 +2,8 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.entity.TermConcept; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; @@ -51,6 +53,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.*; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -85,6 +88,8 @@ public abstract class BaseJpaTest { protected IRequestOperationCallback myRequestOperationCallback = mock(IRequestOperationCallback.class); @Autowired protected DatabaseBackedPagingProvider myDatabaseBackedPagingProvider; + @Autowired + protected IInterceptorRegistry myInterceptorRegistry; @After public void afterPerformCleanup() { @@ -129,6 +134,12 @@ public abstract class BaseJpaTest { when(mySrd.getHeaders(eq(JpaConstants.HEADER_META_SNAPSHOT_MODE))).thenReturn(new ArrayList<>()); } + protected CountDownLatch registerLatchHookInterceptor(int theCount, Pointcut theLatchPointcut) { + CountDownLatch deliveryLatch = new CountDownLatch(theCount); + myInterceptorRegistry.registerAnonymousHookForUnitTest(theLatchPointcut, Integer.MAX_VALUE, t -> deliveryLatch.countDown()); + return deliveryLatch; + } + protected abstract FhirContext getContext(); protected abstract PlatformTransactionManager getTxManager(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java index 202c715e472..4382cf5c80f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.subscription.resthook; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.config.StoppableSubscriptionDeliveringRestHookSubscriber; import ca.uhn.fhir.jpa.model.interceptor.api.Hook; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test; @@ -22,9 +23,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import static org.hamcrest.Matchers.hasItem; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; /** * Test the rest-hook subscriptions @@ -66,35 +69,43 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { public void testBeforeRestHookDelivery_ModifyResourceId() throws Exception { ourNextModifyResourceId = true; + // Create a subscription + CountDownLatch registerLatch = registerLatchHookInterceptor(1, Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); createSubscription("Observation?status=final", "application/fhir+json"); - waitForActivatedSubscriptionCount(1); + registerLatch.await(10, TimeUnit.SECONDS); + // Creating a matching resource + CountDownLatch deliveryLatch = registerLatchHookInterceptor(1, Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY); sendObservation(); + deliveryLatch.await(10, TimeUnit.SECONDS); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); + assertEquals(0, ourCreatedObservations.size()); + assertEquals(1, ourUpdatedObservations.size()); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); assertEquals("Observation/A", ourUpdatedObservations.get(0).getId()); - // TODO: JA a latch would be even better but we'd need to allow customizable orders since the ad-hoc ones run first - waitForTrue(() -> ourHitBeforeRestHookDelivery); - waitForTrue(() -> ourHitAfterRestHookDelivery); + assertTrue(ourHitBeforeRestHookDelivery); + assertTrue(ourHitAfterRestHookDelivery); } @Test public void testBeforeRestHookDelivery_AddHeader() throws Exception { ourNextAddHeader = true; + // Create a subscription + CountDownLatch registerLatch = registerLatchHookInterceptor(1, Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); createSubscription("Observation?status=final", "application/fhir+json"); - waitForActivatedSubscriptionCount(1); + registerLatch.await(10, TimeUnit.SECONDS); + // Creating a matching resource + CountDownLatch deliveryLatch = registerLatchHookInterceptor(1, Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY); sendObservation(); + deliveryLatch.await(10, TimeUnit.SECONDS); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); + assertEquals(0, ourCreatedObservations.size()); + assertEquals(1, ourUpdatedObservations.size()); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); - // TODO: JA a latch would be even better but we'd need to allow customizable orders since the ad-hoc ones run first - waitForTrue(() -> ourHitBeforeRestHookDelivery); - waitForTrue(() -> ourHitAfterRestHookDelivery); + assertTrue(ourHitBeforeRestHookDelivery); + assertTrue(ourHitAfterRestHookDelivery); assertThat(ourHeaders, hasItem("X-Foo: Bar")); } @@ -103,8 +114,10 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { public void testBeforeRestHookDelivery_AbortDelivery() throws Exception { ourNextBeforeRestHookDeliveryReturn = false; + // Create a subscription + CountDownLatch registerLatch = registerLatchHookInterceptor(1, Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); createSubscription("Observation?status=final", "application/fhir+json"); - waitForActivatedSubscriptionCount(1); + registerLatch.await(10, TimeUnit.SECONDS); sendObservation(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java index f23efbfc7bb..3c914295797 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -24,9 +24,14 @@ import com.google.common.annotations.VisibleForTesting; public interface IInterceptorRegistry { + int DEFAULT_ORDER = 0; + @VisibleForTesting void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook); + @VisibleForTesting + void registerAnonymousHookForUnitTest(Pointcut thePointcut, int theOrder, IAnonymousLambdaHook theHook); + @VisibleForTesting void clearAnonymousHookForUnitTest(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java index 462d864c454..979d4331c02 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -67,9 +67,23 @@ public enum Pointcut { * <li>ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage</li> * </ul> */ - SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage") + SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage"), - ; + + /** + * Invoked immediately after an active subscription is "registered". In HAPI FHIR, when + * a subscription + * <p> + * Hooks may make changes to the canonicalized subscription and this will have an effect + * on processing across this server. Note however that timing issues may occur, since the + * subscription is already technically live by the time this hook is called. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription</li> + * </ul> + */ + SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("CanonicalSubscription"); private final List<String> myParameterTypes; diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index 818b44b3d6d..1e4d1f53c65 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.executor; * 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. @@ -66,10 +66,15 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon @Override @VisibleForTesting public void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook) { + registerAnonymousHookForUnitTest(thePointcut, DEFAULT_ORDER, theHook); + } + + @Override + public void registerAnonymousHookForUnitTest(Pointcut thePointcut, int theOrder, IAnonymousLambdaHook theHook) { Validate.notNull(thePointcut); Validate.notNull(theHook); - myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(theHook)); + myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(theHook, theOrder)); } @Override @@ -88,15 +93,27 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon myGlobalInterceptors.add(nextGlobalInterceptor); } - // Sort them - sortByOrderAnnotation(myGlobalInterceptors); - // Pull out the hook methods for (Object nextInterceptor : myGlobalInterceptors) { + + int typeOrder = DEFAULT_ORDER; + Order typeOrderAnnotation = AnnotationUtils.findAnnotation(nextInterceptor.getClass(), Order.class); + if (typeOrderAnnotation != null) { + typeOrder = typeOrderAnnotation.value(); + } + for (Method nextMethod : nextInterceptor.getClass().getDeclaredMethods()) { Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); + if (hook != null) { - HookInvoker invoker = new HookInvoker(hook, nextInterceptor, nextMethod); + + int methodOrder = typeOrder; + Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class); + if (methodOrderAnnotation != null) { + methodOrder = methodOrderAnnotation.value(); + } + + HookInvoker invoker = new HookInvoker(hook, nextInterceptor, nextMethod, methodOrder); for (Pointcut nextPointcut : hook.value()) { myInvokers.put(nextPointcut, invoker); } @@ -104,6 +121,12 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon } } + // Sort everything by declared order + sortByOrderAnnotation(myGlobalInterceptors); + for (Pointcut nextPointcut : myInvokers.keys()) { + List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut); + nextInvokerList.sort(Comparator.naturalOrder()); + } } private void sortByOrderAnnotation(List<Object> theObjects) { @@ -130,10 +153,16 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon public boolean callHooks(Pointcut thePointcut, HookParams theParams) { assert haveAppropriateParams(thePointcut, theParams); - // Anonymous hooks first - List<BaseInvoker> invokers = ListUtils.union( - myAnonymousInvokers.get(thePointcut), - myInvokers.get(thePointcut)); + List<BaseInvoker> globalInvokers = myInvokers.get(thePointcut); + List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut); + + List<BaseInvoker> invokers = globalInvokers; + if (anonymousInvokers.isEmpty() == false) { + invokers = ListUtils.union( + anonymousInvokers, + globalInvokers); + invokers.sort(Comparator.naturalOrder()); + } /* * Call each hook in order @@ -167,14 +196,27 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon return callHooks(thePointcut, new HookParams(theParams)); } - private abstract class BaseInvoker { + private abstract class BaseInvoker implements Comparable<BaseInvoker> { + + private final int myOrder; + + protected BaseInvoker(int theOrder) { + myOrder = theOrder; + } + abstract boolean invoke(HookParams theParams); + + @Override + public int compareTo(BaseInvoker o) { + return myOrder - o.myOrder; + } } private class AnonymousLambdaInvoker extends BaseInvoker { private final IAnonymousLambdaHook myHook; - public AnonymousLambdaInvoker(IAnonymousLambdaHook theHook) { + public AnonymousLambdaInvoker(IAnonymousLambdaHook theHook, int theOrder) { + super(theOrder); myHook = theHook; } @@ -196,7 +238,8 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon /** * Constructor */ - private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod) { + private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { + super(theOrder); myInterceptor = theInterceptor; myParameterTypes = theHookMethod.getParameterTypes(); myMethod = theHookMethod; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index 363a7bcea9b..b18b4a3627e 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * 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. @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.subscription.module.cache; */ import ca.uhn.fhir.jpa.model.entity.ModelConfig; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -37,7 +39,6 @@ import java.util.Collections; import java.util.Optional; /** - * * Cache of active subscriptions. When a new subscription is added to the cache, a new Spring Channel is created * and a new MessageHandler for that subscription is subscribed to that channel. These subscriptions, channels, and * handlers are all caches in this registry so they can be removed it the subscription is deleted. @@ -47,7 +48,7 @@ import java.util.Optional; @Component public class SubscriptionRegistry { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SubscriptionRegistry.class); - + private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); @Autowired SubscriptionCanonicalizer<IBaseResource> mySubscriptionCanonicalizer; @Autowired @@ -56,8 +57,8 @@ public class SubscriptionRegistry { SubscriptionChannelFactory mySubscriptionDeliveryChannelFactory; @Autowired ModelConfig myModelConfig; - - private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); + @Autowired + private IInterceptorRegistry myInterceptorRegistry; public ActiveSubscription get(String theIdPart) { return myActiveSubscriptionCache.get(theIdPart); @@ -98,6 +99,9 @@ public class SubscriptionRegistry { myActiveSubscriptionCache.put(subscriptionId, activeSubscription); + // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED + myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, canonicalized); + return canonicalized; } From c22aeaa2e5fe0cd522c1dc8c8c22ceb756a11860 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sun, 20 Jan 2019 12:09:19 -0500 Subject: [PATCH 33/56] Tweaks to interceptor registry --- .../executor/InterceptorRegistry.java | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index 1e4d1f53c65..4765d95da99 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -46,9 +46,9 @@ import java.util.concurrent.atomic.AtomicInteger; public class InterceptorRegistry implements IInterceptorRegistry, ApplicationContextAware { private static final Logger ourLog = LoggerFactory.getLogger(InterceptorRegistry.class); private ApplicationContext myAppCtx; - private List<Object> myGlobalInterceptors = new ArrayList<>(); - private ListMultimap<Pointcut, BaseInvoker> myInvokers = ArrayListMultimap.create(); - private ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + private final List<Object> myGlobalInterceptors = new ArrayList<>(); + private final ListMultimap<Pointcut, BaseInvoker> myInvokers = ArrayListMultimap.create(); + private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); /** * Constructor @@ -89,39 +89,41 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon // Grab the global interceptors String[] globalInterceptorNames = myAppCtx.getBeanNamesForAnnotation(Interceptor.class); for (String nextName : globalInterceptorNames) { - Object nextGlobalInterceptor = myAppCtx.getBean(nextName); - myGlobalInterceptors.add(nextGlobalInterceptor); + Object nextInterceptor = myAppCtx.getBean(nextName); + registerGlobalInterceptor(nextInterceptor); } - // Pull out the hook methods - for (Object nextInterceptor : myGlobalInterceptors) { + } - int typeOrder = DEFAULT_ORDER; - Order typeOrderAnnotation = AnnotationUtils.findAnnotation(nextInterceptor.getClass(), Order.class); - if (typeOrderAnnotation != null) { - typeOrder = typeOrderAnnotation.value(); - } + private void registerGlobalInterceptor(Object theNextInterceptor) { + int typeOrder = DEFAULT_ORDER; + Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theNextInterceptor.getClass(), Order.class); + if (typeOrderAnnotation != null) { + typeOrder = typeOrderAnnotation.value(); + } - for (Method nextMethod : nextInterceptor.getClass().getDeclaredMethods()) { - Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); + for (Method nextMethod : theNextInterceptor.getClass().getDeclaredMethods()) { + Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); - if (hook != null) { + if (hook != null) { - int methodOrder = typeOrder; - Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class); - if (methodOrderAnnotation != null) { - methodOrder = methodOrderAnnotation.value(); - } + int methodOrder = typeOrder; + Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class); + if (methodOrderAnnotation != null) { + methodOrder = methodOrderAnnotation.value(); + } - HookInvoker invoker = new HookInvoker(hook, nextInterceptor, nextMethod, methodOrder); - for (Pointcut nextPointcut : hook.value()) { - myInvokers.put(nextPointcut, invoker); - } + HookInvoker invoker = new HookInvoker(hook, theNextInterceptor, nextMethod, methodOrder); + for (Pointcut nextPointcut : hook.value()) { + myInvokers.put(nextPointcut, invoker); } } } - // Sort everything by declared order + myGlobalInterceptors.add(theNextInterceptor); + + // Make sure we're always sorted according to the order declared in + // @Order sortByOrderAnnotation(myGlobalInterceptors); for (Pointcut nextPointcut : myInvokers.keys()) { List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut); From af9153d64ee3c3262f09b406c4180a7e3007a299 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sun, 20 Jan 2019 14:37:16 -0500 Subject: [PATCH 34/56] Interceptor API tweak --- .../interceptor/api/IInterceptorRegistry.java | 12 ++++++++++-- .../executor/InterceptorRegistry.java | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java index 3c914295797..4ed950f0d3e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.api; * 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. @@ -35,6 +35,14 @@ public interface IInterceptorRegistry { @VisibleForTesting void clearAnonymousHookForUnitTest(); + /** + * Register an interceptor + * + * @param theInterceptor The interceptor to register + * @return Returns <code>true</code> if at least one valid hook method was found on this interceptor + */ + boolean registerGlobalInterceptor(Object theInterceptor); + /** * Invoke the interceptor methods */ diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index 4765d95da99..9d7744d386d 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -95,14 +95,17 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon } - private void registerGlobalInterceptor(Object theNextInterceptor) { + @Override + public boolean registerGlobalInterceptor(Object theInterceptor) { + boolean retVal = false; + int typeOrder = DEFAULT_ORDER; - Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theNextInterceptor.getClass(), Order.class); + Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theInterceptor.getClass(), Order.class); if (typeOrderAnnotation != null) { typeOrder = typeOrderAnnotation.value(); } - for (Method nextMethod : theNextInterceptor.getClass().getDeclaredMethods()) { + for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) { Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); if (hook != null) { @@ -113,14 +116,16 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon methodOrder = methodOrderAnnotation.value(); } - HookInvoker invoker = new HookInvoker(hook, theNextInterceptor, nextMethod, methodOrder); + HookInvoker invoker = new HookInvoker(hook, theInterceptor, nextMethod, methodOrder); for (Pointcut nextPointcut : hook.value()) { myInvokers.put(nextPointcut, invoker); } + + retVal = true; } } - myGlobalInterceptors.add(theNextInterceptor); + myGlobalInterceptors.add(theInterceptor); // Make sure we're always sorted according to the order declared in // @Order @@ -129,6 +134,8 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut); nextInvokerList.sort(Comparator.naturalOrder()); } + + return retVal; } private void sortByOrderAnnotation(List<Object> theObjects) { From 690af1c7ff252233d5dd97bfb8f6e49982c70849 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sun, 20 Jan 2019 19:43:08 -0500 Subject: [PATCH 35/56] Add interceptor example --- examples/pom.xml | 5 ++++ .../interceptor/MyTestInterceptor.java | 25 +++++++++++++++++++ .../interceptor/api/IInterceptorRegistry.java | 4 +-- .../executor/InterceptorRegistry.java | 4 +-- .../module/cache/SubscriptionRegistry.java | 4 +-- 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 examples/src/main/java/example/interceptor/MyTestInterceptor.java diff --git a/examples/pom.xml b/examples/pom.xml index 15adf39931e..98034c83f21 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -92,6 +92,11 @@ <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-jpaserver-base</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.slf4j</groupId> diff --git a/examples/src/main/java/example/interceptor/MyTestInterceptor.java b/examples/src/main/java/example/interceptor/MyTestInterceptor.java new file mode 100644 index 00000000000..ba4b92f4aaf --- /dev/null +++ b/examples/src/main/java/example/interceptor/MyTestInterceptor.java @@ -0,0 +1,25 @@ +package example.interceptor; + +import ca.uhn.fhir.jpa.model.interceptor.api.Hook; +import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage; + +/** + * Interceptor class + */ +@Interceptor +public class MyTestInterceptor { + + @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) + public boolean beforeRestHookDelivery(ResourceDeliveryMessage theDeliveryMessage, CanonicalSubscription theSubscription) { + + String header = "Authorization: Bearer 1234567"; + + theSubscription.addHeader(header); + + return true; + } + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java index 4ed950f0d3e..6661bd90948 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.api; * 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. diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index 9d7744d386d..14e3ed9c2a3 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.executor; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index b18b4a3627e..29c6e939d2c 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * 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. From 17f03ac84320b67f3a60ea070b85b5cd8f2fe75e Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Sun, 20 Jan 2019 19:43:58 -0500 Subject: [PATCH 36/56] Allow null return type for interceptors --- .../fhir/jpa/interceptor/test/InterceptorRegistryTest.java | 5 +---- .../jpa/model/interceptor/executor/InterceptorRegistry.java | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java index d1f6c0d9ead..de384608297 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java @@ -26,7 +26,6 @@ import static org.junit.Assert.*; @ContextConfiguration(classes = {InterceptorRegistryTest.InterceptorRegistryTestCtxConfig.class}) public class InterceptorRegistryTest { - private static boolean ourNext_beforeRestHookDelivery_Return2; private static boolean ourNext_beforeRestHookDelivery_Return1; private static List<String> ourInvocations = new ArrayList<>(); private static CanonicalSubscription ourLastCanonicalSubscription; @@ -88,7 +87,6 @@ public class InterceptorRegistryTest { @Before public void before() { ourNext_beforeRestHookDelivery_Return1 = true; - ourNext_beforeRestHookDelivery_Return2 = true; ourLastCanonicalSubscription = null; ourLastResourceDeliveryMessage0 = null; ourLastResourceDeliveryMessage1 = null; @@ -140,11 +138,10 @@ public class InterceptorRegistryTest { @Order(200) public static class MyTestInterceptorTwo { @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) - public boolean beforeRestHookDelivery(ResourceDeliveryMessage theResourceDeliveryMessage0, ResourceDeliveryMessage theResourceDeliveryMessage1) { + public void beforeRestHookDelivery(ResourceDeliveryMessage theResourceDeliveryMessage0, ResourceDeliveryMessage theResourceDeliveryMessage1) { ourLastResourceDeliveryMessage0 = theResourceDeliveryMessage0; ourLastResourceDeliveryMessage1 = theResourceDeliveryMessage1; ourInvocations.add("MyTestInterceptorTwo.beforeRestHookDelivery"); - return ourNext_beforeRestHookDelivery_Return2; } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java index 14e3ed9c2a3..da86d6212a0 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java @@ -254,10 +254,10 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon myMethod = theHookMethod; Class<?> returnType = theHookMethod.getReturnType(); - if (returnType.equals(boolean.class) || returnType.equals(Boolean.class)) { + if (returnType.equals(boolean.class)) { myReturnsBoolean = true; } else { - Validate.isTrue(Void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); + Validate.isTrue(void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); myReturnsBoolean = false; } From b4350bbdae408658573ff8c1fa2e39cd75a2caa7 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Mon, 21 Jan 2019 11:05:53 -0500 Subject: [PATCH 37/56] Working through failed tests. Consolidating sleep calls --- .../java/ca/uhn/fhir/jpa/util/TestUtil.java | 17 ++++++- .../FhirResourceDaoDstu3SearchNoFtTest.java | 17 ++++--- ...rResourceDaoDstu3SearchPageExpiryTest.java | 9 ++-- .../dao/dstu3/FhirResourceDaoDstu3Test.java | 6 +-- .../dstu3/FhirResourceDaoDstu3UpdateTest.java | 2 +- .../FhirResourceDaoDstu3ValidateTest.java | 6 +-- .../r4/FhirResourceDaoR4SearchNoFtTest.java | 32 ++++++++----- .../FhirResourceDaoR4SearchNoHashesTest.java | 47 ++++++++++--------- ...FhirResourceDaoR4SearchPageExpiryTest.java | 10 ++-- .../dao/r4/FhirResourceDaoR4UpdateTest.java | 13 +++-- .../r4/ResourceProviderR4CacheTest.java | 3 +- .../provider/r4/ResourceProviderR4Test.java | 4 +- .../module/config/BaseSubscriptionConfig.java | 2 +- .../subscription/module/LatchedService.java | 2 - ...kingQueueSubscribableChannelDstu3Test.java | 8 ++-- 15 files changed, 103 insertions(+), 75 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index 0f41138cc20..1cd6d7098cf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -25,11 +25,15 @@ import com.google.common.collect.ImmutableSet; import com.google.common.reflect.ClassPath; import com.google.common.reflect.ClassPath.ClassInfo; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.InstantType; +import org.hl7.fhir.r4.model.Patient; import javax.persistence.*; import java.io.IOException; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.util.Date; import java.util.HashSet; import java.util.Set; @@ -165,10 +169,21 @@ public class TestUtil { ourLog.info("Sleeping for {}ms", timeToSleep); Thread.sleep(timeToSleep); } catch (InterruptedException theE) { - theE.printStackTrace(); + ourLog.error("Interrupted", theE); } } } + public static void clearAllStaticFieldsForUnitTest() { + ca.uhn.fhir.util.TestUtil.clearAllStaticFieldsForUnitTest(); + } + + public static InstantType getTimestamp(IBaseResource resource) { + return new InstantType(new Date(resource.getMeta().getLastUpdated().getTime())); + } + + public static void sleepOneClick() { + sleepAtLeast(1); + } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 5ebda85d4df..2e7d2c50ff0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -13,6 +13,7 @@ import javax.servlet.http.HttpServletRequest; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.TestUtil; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu3.model.*; @@ -40,7 +41,6 @@ import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.util.TestUtil; @SuppressWarnings("unchecked") public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { @@ -667,6 +667,9 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } long betweenTime = System.currentTimeMillis(); + + TestUtil.sleepOneClick(); + IIdType id2; { Patient patient = new Patient(); @@ -1199,7 +1202,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { int sleep = 100; long start = System.currentTimeMillis(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepAtLeast(sleep); IIdType id1a; { @@ -1218,7 +1221,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepAtLeast(sleep); long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1615,18 +1618,18 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { obs01.setSubject(new Reference(patientId01)); IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date between = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Observation obs02 = new Observation(); obs02.setEffective(new DateTimeType(new Date())); obs02.setSubject(new Reference(locId01)); IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date after = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", new Object[] { patientId01, locId01, obsId01, obsId02 }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java index 90b19eaaa53..779e8175506 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchPageExpiryTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4SearchPageExpiryTest; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; @@ -29,7 +30,6 @@ import org.springframework.transaction.support.TransactionTemplate; import java.util.Date; import java.util.concurrent.atomic.AtomicLong; -import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.*; @@ -105,8 +105,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { } assertEquals(searchUuid1, searchUuid2); - sleepAtLeast(501); - + TestUtil.sleepAtLeast(501); // We're now past 500ms so we shouldn't reuse the search final String searchUuid3; @@ -277,7 +276,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { } assertEquals(searchUuid1, searchUuid2); - sleepAtLeast(501); + TestUtil.sleepAtLeast(501); // We're now past 500ms so we shouldn't reuse the search @@ -363,7 +362,7 @@ public class FhirResourceDaoDstu3SearchPageExpiryTest extends BaseJpaDstu3Test { } }); if (search == null) { - sleepAtLeast(100); + TestUtil.sleepAtLeast(100); } } assertNotNull("Search " + bundleProvider.getUuid() + " not found on disk after 10 seconds", search); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index d8b94a50f9f..0d41ad30e2c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -2867,21 +2867,21 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { p.addName().setFamily(methodName); IIdType id1 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); p = new Patient(); p.addIdentifier().setSystem("urn:system2").setValue(methodName); p.addName().setFamily(methodName); IIdType id2 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); p = new Patient(); p.addIdentifier().setSystem("urn:system3").setValue(methodName); p.addName().setFamily(methodName); IIdType id3 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); p = new Patient(); p.addIdentifier().setSystem("urn:system4").setValue(methodName); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java index 51089f210d4..ae9c5f521c9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java @@ -306,7 +306,7 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test { assertEquals("1", outcome.getId().getVersionIdPart()); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date now = new Date(); Patient retrieved = myPatientDao.read(outcome.getId(), mySrd); InstantType updated = retrieved.getMeta().getLastUpdatedElement().copy(); 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 9c60aea71f1..9e297d4d916 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 @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.ValidationModeEnum; @@ -11,7 +12,6 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.StopWatch; -import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; @@ -25,8 +25,6 @@ import org.junit.Test; import java.io.IOException; import java.nio.charset.StandardCharsets; -import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; - public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoDstu3ValidateTest.class); @@ -64,7 +62,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { MethodOutcome results = myQuestionnaireResponseDao.validate(qr, null, null, null, null, null, null); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(results.getOperationOutcome())); - sleepAtLeast(2500); + TestUtil.sleepAtLeast(2500); try { myQuestionnaireResponseDao.validate(qr, null, null, null, null, null, null); fail(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index f49b54e57bf..bfcbdc6a22c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -12,7 +13,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -151,16 +151,16 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { List<String> ids; Date beforeAll = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Organization org = new Organization(); org.setName("O1"); org.setId("O1"); myOrganizationDao.update(org); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date beforePatient = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Patient p = new Patient(); p.setId("P1"); @@ -168,7 +168,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { p.setManagingOrganization(new Reference("Organization/O1")); myPatientDao.update(p); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date afterAll = new Date(); // Search with between date (should still return Organization even though @@ -216,7 +216,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { myOrganizationDao.update(org); Date beforeAll = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Patient p = new Patient(); p.setId("P1"); @@ -224,17 +224,17 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { p.setManagingOrganization(new Reference("Organization/O1")); myPatientDao.update(p); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date beforeOrg = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); org = new Organization(); org.setActive(true); org.setId("O1"); myOrganizationDao.update(org); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date afterAll = new Date(); // Everything should come back @@ -890,6 +890,9 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } long betweenTime = System.currentTimeMillis(); + + TestUtil.sleepOneClick(); + IIdType id2; { Patient patient = new Patient(); @@ -1266,6 +1269,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { Date betweenTime = new Date(); + TestUtil.sleepOneClick(); + IIdType id2; { Patient patient = new Patient(); @@ -1439,10 +1444,10 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; long start = System.currentTimeMillis(); - Thread.sleep(sleep); + + TestUtil.sleepOneClick(); IIdType id1a; { @@ -1461,7 +1466,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); - Thread.sleep(sleep); + TestUtil.sleepOneClick(); + long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1487,7 +1493,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN, startDateTime.getValue()), - new DateParam(ParamPrefixEnum.LESSTHAN, myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValue()))); + new DateParam(ParamPrefixEnum.LESSTHAN, TestUtil.getTimestamp(myPatientDao.read(id1b, mySrd))))); ourLog.info("Searching: {}", map.getLastUpdated()); assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), containsInAnyOrder(id1a)); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index ff2144142d3..7810878bc0e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -12,7 +13,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; -import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -153,16 +153,16 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { List<String> ids; Date beforeAll = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Organization org = new Organization(); org.setName("O1"); org.setId("O1"); myOrganizationDao.update(org); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date beforePatient = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Patient p = new Patient(); p.setId("P1"); @@ -170,7 +170,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { p.setManagingOrganization(new Reference("Organization/O1")); myPatientDao.update(p); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date afterAll = new Date(); // Search with between date (should still return Organization even though @@ -218,7 +218,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { myOrganizationDao.update(org); Date beforeAll = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Patient p = new Patient(); p.setId("P1"); @@ -226,17 +226,17 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { p.setManagingOrganization(new Reference("Organization/O1")); myPatientDao.update(p); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date beforeOrg = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); org = new Organization(); org.setActive(true); org.setId("O1"); myOrganizationDao.update(org); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(100); + ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick(); Date afterAll = new Date(); // Everything should come back @@ -891,6 +891,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { patient.addIdentifier().setSystem("urn:system").setValue("001"); id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); long betweenTime = System.currentTimeMillis(); IIdType id2; { @@ -1266,6 +1267,8 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenTime = new Date(); IIdType id2; @@ -1349,8 +1352,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { public void testSearchLastUpdatedParam() throws InterruptedException { String methodName = "testSearchLastUpdatedParam"; - int sleep = 100; - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); DateTimeType beforeAny = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI); IIdType id1a; @@ -1368,9 +1370,9 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { id1b = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1100); + TestUtil.sleepAtLeast(1100); DateTimeType beforeR2 = new DateTimeType(new Date(), TemporalPrecisionEnum.MILLI); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1100); + TestUtil.sleepAtLeast(1100); IIdType id2; { @@ -1441,10 +1443,8 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; - long start = System.currentTimeMillis(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); IIdType id1a; { @@ -1463,7 +1463,8 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); + long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1489,7 +1490,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { map = new SearchParameterMap(); map.setLastUpdated(new DateRangeParam(new DateParam(ParamPrefixEnum.GREATERTHAN, startDateTime.getValue()), - new DateParam(ParamPrefixEnum.LESSTHAN, myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValue()))); + new DateParam(ParamPrefixEnum.LESSTHAN, TestUtil.getTimestamp(myPatientDao.read(id1b, mySrd))))); ourLog.info("Searching: {}", map.getLastUpdated()); assertThat(toUnqualifiedVersionlessIds(myPatientDao.search(map)), containsInAnyOrder(id1a)); } @@ -1860,14 +1861,14 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); Date between = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); + TestUtil.sleepOneClick(); Observation obs02 = new Observation(); obs02.setEffective(new DateTimeType(new Date())); obs02.setSubject(new Reference(locId01)); IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); + TestUtil.sleepOneClick(); Date after = new Date(); ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", patientId01, locId01, obsId01, obsId02); @@ -1991,14 +1992,14 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } Date between = new Date(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); + TestUtil.sleepOneClick(); { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("002"); patient.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(10); + TestUtil.sleepOneClick(); Date after = new Date(); SearchParameterMap params; @@ -2945,6 +2946,8 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenDate = new Date(); IIdType tag2id; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java index aa99e4dbb4b..1dd6c8a479d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchPageExpiryTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.data.ISearchDao; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchStatusEnum; import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.StringParam; @@ -29,7 +30,6 @@ import javax.annotation.Nullable; import java.util.Date; import java.util.concurrent.atomic.AtomicLong; -import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.*; @@ -100,7 +100,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { } assertEquals(searchUuid1, searchUuid2); - sleepAtLeast(501); + TestUtil.sleepAtLeast(501); // We're now past 500ms so we shouldn't reuse the search @@ -274,7 +274,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { } assertEquals(searchUuid1, searchUuid2); - sleepAtLeast(501); + TestUtil.sleepAtLeast(501); // We're now past 500ms so we shouldn't reuse the search @@ -360,7 +360,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { } }); if (search == null) { - sleepAtLeast(100); + TestUtil.sleepAtLeast(100); } } assertNotNull("Search " + bundleProvider.getUuid() + " not found on disk after 10 seconds", search); @@ -407,7 +407,7 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test { for (int i = 0; i < 20 && search == null; i++) { search = theSearchEntityDao.findByUuid(theUuid); if (search == null || search.getStatus() == SearchStatusEnum.LOADING) { - sleepAtLeast(100); + TestUtil.sleepAtLeast(100); } } assertNotNull(search); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index aa6008ccc73..06e3dc75327 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; @@ -12,7 +13,6 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -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.*; @@ -314,11 +314,14 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { assertEquals("1", outcome.getId().getVersionIdPart()); Date now = new Date(); + + TestUtil.sleepOneClick(); + Patient retrieved = myPatientDao.read(outcome.getId(), mySrd); - InstantType updated = retrieved.getMeta().getLastUpdatedElement().copy(); + InstantType updated = TestUtil.getTimestamp(retrieved); assertTrue(updated.before(now)); - Thread.sleep(1000); + TestUtil.sleepOneClick(); reset(myInterceptor); retrieved.getIdentifier().get(0).setValue("002"); @@ -341,11 +344,11 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { assertEquals("2", retrieved2.getIdElement().getVersionIdPart()); assertEquals("002", retrieved2.getIdentifier().get(0).getValue()); - InstantType updated2 = retrieved2.getMeta().getLastUpdatedElement(); + InstantType updated2 = TestUtil.getTimestamp(retrieved2); assertTrue(updated2.after(now)); assertTrue(updated2.before(now2)); - Thread.sleep(2000); + TestUtil.sleepOneClick(); /* * Get history diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java index 1792f9fe300..40c0b4012e9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java @@ -2,12 +2,12 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.CacheControlDirective; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Patient; import org.junit.After; @@ -159,6 +159,7 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test { ourClient.create().resource(pt1).execute(); Date beforeFirst = new Date(); + TestUtil.sleepOneClick(); Bundle results1 = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute(); assertEquals(1, results1.getEntry().size()); 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 72f422b8a8b..2faa1d58a5e 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 @@ -37,6 +37,7 @@ import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.rest.api.PreferReturnEnum; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -116,7 +117,6 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.util.StopWatch; -import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.UrlUtil; @SuppressWarnings("Duplicates") @@ -3659,6 +3659,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { Search search1 = newTxTemplate().execute(theStatus -> mySearchEntityDao.findByUuid(uuid1)); Date lastReturned1 = search1.getSearchLastReturned(); + TestUtil.sleepOneClick(); + Bundle result2 = ourClient .search() .forResource("Organization") diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java index a89870c7b0e..b484886ab18 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/config/BaseSubscriptionConfig.java @@ -30,7 +30,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @Configuration @EnableScheduling -@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.searchparam", "ca.uhn.fhir.jpa.subscription.module", "ca.uhn.fhir.jpa.model.interceptor.executor"}) +@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.searchparam", "ca.uhn.fhir.jpa.subscription.module"}) public abstract class BaseSubscriptionConfig { public abstract FhirContext fhirContext(); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java index cca0c509d4b..6e638bdfd36 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java @@ -13,8 +13,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; public class LatchedService implements IAnonymousLambdaHook { diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index 88a16c83ee6..4cb096dba32 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -65,8 +65,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base private static SubscribableChannel ourSubscribableChannel; private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); private long idCounter = 0; - protected LatchedService mySubscriptionMatchingPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_MATCHING); - protected LatchedService mySubscriptionActivatedPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_ACTIVATED); + protected LatchedService mySubscriptionMatchingPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED); + protected LatchedService mySubscriptionActivatedPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); @Before public void beforeReset() { @@ -77,8 +77,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base ourSubscribableChannel = mySubscriptionChannelFactory.newDeliveryChannel("test", Subscription.SubscriptionChannelType.RESTHOOK.toCode().toLowerCase()); ourSubscribableChannel.subscribe(myStandaloneSubscriptionMessageHandler); } - myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_MATCHING, mySubscriptionMatchingPost); - myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_SUBSCRIPTION_ACTIVATED, mySubscriptionActivatedPost); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, mySubscriptionMatchingPost); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, mySubscriptionActivatedPost); } @After From 9cf64f78d023e3e9643cd801492e4857e856af19 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Mon, 21 Jan 2019 14:46:17 -0500 Subject: [PATCH 38/56] Deprecate a bad setter on RetfulServer, fix a paging issue, and add a disabled unit test to the subscription matcher --- .../provider/r4/ResourceProviderR4Test.java | 28 +++++++++++++++++++ .../InMemorySubscriptionMatcherTestR3.java | 20 +++++++++++++ .../uhn/fhir/rest/server/RestfulServer.java | 14 ++++++++++ .../rest/server/method/PageMethodBinding.java | 2 +- .../fhir/rest/server/GraphQLR4RawTest.java | 3 +- .../uhn/fhir/rest/server/HistoryR4Test.java | 3 +- src/changes/changes.xml | 7 +++++ 7 files changed, 73 insertions(+), 4 deletions(-) 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 72f422b8a8b..4a4cddfa7e7 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 @@ -198,6 +198,34 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { } + @Test + public void testSearchFetchPageBeyondEnd() { + for (int i = 0; i < 10; i++) { + Organization o = new Organization(); + o.setId("O" + i); + o.setName("O" + i); + IIdType oid = ourClient.update().resource(o).execute().getId().toUnqualifiedVersionless(); + } + + Bundle output = ourClient + .search() + .forResource("Organization") + .count(3) + .returnBundle(Bundle.class) + .execute(); + + String nextPageUrl = output.getLink("next").getUrl(); + String url = nextPageUrl.replace("_getpagesoffset=3", "_getpagesoffset=999"); + ourLog.info("Going to request URL: {}", url); + + output = ourClient + .loadPage() + .byUrl(url) + .andReturnBundle(Bundle.class) + .execute(); + assertEquals(0, output.getEntry().size()); + + } @Test public void testDeleteConditional() { diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java index 0e004ee603e..eb0706b2001 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java @@ -7,6 +7,7 @@ import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.codesystems.MedicationRequestCategory; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +38,25 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test assertFalse(result.matched()); } + @Test + @Ignore + public void testResourceById() { + + ProcedureRequest pr = new ProcedureRequest(); + pr.setId("ProcedureRequest/123"); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER); + + assertMatched(pr, "ProcedureRequest?_id=123"); + assertMatched(pr, "ProcedureRequest?_id=Patient/123"); + assertMatched(pr, "ProcedureRequest?_id=Patient/123,Patient/999"); + assertMatched(pr, "ProcedureRequest?_id=Patient/123&_id=Patient/123"); + assertNotMatched(pr, "ProcedureRequest?_id=Patient/888"); + assertNotMatched(pr, "ProcedureRequest?_id=Patient/888,Patient/999"); + assertNotMatched(pr, "ProcedureRequest?_id=Patient/123&_id=Patient/888"); + + } + + /* The following tests are copied from an e-mail from a site using HAPI FHIR */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index b825207ad85..99f5d50544b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -599,7 +599,9 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet * Sets the non-resource specific providers which implement method calls on this server. * * @see #setResourceProviders(Collection) + * @deprecated This method causes inconsistent behaviour depending on the order it is called in. Use {@link #registerProviders(Object...)} instead. */ + @Deprecated public void setPlainProviders(Object... theProv) { setPlainProviders(Arrays.asList(theProv)); } @@ -608,7 +610,9 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet * Sets the non-resource specific providers which implement method calls on this server. * * @see #setResourceProviders(Collection) + * @deprecated This method causes inconsistent behaviour depending on the order it is called in. Use {@link #registerProviders(Object...)} instead. */ + @Deprecated public void setPlainProviders(Collection<Object> theProviders) { Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); @@ -1359,6 +1363,16 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet } } + /** + * Register a group of providers. These could be Resource Providers (classes implementing {@link IResourceProvider}) or "plain" providers, or a mixture of the two. + * + * @param theProviders a {@code Collection} of theProviders. The parameter could be null or an empty {@code Collection} + */ + public void registerProviders(Object... theProviders) { + Validate.noNullElements(theProviders); + registerProviders(Arrays.asList(theProviders)); + } + /** * Register a group of theProviders. These could be Resource Providers, "plain" theProviders or a mixture of the two. * diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java index 581b8b753a3..0e83b7c6dfe 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/PageMethodBinding.java @@ -118,7 +118,7 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding { Integer totalNum = resultList.size(); start = offsetI; if (totalNum != null) { - start = Math.min(start, totalNum - 1); + start = Math.min(start, totalNum); } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java index 1d3a9785efe..012526f0638 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/GraphQLR4RawTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -64,7 +65,7 @@ public class GraphQLR4RawTest { servlet.setDefaultResponseEncoding(EncodingEnum.JSON); servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); - servlet.registerProvider(new MyGraphQLProvider()); + servlet.registerProviders(Collections.singletonList(new MyGraphQLProvider())); servlet.registerProvider(new MyPatientResourceProvider()); ServletHolder servletHolder = new ServletHolder(servlet); proxyHandler.addServletWithMapping(servletHolder, "/*"); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/HistoryR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/HistoryR4Test.java index 492375ac3d6..895a99183fd 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/HistoryR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/HistoryR4Test.java @@ -187,8 +187,7 @@ public class HistoryR4Test { ServletHandler proxyHandler = new ServletHandler(); RestfulServer servlet = new RestfulServer(ourCtx); - servlet.setPlainProviders(plainProvider); - servlet.setResourceProviders(patientProvider); + servlet.registerProviders(plainProvider, patientProvider); ServletHolder servletHolder = new ServletHolder(servlet); proxyHandler.addServletWithMapping(servletHolder, "/*"); ourServer.setHandler(proxyHandler); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d0d8422ed69..6646dde15ba 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -313,6 +313,13 @@ HAPI FHIR will now log the Git revision when it first starts up (on the ame line as the version number that it already logs). </action> + <action type="fix"> + When fetching a page of search results, if a page offset beyond the total number + of available result was requested, a single result was still returned (e.g. + requesting a page beginning at index 1000 when there are only 10 results would + result in the 10th result being returned). This will now result in an empty + response Bundle as would be expected. + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From a6d1cc56c7ee7962c8cfdfb1a79d90ec15a47344 Mon Sep 17 00:00:00 2001 From: Ken Stevens <khstevens@gmail.com> Date: Mon, 21 Jan 2019 16:58:14 -0500 Subject: [PATCH 39/56] added maxRetriex extension to canonical subscription --- .../module/CanonicalSubscription.java | 29 ++++++++++++++----- .../cache/SubscriptionCanonicalizer.java | 17 +++++++++-- .../module/cache/SubscriptionConstants.java | 6 ++++ ...scriptionDeliveringRestHookSubscriber.java | 3 +- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java index f240b810453..707a75aabc4 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java @@ -295,6 +295,8 @@ public class CanonicalSubscription implements Serializable, Cloneable { private boolean myStripVersionId; @JsonProperty("deliverLatestVersion") private boolean myDeliverLatestVersion; + @JsonProperty("maxRetries") + private int myMaxRetries; /** * Constructor @@ -311,6 +313,23 @@ public class CanonicalSubscription implements Serializable, Cloneable { myDeliverLatestVersion = theDeliverLatestVersion; } + + public boolean isStripVersionId() { + return myStripVersionId; + } + + public void setStripVersionId(boolean theStripVersionId) { + myStripVersionId = theStripVersionId; + } + + public int getMaxRetries() { + return myMaxRetries; + } + + public void setMaxRetries(int theMaxRetries) { + myMaxRetries = theMaxRetries; + } + @Override public boolean equals(Object theO) { if (this == theO) return true; @@ -322,6 +341,7 @@ public class CanonicalSubscription implements Serializable, Cloneable { return new EqualsBuilder() .append(myStripVersionId, that.myStripVersionId) .append(myDeliverLatestVersion, that.myDeliverLatestVersion) + .append(myMaxRetries, that.myMaxRetries) .isEquals(); } @@ -330,17 +350,10 @@ public class CanonicalSubscription implements Serializable, Cloneable { return new HashCodeBuilder(17, 37) .append(myStripVersionId) .append(myDeliverLatestVersion) + .append(myMaxRetries) .toHashCode(); } - public boolean isStripVersionId() { - return myStripVersionId; - } - - public void setStripVersionId(boolean theStripVersionId) { - myStripVersionId = theStripVersionId; - } - } @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index b7a3f09ce7c..ed25780a416 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -96,10 +97,10 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { retVal.setIdElement(subscription.getIdElement()); retVal.setPayloadString(subscription.getChannel().getPayload()); - if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { + if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { String from; String subjectTemplate; - String bodyTemplate; + try { from = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_EMAIL_FROM); subjectTemplate = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_SUBJECT_TEMPLATE); @@ -111,16 +112,22 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { } if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { + String stripVersionIds; String deliverLatestVersion; + String maxRetries; try { stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); + maxRetries = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_MAX_RETRIES); } catch (FHIRException theE) { throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); } retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); + if (isNotBlank(maxRetries)) { + retVal.getRestHookDetails().setMaxRetries(Integer.parseInt(maxRetries)); + } } } catch (FHIRException theE) { @@ -239,14 +246,20 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { String stripVersionIds; String deliverLatestVersion; + String maxRetries; try { stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); + maxRetries = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_MAX_RETRIES); + } catch (FHIRException theE) { throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); } retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); + if (isNotBlank(maxRetries)) { + retVal.getRestHookDetails().setMaxRetries(Integer.parseInt(maxRetries)); + } } List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java index 5d9f3f1ef20..e785464508d 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java @@ -67,6 +67,12 @@ public class SubscriptionConstants { */ public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version"; + /** + * This extension URL indicates the maximum number of delivery retries that will be attempted before the subscription delivery is considered to have failed. + */ + + public static final String EXT_SUBSCRIPTION_MAX_RETRIES = "http://hapifhir.io/fhir/StructureDefinition/subscription-max-retries"; + /** * The number of threads used in subscription channel processing */ diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index 3358e8ab0cd..cefaf35eb79 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -101,8 +101,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe try { operation.execute(); } catch (ResourceNotFoundException e) { - ourLog.error("Cannot reach " + theMsg.getSubscription().getEndpointUrl()); - e.printStackTrace(); + ourLog.error("Cannot reach " + theMsg.getSubscription().getEndpointUrl(), e); throw e; } } From 6b22977d7c464259f392f267d03ae6c94e37c62c Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Mon, 21 Jan 2019 22:28:48 -0500 Subject: [PATCH 40/56] fixing more tests for Windows --- .../jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java | 3 ++- .../fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java | 1 + .../ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java | 1 + .../uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java | 4 ++-- .../ca/uhn/fhir/jpa/subscription/module/LatchedService.java | 3 ++- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java index 7bfd40cfb5c..9662d068099 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.dstu2.composite.*; @@ -19,7 +20,6 @@ import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.util.TestUtil; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -858,6 +858,7 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { SearchParameterMap params; Date startDate = new Date(start); + TestUtil.sleepOneClick(); Date endDate = new Date(end); DateTimeDt startDateTime = new DateTimeDt(startDate, TemporalPrecisionEnum.MILLI); DateTimeDt endDateTime = new DateTimeDt(endDate, TemporalPrecisionEnum.MILLI); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index 7810878bc0e..8300f487fd7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -1469,6 +1469,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { SearchParameterMap map; Date startDate = new Date(start); + TestUtil.sleepOneClick(); Date endDate = new Date(end); DateTimeType startDateTime = new DateTimeType(startDate, TemporalPrecisionEnum.MILLI); DateTimeType endDateTime = new DateTimeType(endDate, TemporalPrecisionEnum.MILLI); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index 06e3dc75327..b1f7d1b2c4c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -338,6 +338,7 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { assertEquals("Patient", details.getResourceType()); assertEquals(Patient.class, details.getResource().getClass()); + TestUtil.sleepOneClick(); Date now2 = new Date(); Patient retrieved2 = myPatientDao.read(outcome.getId().toVersionless(), mySrd); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java index 40c0b4012e9..16ed5ca99cb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java @@ -165,8 +165,8 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test { assertEquals(1, results1.getEntry().size()); assertEquals(1, mySearchEntityDao.count()); assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty()); - assertThat(results1.getMeta().getLastUpdated(), greaterThan(beforeFirst)); - assertThat(results1.getMeta().getLastUpdated(), lessThan(new Date())); + assertThat(TestUtil.getTimestamp(results1).getValue(), greaterThan(beforeFirst)); + assertThat(TestUtil.getTimestamp(results1).getValue(), lessThan(new Date())); assertThat(results1.getId(), not(blankOrNullString())); Patient pt2 = new Patient(); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java index 6e638bdfd36..0bb32a97fe5 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertTrue; public class LatchedService implements IAnonymousLambdaHook { private static final Logger ourLog = LoggerFactory.getLogger(LatchedService.class); + private static final int DEFAULT_TIMEOUT_SECONDS = 20; private final String name; private CountDownLatch myCountdownLatch; @@ -48,7 +49,7 @@ public class LatchedService implements IAnonymousLambdaHook { } public void awaitExpected() throws InterruptedException { - awaitExpectedWithTimeout(10); + awaitExpectedWithTimeout(DEFAULT_TIMEOUT_SECONDS); } public void awaitExpectedWithTimeout(int timeoutSecond) throws InterruptedException { From 2c7eb39b29510220e296417d4696de5f33ad3519 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Tue, 22 Jan 2019 18:53:54 -0500 Subject: [PATCH 41/56] final batch of windows fixes (to deal with jumpy windows clock) also added semaphore to PointcutLatch --- .../FhirResourceDaoDstu2SearchNoFtTest.java | 13 ++- .../FhirResourceDaoDstu3SearchNoFtTest.java | 21 ++-- .../r4/FhirResourceDaoR4SearchNoFtTest.java | 20 +++- .../FhirResourceDaoR4SearchNoHashesTest.java | 7 ++ .../jpa/dao/r4/FhirResourceDaoR4Test.java | 8 +- .../dao/r4/FhirResourceDaoR4UpdateTest.java | 4 +- .../r4/ResourceProviderR4CacheTest.java | 9 +- .../subscription/module/LatchedService.java | 72 -------------- .../subscription/module/PointcutLatch.java | 95 +++++++++++++++++++ ...kingQueueSubscribableChannelDstu3Test.java | 23 +++-- .../SubscriptionCheckingSubscriberTest.java | 8 +- .../SubscriptionMatchingSubscriberTest.java | 8 +- 12 files changed, 173 insertions(+), 115 deletions(-) delete mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java index 9662d068099..bfa828616d7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java @@ -374,6 +374,9 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { patient.addIdentifier().setSystem("urn:system").setValue("001"); id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + + TestUtil.sleepOneClick(); + long betweenTime = System.currentTimeMillis(); IIdType id2; { @@ -820,8 +823,6 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { @Test public void testSearchLastUpdatedParamWithComparator() throws InterruptedException { - String methodName = "testSearchLastUpdatedParamWithComparator"; - IIdType id0; { Patient patient = new Patient(); @@ -829,18 +830,16 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; - long start = System.currentTimeMillis(); - Thread.sleep(sleep); + TestUtil.sleepOneClick(); - DateTimeDt beforeAny = new DateTimeDt(new Date(), TemporalPrecisionEnum.MILLI); IIdType id1a; { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("001"); id1a = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); IIdType id1b; { Patient patient = new Patient(); @@ -853,7 +852,7 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { InstantDt id1bpublished = ResourceMetadataKeyEnum.PUBLISHED.get(myPatientDao.read(id1b, mySrd)); ourLog.info("Res 3: {}", id1bpublished.getValueAsString()); - Thread.sleep(sleep); + TestUtil.sleepOneClick(); long end = System.currentTimeMillis(); SearchParameterMap params; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 2e7d2c50ff0..77bdfa62ed1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -1023,7 +1023,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { patient.addName().setFamily("testSearchLanguageParam").addGiven("Joe"); id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - + TestUtil.sleepOneClick(); Date betweenTime = new Date(); IIdType id2; @@ -1199,10 +1199,9 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; - + TestUtil.sleepOneClick(); long start = System.currentTimeMillis(); - TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); IIdType id1a; { @@ -1221,7 +1220,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { ourLog.info("Res 2: {}", myPatientDao.read(id1a, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); ourLog.info("Res 3: {}", myPatientDao.read(id1b, mySrd).getMeta().getLastUpdatedElement().getValueAsString()); - TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1856,15 +1855,15 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); Date between = new Date(); - Thread.sleep(10); { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("002"); patient.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - Thread.sleep(10); + TestUtil.sleepOneClick(); Date after = new Date(); SearchParameterMap params; @@ -2874,6 +2873,8 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenDate = new Date(); IIdType tag2id; @@ -3196,7 +3197,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { p01.addName().setFamily("B").addGiven("A"); String id1 = myPatientDao.create(p01).getId().toUnqualifiedVersionless().getValue(); - Thread.sleep(10); + TestUtil.sleepOneClick(); // Numeric ID Patient p02 = new Patient(); @@ -3206,7 +3207,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { p02.addName().setFamily("Z").addGiven("Z"); String id2 = myPatientDao.create(p02).getId().toUnqualifiedVersionless().getValue(); - Thread.sleep(10); + TestUtil.sleepOneClick(); // Forced ID Patient pAB = new Patient(); @@ -3216,7 +3217,7 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { pAB.addName().setFamily("A").addGiven("B"); myPatientDao.update(pAB); - Thread.sleep(10); + TestUtil.sleepOneClick(); // Forced ID Patient pAA = new Patient(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index bfcbdc6a22c..221fbeaec8c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -889,10 +889,11 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { patient.addIdentifier().setSystem("urn:system").setValue("001"); id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - long betweenTime = System.currentTimeMillis(); TestUtil.sleepOneClick(); + long betweenTime = System.currentTimeMillis(); + IIdType id2; { Patient patient = new Patient(); @@ -1267,6 +1268,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenTime = new Date(); TestUtil.sleepOneClick(); @@ -1444,6 +1447,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); long start = System.currentTimeMillis(); @@ -1455,6 +1459,9 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { patient.addIdentifier().setSystem("urn:system").setValue("001"); id1a = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + + TestUtil.sleepOneClick(); + IIdType id1b; { Patient patient = new Patient(); @@ -1863,15 +1870,15 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { obs01.setSubject(new Reference(patientId01)); IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); + TestUtil.sleepOneClick(); Date between = new Date(); - Thread.sleep(10); Observation obs02 = new Observation(); obs02.setEffective(new DateTimeType(new Date())); obs02.setSubject(new Reference(locId01)); IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); - Thread.sleep(10); + TestUtil.sleepOneClick(); Date after = new Date(); ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", patientId01, locId01, obsId01, obsId02); @@ -1994,15 +2001,16 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); Date between = new Date(); - Thread.sleep(10); + { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("002"); patient.addName().setFamily("Tester_testSearchStringParam").addGiven("John"); pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - Thread.sleep(10); + TestUtil.sleepOneClick(); Date after = new Date(); SearchParameterMap params; @@ -2972,6 +2980,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenDate = new Date(); IIdType tag2id; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index 8300f487fd7..9d2c7036f75 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -1443,7 +1443,10 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + long start = System.currentTimeMillis(); + TestUtil.sleepOneClick(); IIdType id1a; @@ -1452,6 +1455,9 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { patient.addIdentifier().setSystem("urn:system").setValue("001"); id1a = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + + TestUtil.sleepOneClick(); + IIdType id1b; { Patient patient = new Patient(); @@ -1992,6 +1998,7 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); Date between = new Date(); TestUtil.sleepOneClick(); { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java index f75d4a5fbba..ea6c924d38b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4Test.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -20,7 +21,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -import ca.uhn.fhir.util.TestUtil; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; @@ -3159,16 +3159,22 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test { p.addName().setFamily(methodName); IIdType id1 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + TestUtil.sleepOneClick(); + p = new Patient(); p.addIdentifier().setSystem("urn:system2").setValue(methodName); p.addName().setFamily(methodName); IIdType id2 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + TestUtil.sleepOneClick(); + p = new Patient(); p.addIdentifier().setSystem("urn:system3").setValue(methodName); p.addName().setFamily(methodName); IIdType id3 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); + TestUtil.sleepOneClick(); + p = new Patient(); p.addIdentifier().setSystem("urn:system4").setValue(methodName); p.addName().setFamily(methodName); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index b1f7d1b2c4c..9301cbd1c59 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java @@ -313,10 +313,10 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { assertEquals("1", outcome.getId().getVersionIdPart()); - Date now = new Date(); - TestUtil.sleepOneClick(); + Date now = new Date(); + Patient retrieved = myPatientDao.read(outcome.getId(), mySrd); InstantType updated = TestUtil.getTimestamp(retrieved); assertTrue(updated.before(now)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java index 16ed5ca99cb..dd4eb173b2f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java @@ -159,14 +159,19 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test { ourClient.create().resource(pt1).execute(); Date beforeFirst = new Date(); + TestUtil.sleepOneClick(); Bundle results1 = ourClient.search().forResource("Patient").where(Patient.FAMILY.matches().value("FAM")).returnBundle(Bundle.class).execute(); + + TestUtil.sleepOneClick(); + assertEquals(1, results1.getEntry().size()); assertEquals(1, mySearchEntityDao.count()); assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), empty()); - assertThat(TestUtil.getTimestamp(results1).getValue(), greaterThan(beforeFirst)); - assertThat(TestUtil.getTimestamp(results1).getValue(), lessThan(new Date())); + Date results1Date = TestUtil.getTimestamp(results1).getValue(); + assertThat(results1Date, greaterThan(beforeFirst)); + assertThat(results1Date, lessThan(new Date())); assertThat(results1.getId(), not(blankOrNullString())); Patient pt2 = new Patient(); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java deleted file mode 100644 index 0bb32a97fe5..00000000000 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/LatchedService.java +++ /dev/null @@ -1,72 +0,0 @@ -package ca.uhn.fhir.jpa.subscription.module; - -import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; -import ca.uhn.fhir.jpa.model.interceptor.api.IAnonymousLambdaHook; -import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertTrue; - -public class LatchedService implements IAnonymousLambdaHook { - private static final Logger ourLog = LoggerFactory.getLogger(LatchedService.class); - private static final int DEFAULT_TIMEOUT_SECONDS = 20; - private final String name; - - private CountDownLatch myCountdownLatch; - private AtomicReference<String> myFailure; - private AtomicReference<List<HookParams>> myCalledWith; - - public LatchedService(Pointcut thePointcut) { - this.name = thePointcut.name(); - } - - public LatchedService(String theName) { - this.name = theName; - } - - public void countdown() { - if (myCountdownLatch == null) { - myFailure.set(name + " latch countdown() called before expectedCount set."); - } else if (myCountdownLatch.getCount() <= 0) { - myFailure.set(name + " latch countdown() called "+ (1 - myCountdownLatch.getCount()) + " more times than expected."); - } - ourLog.info("{} counting down {}", name, myCountdownLatch); - myCountdownLatch.countDown(); - } - - public void setExpectedCount(int count) { - myFailure = new AtomicReference<>(); - myCalledWith = new AtomicReference<>(new ArrayList<>()); - myCountdownLatch = new CountDownLatch(count); - } - - public void awaitExpected() throws InterruptedException { - awaitExpectedWithTimeout(DEFAULT_TIMEOUT_SECONDS); - } - - public void awaitExpectedWithTimeout(int timeoutSecond) throws InterruptedException { - assertTrue(name +" latch timed out waiting "+timeoutSecond+" seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); - - if (myFailure.get() != null) { - String error = myFailure.get(); - error += "\nLatch called with values: "+myCalledWith.get().stream().map(Object::toString).collect(Collectors.joining(", ")); - throw new AssertionError(error); - } - } - - @Override - public void invoke(HookParams theArgs) { - this.countdown(); - if (myCalledWith.get() != null) { - myCalledWith.get().add(theArgs); - } - } -} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java new file mode 100644 index 00000000000..a8d6f91468e --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java @@ -0,0 +1,95 @@ +package ca.uhn.fhir.jpa.subscription.module; + +import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; +import ca.uhn.fhir.jpa.model.interceptor.api.IAnonymousLambdaHook; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class PointcutLatch implements IAnonymousLambdaHook { + private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class); + private static final int DEFAULT_TIMEOUT_SECONDS = 10; + private final String name; + + private Semaphore mySemaphore = new Semaphore(1); + private CountDownLatch myCountdownLatch; + private AtomicReference<String> myFailure; + private AtomicReference<List<HookParams>> myCalledWith; + + public PointcutLatch(Pointcut thePointcut) { + this.name = thePointcut.name(); + } + + public PointcutLatch(String theName) { + this.name = theName; + } + + private void countdown() { + if (myCountdownLatch == null) { + myFailure.set(name + " latch countdown() called before expectedCount set."); + } else if (myCountdownLatch.getCount() <= 0) { + myFailure.set(name + " latch countdown() called "+ (1 - myCountdownLatch.getCount()) + " more times than expected."); + } + ourLog.info("{} counting down {}", name, myCountdownLatch); + myCountdownLatch.countDown(); + } + + public void setExpectedCount(int count) throws InterruptedException { + mySemaphore.acquire(); + if (myCountdownLatch != null) { + myFailure.set(name + " latch setExpectedCount() called before previous awaitExpected() completed."); + } + myFailure = new AtomicReference<>(); + myCalledWith = new AtomicReference<>(new ArrayList<>()); + myCountdownLatch = new CountDownLatch(count); + } + + public void awaitExpected() throws InterruptedException { + awaitExpected(true); + } + + public void awaitExpected(boolean release) throws InterruptedException { + awaitExpectedWithTimeout(DEFAULT_TIMEOUT_SECONDS, release); + } + + public void awaitExpectedWithTimeout(int timeoutSecond, boolean release) throws InterruptedException { + try { + assertNotNull(name + " latch awaitExpected() called before previous setExpected() called.", myCountdownLatch); + assertTrue(name + " latch timed out waiting " + timeoutSecond + " seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); + + if (myFailure.get() != null) { + String error = myFailure.get(); + error += "\nLatch called with values: " + myCalledWith.get().stream().map(Object::toString).collect(Collectors.joining(", ")); + throw new AssertionError(error); + } + } finally { + if (release) { + release(); + } + } + } + + public void release() { + myCountdownLatch = null; + mySemaphore.release(); + } + + @Override + public void invoke(HookParams theArgs) { + this.countdown(); + if (myCalledWith.get() != null) { + myCalledWith.get().add(theArgs); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index 4cb096dba32..4269641541a 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -1,15 +1,14 @@ package ca.uhn.fhir.jpa.subscription.module.standalone; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; -import ca.uhn.fhir.jpa.subscription.module.LatchedService; +import ca.uhn.fhir.jpa.subscription.module.PointcutLatch; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionChannelFactory; -import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage; -import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriber; import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriberTest; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.ResourceParam; @@ -65,8 +64,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base private static SubscribableChannel ourSubscribableChannel; private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); private long idCounter = 0; - protected LatchedService mySubscriptionMatchingPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED); - protected LatchedService mySubscriptionActivatedPost = new LatchedService(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); + protected PointcutLatch mySubscriptionMatchingPost = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED); + protected PointcutLatch mySubscriptionActivatedPost = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); @Before public void beforeReset() { @@ -165,7 +164,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base public static class ObservationListener implements IResourceProvider { - private LatchedService updateLatch = new LatchedService("Observation Update"); + private PointcutLatch updateLatch = new PointcutLatch("Observation Update"); @Create public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { @@ -184,17 +183,21 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); ourUpdatedObservations.add(theObservation); - updateLatch.countdown(); + updateLatch.invoke(new HookParams().add(Observation.class, theObservation)); ourLog.info("Received Listener Update (now have {} updates)", ourUpdatedObservations.size()); return new MethodOutcome(new IdType("Observation/1"), false); } - public void setExpectedCount(int count) { + public void setExpectedCount(int count) throws InterruptedException { updateLatch.setExpectedCount(count); } - public void awaitExpected() throws InterruptedException { - updateLatch.awaitExpected(); + public void awaitExpected(boolean release) throws InterruptedException { + updateLatch.awaitExpected(release); + } + + public void release() { + updateLatch.release(); } } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java index 58e5f696f38..5af2d728dff 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java @@ -27,9 +27,10 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(); + ourObservationListener.awaitExpected(false); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + ourObservationListener.release(); } @Test @@ -45,9 +46,10 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(); + ourObservationListener.awaitExpected(false); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); + ourObservationListener.release(); } @Test @@ -65,6 +67,6 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri mySubscriptionMatchingPost.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); mySubscriptionMatchingPost.awaitExpected(); - ourObservationListener.awaitExpected(); + ourObservationListener.awaitExpected(true); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java index 0bd4ac035c1..8fd2e436e4e 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java @@ -27,9 +27,10 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(); + ourObservationListener.awaitExpected(false); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + ourObservationListener.release(); } @Test @@ -45,9 +46,10 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(); + ourObservationListener.awaitExpected(false); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); + ourObservationListener.release(); } @Test @@ -65,6 +67,6 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri mySubscriptionMatchingPost.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); mySubscriptionMatchingPost.awaitExpected(); - ourObservationListener.awaitExpected(); + ourObservationListener.awaitExpected(true); } } From eee4c75300c6a74a4cd9b99dd0d3ff47de936ff3 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Wed, 23 Jan 2019 05:57:20 -0500 Subject: [PATCH 42/56] Version bump hibernate --- pom.xml | 2 +- src/changes/changes.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e99d52fdaf3..d9795272a1f 100644 --- a/pom.xml +++ b/pom.xml @@ -534,7 +534,7 @@ <jetty_version>9.4.14.v20181114</jetty_version> <jsr305_version>3.0.2</jsr305_version> <!--<hibernate_version>5.2.10.Final</hibernate_version>--> - <hibernate_version>5.4.0.Final</hibernate_version> + <hibernate_version>5.4.1.Final</hibernate_version> <!-- Update lucene version when you update hibernate-search version --> <hibernate_search_version>5.11.0.Final</hibernate_search_version> <lucene_version>5.5.5</lucene_version> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 6646dde15ba..b16733c2619 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -19,7 +19,7 @@ <ul> <li>Spring (JPA): 5.0.8.RELEASE -> 5.1.3.RELEASE</li> <li>Spring-Data (JPA): 2.0.7.RELEASE -> 2.1.3.RELEASE</li> - <li>Hibernate-Core (JPA): 5.3.6.FINAL -> 5.4.0.FINAL</li> + <li>Hibernate-Core (JPA): 5.3.6.FINAL -> 5.4.1.FINAL</li> <li>Hibernate-Search (JPA): 5.10.3.FINAL -> 5.11.0.FINAL</li> <li>Thymeleaf (JPA): 3.0.9.RELEASE -> 3.0.11.RELEASE</li> <li>thymeleaf-spring4 (Testpage Overlay) has been replaced with thymeleaf-spring5</li> From 68fae08f204f72386975294a50e1add1812f8504 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Wed, 23 Jan 2019 08:33:37 -0500 Subject: [PATCH 43/56] Fix casing in base64Binary type --- .../fhir/dstu3/model/Base64BinaryType.java | 2 +- .../ca/uhn/fhir/model/ModelDstu3Test.java | 8 +- .../hl7/fhir/r4/model/Base64BinaryType.java | 156 +++++++++--------- .../org/hl7/fhir/r4/model/ModelR4Test.java | 19 +++ src/changes/changes.xml | 4 + 5 files changed, 109 insertions(+), 80 deletions(-) create mode 100644 hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/model/ModelR4Test.java diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/Base64BinaryType.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/Base64BinaryType.java index 6c07d2f0a5e..0c92c6d0c0c 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/Base64BinaryType.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/model/Base64BinaryType.java @@ -35,7 +35,7 @@ import ca.uhn.fhir.model.api.annotation.DatatypeDef; /** * Primitive type "base64Binary" in FHIR: a sequence of bytes represented in base64 */ -@DatatypeDef(name = "base64binary") +@DatatypeDef(name = "base64Binary") public class Base64BinaryType extends PrimitiveType<byte[]> { private static final long serialVersionUID = 3L; diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/model/ModelDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/model/ModelDstu3Test.java index 0e2c2edb6f4..67a7679c23f 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/model/ModelDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/model/ModelDstu3Test.java @@ -46,7 +46,13 @@ public class ModelDstu3Test { @Test public void testSetters() { Claim claim = new Claim(); - claim.setIdentifier(new ArrayList<Identifier>()).setCareTeam(new ArrayList<CareTeamComponent>()); + claim.setIdentifier(new ArrayList<>()).setCareTeam(new ArrayList<>()); + } + + @Test + public void testbase64BinaryName() { + assertEquals("base64Binary", ourCtx.getElementDefinition("base64binary").getName()); + assertEquals("base64Binary", ourCtx.getElementDefinition("base64Binary").getName()); } @Test diff --git a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/model/Base64BinaryType.java b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/model/Base64BinaryType.java index e0813445603..36f7a237b12 100644 --- a/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/model/Base64BinaryType.java +++ b/hapi-fhir-structures-r4/src/main/java/org/hl7/fhir/r4/model/Base64BinaryType.java @@ -1,78 +1,78 @@ -/* -Copyright (c) 2011+, HL7, Inc -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of HL7 nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. - - */ -package org.hl7.fhir.r4.model; - -import org.apache.commons.codec.binary.Base64; - -import ca.uhn.fhir.model.api.annotation.DatatypeDef; - -/** - * Primitive type "base64Binary" in FHIR: a sequence of bytes represented in base64 - */ -@DatatypeDef(name="base64binary") -public class Base64BinaryType extends PrimitiveType<byte[]> { - - private static final long serialVersionUID = 3L; - - /** - * Constructor - */ - public Base64BinaryType() { - super(); - } - - public Base64BinaryType(byte[] theBytes) { - super(); - setValue(theBytes); - } - - public Base64BinaryType(String theValue) { - super(); - setValueAsString(theValue); - } - - protected byte[] parse(String theValue) { - return Base64.decodeBase64(theValue); - } - - protected String encode(byte[] theValue) { - return Base64.encodeBase64String(theValue); - } - - @Override - public Base64BinaryType copy() { - Base64BinaryType ret = new Base64BinaryType(getValue()); - copyValues(ret); - return ret; - } - - public String fhirType() { - return "base64Binary"; - } -} +/* +Copyright (c) 2011+, HL7, Inc +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of HL7 nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + */ +package org.hl7.fhir.r4.model; + +import org.apache.commons.codec.binary.Base64; + +import ca.uhn.fhir.model.api.annotation.DatatypeDef; + +/** + * Primitive type "base64Binary" in FHIR: a sequence of bytes represented in base64 + */ +@DatatypeDef(name="base64Binary") +public class Base64BinaryType extends PrimitiveType<byte[]> { + + private static final long serialVersionUID = 3L; + + /** + * Constructor + */ + public Base64BinaryType() { + super(); + } + + public Base64BinaryType(byte[] theBytes) { + super(); + setValue(theBytes); + } + + public Base64BinaryType(String theValue) { + super(); + setValueAsString(theValue); + } + + protected byte[] parse(String theValue) { + return Base64.decodeBase64(theValue); + } + + protected String encode(byte[] theValue) { + return Base64.encodeBase64String(theValue); + } + + @Override + public Base64BinaryType copy() { + Base64BinaryType ret = new Base64BinaryType(getValue()); + copyValues(ret); + return ret; + } + + public String fhirType() { + return "base64Binary"; + } +} diff --git a/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/model/ModelR4Test.java b/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/model/ModelR4Test.java new file mode 100644 index 00000000000..3610cad6327 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/org/hl7/fhir/r4/model/ModelR4Test.java @@ -0,0 +1,19 @@ +package org.hl7.fhir.r4.model; + +import ca.uhn.fhir.context.FhirContext; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ModelR4Test { + + private static FhirContext ourCtx = FhirContext.forR4(); + + @Test + public void testbase64BinaryName() { + assertEquals("base64Binary", ourCtx.getElementDefinition("base64binary").getName()); + assertEquals("base64Binary", ourCtx.getElementDefinition("base64Binary").getName()); + } + + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b16733c2619..072f559de66 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -320,6 +320,10 @@ result in the 10th result being returned). This will now result in an empty response Bundle as would be expected. </action> + <action type="fix"> + The casing of the base64Binary datatype was incorrect in the DSTU3 and R4 model classes. + This has been corrected. + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From afaa1b065f5cd9edb2294a130fa1118305aad69c Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Wed, 23 Jan 2019 09:38:49 -0500 Subject: [PATCH 44/56] Hivernate back to 5.4.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d9795272a1f..e99d52fdaf3 100644 --- a/pom.xml +++ b/pom.xml @@ -534,7 +534,7 @@ <jetty_version>9.4.14.v20181114</jetty_version> <jsr305_version>3.0.2</jsr305_version> <!--<hibernate_version>5.2.10.Final</hibernate_version>--> - <hibernate_version>5.4.1.Final</hibernate_version> + <hibernate_version>5.4.0.Final</hibernate_version> <!-- Update lucene version when you update hibernate-search version --> <hibernate_search_version>5.11.0.Final</hibernate_search_version> <lucene_version>5.5.5</lucene_version> From 707bf0709936f88105a49b4599d234f2e04cbaf8 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Wed, 23 Jan 2019 11:57:59 -0500 Subject: [PATCH 45/56] optimistic the windows tests will now finally pass! --- .../jpa/model/interceptor/api/HookParams.java | 5 + .../module/cache/ActiveSubscriptionCache.java | 6 + .../module/cache/SubscriptionRegistry.java | 6 + .../subscription/module/PointcutLatch.java | 132 +++++++++++++----- ...kingQueueSubscribableChannelDstu3Test.java | 29 ++-- .../SubscriptionCheckingSubscriberTest.java | 22 ++- .../SubscriptionMatchingSubscriberTest.java | 20 ++- 7 files changed, 156 insertions(+), 64 deletions(-) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java index 5c97f1ae081..5156e67c66a 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java @@ -23,6 +23,8 @@ package ca.uhn.fhir.jpa.model.interceptor.api; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -73,4 +75,7 @@ public class HookParams { return myParams.values().stream().map(t -> t.getClass().getSimpleName()).collect(Collectors.toList()); } + public Collection<Object> values() { + return Collections.unmodifiableCollection(myParams.values()); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java index 0aa92298c04..6afcd485850 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/ActiveSubscriptionCache.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * #L% */ +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import java.util.ArrayList; @@ -69,4 +70,9 @@ public class ActiveSubscriptionCache { } } } + + @VisibleForTesting + public void clearForUnitTests() { + myCache.clear(); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index 29c6e939d2c..7c9b9b5da50 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -155,4 +156,9 @@ public class SubscriptionRegistry { public int size() { return myActiveSubscriptionCache.size(); } + + @VisibleForTesting + public void clearForUnitTests() { + myActiveSubscriptionCache.clearForUnitTests(); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java index a8d6f91468e..6c0a5108203 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java @@ -1,17 +1,19 @@ package ca.uhn.fhir.jpa.subscription.module; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; import ca.uhn.fhir.jpa.model.interceptor.api.IAnonymousLambdaHook; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Collectors; import static org.junit.Assert.assertNotNull; @@ -22,7 +24,6 @@ public class PointcutLatch implements IAnonymousLambdaHook { private static final int DEFAULT_TIMEOUT_SECONDS = 10; private final String name; - private Semaphore mySemaphore = new Semaphore(1); private CountDownLatch myCountdownLatch; private AtomicReference<String> myFailure; private AtomicReference<List<HookParams>> myCalledWith; @@ -35,61 +36,116 @@ public class PointcutLatch implements IAnonymousLambdaHook { this.name = theName; } - private void countdown() { - if (myCountdownLatch == null) { - myFailure.set(name + " latch countdown() called before expectedCount set."); - } else if (myCountdownLatch.getCount() <= 0) { - myFailure.set(name + " latch countdown() called "+ (1 - myCountdownLatch.getCount()) + " more times than expected."); + public void setExpectedCount(int count) throws InterruptedException { + if (myCountdownLatch != null) { + throw new PointcutLatchException("setExpectedCount() called before previous awaitExpected() completed."); } - ourLog.info("{} counting down {}", name, myCountdownLatch); - myCountdownLatch.countDown(); + createLatch(count); } - public void setExpectedCount(int count) throws InterruptedException { - mySemaphore.acquire(); - if (myCountdownLatch != null) { - myFailure.set(name + " latch setExpectedCount() called before previous awaitExpected() completed."); - } + private void createLatch(int count) { myFailure = new AtomicReference<>(); myCalledWith = new AtomicReference<>(new ArrayList<>()); myCountdownLatch = new CountDownLatch(count); } - public void awaitExpected() throws InterruptedException { - awaitExpected(true); - } - - public void awaitExpected(boolean release) throws InterruptedException { - awaitExpectedWithTimeout(DEFAULT_TIMEOUT_SECONDS, release); - } - - public void awaitExpectedWithTimeout(int timeoutSecond, boolean release) throws InterruptedException { - try { - assertNotNull(name + " latch awaitExpected() called before previous setExpected() called.", myCountdownLatch); - assertTrue(name + " latch timed out waiting " + timeoutSecond + " seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); - - if (myFailure.get() != null) { - String error = myFailure.get(); - error += "\nLatch called with values: " + myCalledWith.get().stream().map(Object::toString).collect(Collectors.joining(", ")); - throw new AssertionError(error); - } - } finally { - if (release) { - release(); - } + private void setFailure(String failure) { + if (myFailure != null) { + myFailure.set(failure); + } else { + throw new PointcutLatchException("trying to set failure on latch that hasn't been created: " + failure); } } - public void release() { + private String getName() { + return name + " " + this.getClass().getSimpleName(); + } + + public void awaitExpected() throws InterruptedException { + awaitExpectedWithTimeout(DEFAULT_TIMEOUT_SECONDS); + } + + public void awaitExpectedWithTimeout(int timeoutSecond) throws InterruptedException { + try { + assertNotNull(getName() + " awaitExpected() called before setExpected() called.", myCountdownLatch); + assertTrue(getName() + " timed out waiting " + timeoutSecond + " seconds for latch to be triggered.", myCountdownLatch.await(timeoutSecond, TimeUnit.SECONDS)); + + if (myFailure.get() != null) { + String error = getName() + ": " + myFailure.get(); + error += "\nLatch called with values: " + myCalledWithString(); + throw new AssertionError(error); + } + } finally { + destroyLatch(); + } + } + + public void expectNothing() { + destroyLatch(); + } + + private void destroyLatch() { myCountdownLatch = null; - mySemaphore.release(); + } + + private String myCalledWithString() { + if (myCalledWith == null) { + return "[]"; + } + List<HookParams> calledWith = myCalledWith.get(); + if (calledWith.isEmpty()) { + return "[]"; + } + String retVal = "[ "; + retVal += calledWith.stream().flatMap(hookParams -> hookParams.values().stream()).map(itemToString()).collect(Collectors.joining(", ")); + return retVal + " ]"; + } + + private static Function<Object, String> itemToString() { + return object -> { + if (object instanceof IBaseResource) { + IBaseResource resource = (IBaseResource) object; + return "Resource " + resource.getIdElement().getValue(); + } else if (object instanceof ResourceModifiedMessage) { + ResourceModifiedMessage resourceModifiedMessage = (ResourceModifiedMessage)object; + // FIXME KHS can we get the context from the payload? + return "ResourceModified Message { " + resourceModifiedMessage.getOperationType() + ", " + resourceModifiedMessage.getNewPayload(FhirContext.forDstu3()).getIdElement().getValue() + "}"; + } else { + return object.toString(); + } + }; } @Override public void invoke(HookParams theArgs) { + if (myCountdownLatch == null) { + throw new PointcutLatchException("countdown() called before setExpectedCount() called.", theArgs); + } else if (myCountdownLatch.getCount() <= 0) { + setFailure("countdown() called " + (1 - myCountdownLatch.getCount()) + " more times than expected."); + } + this.countdown(); if (myCalledWith.get() != null) { myCalledWith.get().add(theArgs); } } + + private void countdown() { + ourLog.info("{} counting down {}", name, myCountdownLatch); + myCountdownLatch.countDown(); + } + + private class PointcutLatchException extends IllegalStateException { + public PointcutLatchException(String message, HookParams theArgs) { + super(getName() + ": " + message + " called with values: " + hookParamsToString(theArgs)); + } + + public PointcutLatchException(String message) { + super(getName() + ": " + message); + } + } + + private static String hookParamsToString(HookParams hookParams) { + return hookParams.values().stream().map(itemToString()).collect(Collectors.joining(", ")); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index 4269641541a..f2608b12f95 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; import ca.uhn.fhir.jpa.subscription.module.PointcutLatch; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriberTest; import ca.uhn.fhir.rest.annotation.Create; @@ -38,6 +39,7 @@ import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends BaseSubscriptionDstu3Test { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatchingSubscriberTest.class); @@ -51,6 +53,9 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base SubscriptionChannelFactory mySubscriptionChannelFactory; @Autowired InterceptorRegistry myInterceptorRegistry; + @Autowired + protected SubscriptionRegistry mySubscriptionRegistry; + protected String myCode = "1000000050"; @@ -63,7 +68,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base protected static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>()); private static SubscribableChannel ourSubscribableChannel; private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); - private long idCounter = 0; + private static AtomicLong idCounter = new AtomicLong(); protected PointcutLatch mySubscriptionMatchingPost = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED); protected PointcutLatch mySubscriptionActivatedPost = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); @@ -72,6 +77,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base ourCreatedObservations.clear(); ourUpdatedObservations.clear(); ourContentTypes.clear(); + mySubscriptionRegistry.clearForUnitTests(); if (ourSubscribableChannel == null) { ourSubscribableChannel = mySubscriptionChannelFactory.newDeliveryChannel("test", Subscription.SubscriptionChannelType.RESTHOOK.toCode().toLowerCase()); ourSubscribableChannel.subscribe(myStandaloneSubscriptionMessageHandler); @@ -85,10 +91,12 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base myInterceptorRegistry.clearAnonymousHookForUnitTest(); } - public <T extends IBaseResource> T sendResource(T theResource) { + public <T extends IBaseResource> T sendResource(T theResource) throws InterruptedException { ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); ResourceModifiedJsonMessage message = new ResourceModifiedJsonMessage(msg); + mySubscriptionMatchingPost.setExpectedCount(1); ourSubscribableChannel.send(message); + mySubscriptionMatchingPost.awaitExpected(); return theResource; } @@ -105,8 +113,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE); subscription.setCriteria(theCriteria); - ++idCounter; - IdType id = new IdType("Subscription", idCounter); + IdType id = new IdType("Subscription", idCounter.incrementAndGet()); subscription.setId(id); Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent(); @@ -117,10 +124,9 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base return subscription; } - protected Observation sendObservation(String code, String system) { + protected Observation sendObservation(String code, String system) throws InterruptedException { Observation observation = new Observation(); - ++idCounter; - IdType id = new IdType("Observation", idCounter); + IdType id = new IdType("Observation", idCounter.incrementAndGet()); observation.setId(id); CodeableConcept codeableConcept = new CodeableConcept(); @@ -134,7 +140,6 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base return sendResource(observation); } - @BeforeClass public static void startListenerServer() throws Exception { ourListenerPort = PortUtil.findFreePort(); @@ -192,12 +197,12 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base updateLatch.setExpectedCount(count); } - public void awaitExpected(boolean release) throws InterruptedException { - updateLatch.awaitExpected(release); + public void awaitExpected() throws InterruptedException { + updateLatch.awaitExpected(); } - public void release() { - updateLatch.release(); + public void expectNothing() { + updateLatch.expectNothing(); } } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java index 5af2d728dff..d3fe0ba2a38 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java @@ -1,10 +1,12 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.standalone.BaseBlockingQueueSubscribableChannelDstu3Test; import ca.uhn.fhir.rest.api.Constants; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import static org.junit.Assert.assertEquals; @@ -25,12 +27,14 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + assertEquals(2, mySubscriptionRegistry.size()); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(false); + ourObservationListener.awaitExpected(); + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); - ourObservationListener.release(); } @Test @@ -44,12 +48,14 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + assertEquals(2, mySubscriptionRegistry.size()); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(false); + ourObservationListener.awaitExpected(); + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); - ourObservationListener.release(); } @Test @@ -63,10 +69,12 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + assertEquals(2, mySubscriptionRegistry.size()); + ourObservationListener.setExpectedCount(0); - mySubscriptionMatchingPost.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - mySubscriptionMatchingPost.awaitExpected(); - ourObservationListener.awaitExpected(true); + ourObservationListener.expectNothing(); + + assertEquals(0, ourContentTypes.size()); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java index 8fd2e436e4e..446b903e4a3 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java @@ -25,12 +25,14 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + assertEquals(2, mySubscriptionRegistry.size()); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(false); + ourObservationListener.awaitExpected(); + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); - ourObservationListener.release(); } @Test @@ -44,12 +46,14 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + assertEquals(2, mySubscriptionRegistry.size()); + ourObservationListener.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - ourObservationListener.awaitExpected(false); + ourObservationListener.awaitExpected(); + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); - ourObservationListener.release(); } @Test @@ -63,10 +67,12 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); + assertEquals(2, mySubscriptionRegistry.size()); + ourObservationListener.setExpectedCount(0); - mySubscriptionMatchingPost.setExpectedCount(1); sendObservation(code, "SNOMED-CT"); - mySubscriptionMatchingPost.awaitExpected(); - ourObservationListener.awaitExpected(true); + ourObservationListener.expectNothing(); + + assertEquals(0, ourContentTypes.size()); } } From 5a08593abdf258850fdf3d01447000364e83bf33 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Wed, 23 Jan 2019 14:37:34 -0500 Subject: [PATCH 46/56] _id support (#1176) * added support for _id in in-memory matcher --- .../cache/SubscriptionCanonicalizer.java | 6 +-- .../matcher/CriteriaResourceMatcher.java | 54 +++++++++++++------ .../matcher/InMemorySubscriptionMatcher.java | 4 +- ...scriptionDeliveringRestHookSubscriber.java | 4 +- .../InMemorySubscriptionMatcherTestR3.java | 20 +++---- src/changes/changes.xml | 3 ++ 6 files changed, 56 insertions(+), 35 deletions(-) diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index b7a3f09ce7c..4199054d76b 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java @@ -63,7 +63,7 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { } } - protected CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) { + private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) { ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription; CanonicalSubscription retVal = new CanonicalSubscription(); @@ -82,7 +82,7 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { return retVal; } - protected CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) { + private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) { org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription; CanonicalSubscription retVal = new CanonicalSubscription(); @@ -210,7 +210,7 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { return null; } - protected CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) { + private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) { org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription; CanonicalSubscription retVal = new CanonicalSubscription(); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java index 3e8fabb9aba..34f1793fede 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; * #L% */ +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; @@ -30,8 +31,11 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.param.BaseParamWithPrefix; import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -42,16 +46,18 @@ import java.util.function.Predicate; @Service public class CriteriaResourceMatcher { - public static final String CRITERIA = "CRITERIA"; + private static final String CRITERIA = "CRITERIA"; @Autowired private MatchUrlService myMatchUrlService; @Autowired ISearchParamRegistry mySearchParamRegistry; + @Autowired + FhirContext myFhirContext; - public SubscriptionMatchResult match(String theCriteria, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) { + public SubscriptionMatchResult match(String theCriteria, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) { SearchParameterMap searchParameterMap; try { - searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, theResourceDefinition); + searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, myFhirContext.getResourceDefinition(theResource)); } catch (UnsupportedOperationException e) { return new SubscriptionMatchResult(theCriteria, CRITERIA); } @@ -63,7 +69,7 @@ public class CriteriaResourceMatcher { for (Map.Entry<String, List<List<? extends IQueryParameterType>>> entry : searchParameterMap.entrySet()) { String theParamName = entry.getKey(); List<List<? extends IQueryParameterType>> theAndOrParams = entry.getValue(); - SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, theSearchParams); + SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResource, theSearchParams); if (!result.matched()){ return result; } @@ -72,7 +78,7 @@ public class CriteriaResourceMatcher { } // This method is modelled from SearchBuilder.searchForIdsWithAndOr() - private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) { + private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) { if (theAndOrParams.isEmpty()) { return new SubscriptionMatchResult(true, CRITERIA); } @@ -90,30 +96,44 @@ public class CriteriaResourceMatcher { if (hasChain(theAndOrParams)) { return new SubscriptionMatchResult(theParamName, "Chained references are not supported"); } - if (theParamName.equals(IAnyResource.SP_RES_ID)) { + switch (theParamName) { + case IAnyResource.SP_RES_ID: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return new SubscriptionMatchResult(matchIdsAndOr(theAndOrParams, theResource), CRITERIA); - } else if (theParamName.equals(IAnyResource.SP_RES_LANGUAGE)) { + case IAnyResource.SP_RES_LANGUAGE: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return new SubscriptionMatchResult(theParamName, CRITERIA); - } else if (theParamName.equals(Constants.PARAM_HAS)) { + case Constants.PARAM_HAS: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return new SubscriptionMatchResult(theParamName, CRITERIA); - } else if (theParamName.equals(Constants.PARAM_TAG) || theParamName.equals(Constants.PARAM_PROFILE) || theParamName.equals(Constants.PARAM_SECURITY)) { + case Constants.PARAM_TAG: + case Constants.PARAM_PROFILE: + case Constants.PARAM_SECURITY: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return new SubscriptionMatchResult(theParamName, CRITERIA); - } else { + default: - String resourceName = theResourceDefinition.getName(); - RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); - return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); + String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); + RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); + return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); } } + private boolean matchIdsAndOr(List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource) { + return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource)); + } + private boolean matchIdsOr(List<? extends IQueryParameterType> theOrParams, IBaseResource theResource) { + return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam)param).getValue(), theResource.getIdElement())); + } + + private boolean matchId(String theValue, IIdType theId) { + return theValue.equals(theId.getValue()) || theValue.equals(theId.getIdPart()); + } + private SubscriptionMatchResult matchResourceParam(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) { if (theParamDef != null) { switch (theParamDef.getParamType()) { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java index 414cac544b0..c7e789b0c37 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java @@ -21,7 +21,6 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; import ca.uhn.fhir.jpa.searchparam.extractor.ResourceLinkExtractor; @@ -61,7 +60,6 @@ public class InMemorySubscriptionMatcher implements ISubscriptionMatcher { ResourceIndexedSearchParams searchParams = new ResourceIndexedSearchParams(); mySearchParamExtractorService.extractFromResource(searchParams, entity, resource); myResourceLinkExtractor.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), myInlineResourceLinkResolver); - RuntimeResourceDefinition resourceDefinition = myContext.getResourceDefinition(resource); - return myCriteriaResourceMatcher.match(criteria, resourceDefinition, searchParams); + return myCriteriaResourceMatcher.match(criteria, resource, searchParams); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index 3358e8ab0cd..0667f7be9f2 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -101,8 +101,8 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe try { operation.execute(); } catch (ResourceNotFoundException e) { - ourLog.error("Cannot reach " + theMsg.getSubscription().getEndpointUrl()); - e.printStackTrace(); + ourLog.error("Cannot reach {} ", theMsg.getSubscription().getEndpointUrl()); + ourLog.error("Exception: ", e); throw e; } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java index eb0706b2001..ff6b32d436c 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java @@ -12,6 +12,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import java.util.Arrays; +import java.util.Collections; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -39,7 +40,6 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test } @Test - @Ignore public void testResourceById() { ProcedureRequest pr = new ProcedureRequest(); @@ -47,12 +47,12 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER); assertMatched(pr, "ProcedureRequest?_id=123"); - assertMatched(pr, "ProcedureRequest?_id=Patient/123"); - assertMatched(pr, "ProcedureRequest?_id=Patient/123,Patient/999"); - assertMatched(pr, "ProcedureRequest?_id=Patient/123&_id=Patient/123"); - assertNotMatched(pr, "ProcedureRequest?_id=Patient/888"); - assertNotMatched(pr, "ProcedureRequest?_id=Patient/888,Patient/999"); - assertNotMatched(pr, "ProcedureRequest?_id=Patient/123&_id=Patient/888"); + assertMatched(pr, "ProcedureRequest?_id=ProcedureRequest/123"); + assertMatched(pr, "ProcedureRequest?_id=ProcedureRequest/123,ProcedureRequest/999"); + assertMatched(pr, "ProcedureRequest?_id=ProcedureRequest/123&_id=ProcedureRequest/123"); + assertNotMatched(pr, "ProcedureRequest?_id=ProcedureRequest/888"); + assertNotMatched(pr, "ProcedureRequest?_id=ProcedureRequest/888,ProcedureRequest/999"); + assertNotMatched(pr, "ProcedureRequest?_id=ProcedureRequest/123&_id=ProcedureRequest/888"); } @@ -301,7 +301,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); - IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid"); initSearchParamRegistry(bundle); { @@ -333,7 +333,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); - IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid"); initSearchParamRegistry(bundle); { @@ -425,7 +425,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); - IBundleProvider bundle = new SimpleBundleProvider(Arrays.asList(sp), "uuid"); + IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid"); initSearchParamRegistry(bundle); { diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 072f559de66..aac1c8be1cc 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -320,6 +320,9 @@ result in the 10th result being returned). This will now result in an empty response Bundle as would be expected. </action> + <action type="add"> + Added support for _id in in-memory matcher + </action> <action type="fix"> The casing of the base64Binary datatype was incorrect in the DSTU3 and R4 model classes. This has been corrected. From 7d1d5a102ce5f75afda6556ec78d123dd0175f82 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Wed, 23 Jan 2019 16:14:53 -0500 Subject: [PATCH 47/56] Attempt to fix a weird lucene indexing issue --- .../jpa/provider/SystemProviderDstu2Test.java | 20 +++++++++++++++---- .../jpa/provider/r4/SystemProviderR4Test.java | 15 ++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index ae52fbdfcd3..48325472741 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java @@ -211,9 +211,11 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { obs.getCode().setText("ZXCVBNM ASDFGHJKL QWERTYUIOPASDFGHJKL"); myObservationDao.update(obs, mySrd); + // Try to wait for the indexing to complete + waitToParamsToIndex(ptId); + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + ptId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); - CloseableHttpResponse http = ourHttpClient.execute(get); - try { + try (CloseableHttpResponse http = ourHttpClient.execute(get)) { assertEquals(200, http.getStatusLine().getStatusCode()); String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); @@ -225,11 +227,21 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { assertEquals("score", parameters.getParameter().get(0).getPart().get(1).getName()); assertEquals(new DecimalDt("1.0"), parameters.getParameter().get(0).getPart().get(1).getValue()); - } finally { - http.close(); } } + private void waitToParamsToIndex(IIdType thePtId) throws Exception { + waitForSize(2, ()->{ + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + thePtId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); + try (CloseableHttpResponse http = ourHttpClient.execute(get)) { + assertEquals(200, http.getStatusLine().getStatusCode()); + String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); + Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); + return parameters.getParameter().size(); + } + }); + } + @Test public void testSuggestKeywordsInvalid() throws Exception { Patient patient = new Patient(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java index 137d05f9492..4389dc159cb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java @@ -279,6 +279,9 @@ public class SystemProviderR4Test extends BaseJpaR4Test { obs.getCode().setText("ZXCVBNM ASDFGHJKL QWERTYUIOPASDFGHJKL"); myObservationDao.update(obs, mySrd); + // Try to wait for the indexing to complete + waitForParamsToIndex(ptId); + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + ptId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); CloseableHttpResponse http = ourHttpClient.execute(get); try { @@ -298,6 +301,18 @@ public class SystemProviderR4Test extends BaseJpaR4Test { } } + private void waitForParamsToIndex(IIdType thePtId) throws Exception { + waitForSize(2, ()->{ + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + thePtId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); + try (CloseableHttpResponse http = ourHttpClient.execute(get)) { + assertEquals(200, http.getStatusLine().getStatusCode()); + String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); + Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); + return parameters.getParameter().size(); + } + }); + } + @Test public void testSuggestKeywordsInvalid() throws Exception { Patient patient = new Patient(); From 5c7907dfeae332a05f06d63dab7734e527455df4 Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Wed, 23 Jan 2019 16:17:41 -0500 Subject: [PATCH 48/56] magic number --- .../uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 aeb14f9a454..aee27813903 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 @@ -123,6 +123,7 @@ import ca.uhn.fhir.util.UrlUtil; public class ResourceProviderR4Test extends BaseResourceProviderR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderR4Test.class); + public static final int LARGE_NUMBER = 77; private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw; private CapturingInterceptor myCapturingInterceptor = new CapturingInterceptor(); @@ -1755,7 +1756,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { p.setActive(true); IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless(); - for (int i = 1; i < 77; i++) { + for (int i = 1; i < LARGE_NUMBER; i++) { Observation obs = new Observation(); obs.setId("A" + StringUtils.leftPad(Integer.toString(i), 2, '0')); obs.setSubject(new Reference(id)); @@ -1793,8 +1794,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { } assertThat(ids, hasItem(id.getIdPart())); - assertEquals(77, ids.size()); - for (int i = 1; i < 77; i++) { + assertEquals(LARGE_NUMBER, ids.size()); + for (int i = 1; i < LARGE_NUMBER; i++) { assertThat(ids.size() + " ids: " + ids, ids, hasItem("A" + StringUtils.leftPad(Integer.toString(i), 2, '0'))); } } From e819b83a94d6c064550edd6d7dfde376243f83f7 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Wed, 23 Jan 2019 16:21:13 -0500 Subject: [PATCH 49/56] Make busywait more clear --- .../jpa/provider/SystemProviderDstu2Test.java | 20 +++++++++---------- .../jpa/provider/r4/SystemProviderR4Test.java | 20 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index 48325472741..af487089ba1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java @@ -212,7 +212,7 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { myObservationDao.update(obs, mySrd); // Try to wait for the indexing to complete - waitToParamsToIndex(ptId); + waitForSize(2, ()-> fetchSuggestionCount(ptId)); HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + ptId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); try (CloseableHttpResponse http = ourHttpClient.execute(get)) { @@ -230,16 +230,14 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { } } - private void waitToParamsToIndex(IIdType thePtId) throws Exception { - waitForSize(2, ()->{ - HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + thePtId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); - try (CloseableHttpResponse http = ourHttpClient.execute(get)) { - assertEquals(200, http.getStatusLine().getStatusCode()); - String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); - Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); - return parameters.getParameter().size(); - } - }); + private Number fetchSuggestionCount(IIdType thePtId) throws IOException { + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + thePtId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); + try (CloseableHttpResponse http = ourHttpClient.execute(get)) { + assertEquals(200, http.getStatusLine().getStatusCode()); + String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); + Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); + return parameters.getParameter().size(); + } } @Test diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java index 4389dc159cb..3c4300011c7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java @@ -280,7 +280,7 @@ public class SystemProviderR4Test extends BaseJpaR4Test { myObservationDao.update(obs, mySrd); // Try to wait for the indexing to complete - waitForParamsToIndex(ptId); + waitForSize(2, ()-> fetchSuggestionCount(ptId)); HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + ptId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); CloseableHttpResponse http = ourHttpClient.execute(get); @@ -301,16 +301,14 @@ public class SystemProviderR4Test extends BaseJpaR4Test { } } - private void waitForParamsToIndex(IIdType thePtId) throws Exception { - waitForSize(2, ()->{ - HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + thePtId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); - try (CloseableHttpResponse http = ourHttpClient.execute(get)) { - assertEquals(200, http.getStatusLine().getStatusCode()); - String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); - Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); - return parameters.getParameter().size(); - } - }); + private Number fetchSuggestionCount(IIdType thePtId) throws IOException { + HttpGet get = new HttpGet(ourServerBase + "/$suggest-keywords?context=Patient/" + thePtId.getIdPart() + "/$everything&searchParam=_content&text=zxc&_pretty=true&_format=xml"); + try (CloseableHttpResponse http = ourHttpClient.execute(get)) { + assertEquals(200, http.getStatusLine().getStatusCode()); + String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); + Parameters parameters = ourCtx.newXmlParser().parseResource(Parameters.class, output); + return parameters.getParameter().size(); + } } @Test From 0eb70b81fe3a7559cc9ef2dfb8859c08d53220cd Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Wed, 23 Jan 2019 18:13:29 -0500 Subject: [PATCH 50/56] Oops. This was supposed to be automatically backed out. But SQUASH MERGE prevented that from happening. Hmmmmm...... --- .../subscription/module/CanonicalSubscription.java | 12 ------------ .../module/cache/SubscriptionCanonicalizer.java | 11 ----------- .../module/cache/SubscriptionConstants.java | 6 ------ 3 files changed, 29 deletions(-) diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java index 707a75aabc4..b055a20d5c4 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/CanonicalSubscription.java @@ -295,8 +295,6 @@ public class CanonicalSubscription implements Serializable, Cloneable { private boolean myStripVersionId; @JsonProperty("deliverLatestVersion") private boolean myDeliverLatestVersion; - @JsonProperty("maxRetries") - private int myMaxRetries; /** * Constructor @@ -322,14 +320,6 @@ public class CanonicalSubscription implements Serializable, Cloneable { myStripVersionId = theStripVersionId; } - public int getMaxRetries() { - return myMaxRetries; - } - - public void setMaxRetries(int theMaxRetries) { - myMaxRetries = theMaxRetries; - } - @Override public boolean equals(Object theO) { if (this == theO) return true; @@ -341,7 +331,6 @@ public class CanonicalSubscription implements Serializable, Cloneable { return new EqualsBuilder() .append(myStripVersionId, that.myStripVersionId) .append(myDeliverLatestVersion, that.myDeliverLatestVersion) - .append(myMaxRetries, that.myMaxRetries) .isEquals(); } @@ -350,7 +339,6 @@ public class CanonicalSubscription implements Serializable, Cloneable { return new HashCodeBuilder(17, 37) .append(myStripVersionId) .append(myDeliverLatestVersion) - .append(myMaxRetries) .toHashCode(); } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index 24a559b6a8f..0c87b0d0076 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java @@ -115,19 +115,14 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { String stripVersionIds; String deliverLatestVersion; - String maxRetries; try { stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); - maxRetries = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_MAX_RETRIES); } catch (FHIRException theE) { throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); } retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); - if (isNotBlank(maxRetries)) { - retVal.getRestHookDetails().setMaxRetries(Integer.parseInt(maxRetries)); - } } } catch (FHIRException theE) { @@ -246,20 +241,14 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { String stripVersionIds; String deliverLatestVersion; - String maxRetries; try { stripVersionIds = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS); deliverLatestVersion = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION); - maxRetries = subscription.getChannel().getExtensionString(SubscriptionConstants.EXT_SUBSCRIPTION_MAX_RETRIES); - } catch (FHIRException theE) { throw new ConfigurationException("Failed to extract subscription extension(s): " + theE.getMessage(), theE); } retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); - if (isNotBlank(maxRetries)) { - retVal.getRestHookDetails().setMaxRetries(Integer.parseInt(maxRetries)); - } } List<Extension> topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java index e785464508d..5d9f3f1ef20 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java @@ -67,12 +67,6 @@ public class SubscriptionConstants { */ public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version"; - /** - * This extension URL indicates the maximum number of delivery retries that will be attempted before the subscription delivery is considered to have failed. - */ - - public static final String EXT_SUBSCRIPTION_MAX_RETRIES = "http://hapifhir.io/fhir/StructureDefinition/subscription-max-retries"; - /** * The number of threads used in subscription channel processing */ From 38d03ea99ac5c38cd96f0afa19d4ccc1d36458b2 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Wed, 23 Jan 2019 21:17:47 -0500 Subject: [PATCH 51/56] Invalid ids in subscription queue (#1175) * Start work on this * Work on interceptors * Attempt fix * Avoid environment dependency * Test fixes * One more test fix * One more build tweak * Lots of cleanup * A bit more cleanup * Still more cleanup * Some test fixes * Add legacy methods temporarily * Don't auto-scan interceptor beans * One more test fix * rsolve merge conflicts * Address review comments --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 34 ++- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 67 +++-- .../uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java | 2 +- .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 1 - .../fhir/jpa/dao/TransactionProcessor.java | 105 ++++--- ...rchParamWithInlineReferencesExtractor.java | 2 +- .../reindex/ResourceReindexingSvcImpl.java | 1 - .../SubscriptionActivatingInterceptor.java | 19 +- .../SubscriptionInterceptorLoader.java | 11 +- .../SubscriptionMatcherInterceptor.java | 21 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 5 + .../test/InterceptorRegistryTest.java | 159 ----------- .../provider/r4/HookInterceptorR4Test.java | 110 +++++++ .../r4/ResourceProviderInterceptorR4Test.java | 1 + .../subscription/BaseSubscriptionsR4Test.java | 5 +- .../resthook/RestHookTestDstu2Test.java | 6 +- .../resthook/RestHookTestR4Test.java | 34 +++ .../RestHookWithInterceptorR4Test.java | 7 +- .../jpa/model/interceptor/api/HookParams.java | 9 +- .../api/IInterceptorBroadcaster.java | 35 +++ .../interceptor/api/IInterceptorRegistry.java | 47 +-- .../model/interceptor/api/Interceptor.java | 7 + .../jpa/model/interceptor/api/Pointcut.java | 96 ++++++- ...rRegistry.java => InterceptorService.java} | 223 ++++++++++----- .../executor/InterceptorServiceTest.java | 268 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 15 + .../extractor/ResourceLinkExtractor.java | 39 ++- .../fhir/jpa/searchparam/retry/Retrier.java | 1 + .../module/ResourceModifiedMessage.java | 37 ++- .../module/cache/SubscriptionRegistry.java | 8 +- .../matcher/InMemorySubscriptionMatcher.java | 2 +- ...scriptionDeliveringRestHookSubscriber.java | 7 +- .../SubscriptionMatchingSubscriber.java | 10 +- .../config/TestSubscriptionDstu3Config.java | 13 +- .../InMemorySubscriptionMatcherTestR3.java | 27 ++ ...kingQueueSubscribableChannelDstu3Test.java | 4 +- 36 files changed, 1022 insertions(+), 416 deletions(-) delete mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/HookInterceptorR4Test.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorBroadcaster.java rename hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/{InterceptorRegistry.java => InterceptorService.java} (58%) create mode 100644 hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java create mode 100644 hapi-fhir-jpaserver-model/src/test/resources/logback-test.xml 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 6bb1e235b7c..b9bd91e2eb6 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 @@ -7,6 +7,9 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; @@ -69,6 +72,8 @@ import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; import javax.persistence.*; @@ -115,7 +120,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, public static final String OO_SEVERITY_INFO = "information"; public static final String OO_SEVERITY_WARN = "warning"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirDao.class); - private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<FhirVersionEnum, FhirContext>(); + private static final Map<FhirVersionEnum, FhirContext> ourRetrievalContexts = new HashMap<>(); private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; private static boolean ourValidationDisabledForUnitTest; private static boolean ourDisableIncrementOnUpdateForUnitTest = false; @@ -125,6 +130,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, @Autowired protected IdHelperService myIdHelperService; @Autowired + protected IInterceptorBroadcaster myInterceptorBroadcaster; + @Autowired protected IForcedIdDao myForcedIdDao; @Autowired protected ISearchResultDao mySearchResultDao; @@ -1420,9 +1427,8 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true); } - public ResourceTable updateInternal(RequestDetails theRequest, T theResource, boolean thePerformIndexing, - boolean theForceUpdateVersion, RequestDetails theRequestDetails, ResourceTable theEntity, IIdType - theResourceId, IBaseResource theOldResource) { + public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion, + ResourceTable theEntity, IIdType theResourceId, IBaseResource theOldResource) { // Notify interceptors ActionRequestDetails requestDetails; if (theRequestDetails != null) { @@ -1439,9 +1445,13 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ((IServerOperationInterceptor) next).resourcePreUpdate(theRequestDetails, theOldResource, theResource); } } + HookParams hookParams = new HookParams() + .add(IBaseResource.class, theOldResource) + .add(IBaseResource.class, theResource); + myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, hookParams); // Perform update - ResourceTable savedEntity = updateEntity(theRequest, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing); + ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, theEntity, null, thePerformIndexing, thePerformIndexing, new Date(), theForceUpdateVersion, thePerformIndexing); /* * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), @@ -1470,7 +1480,17 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, ((IServerOperationInterceptor) next).resourceUpdated(theRequestDetails, theOldResource, theResource); } } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void beforeCommit(boolean readOnly) { + HookParams hookParams = new HookParams() + .add(IBaseResource.class, theOldResource) + .add(IBaseResource.class, theResource); + myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, hookParams); + } + }); } + return savedEntity; } @@ -1480,6 +1500,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao, id = getContext().getVersion().newIdType().setValue(id.getValue()); } + if (id.hasResourceType() == false) { + id = id.withResourceType(theEntity.getResourceType()); + } + theResource.setId(id); if (theResource instanceof IResource) { ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, id.getVersionIdPart()); 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 9cfd6afec1c..d55055aaa91 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 @@ -26,6 +26,8 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; @@ -59,6 +61,8 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Nonnull; @@ -173,7 +177,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B } @Override - public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theReques) { + public DaoMethodOutcome delete(IIdType theId, List<DeleteConflict> theDeleteConflicts, RequestDetails theRequest) { if (theId == null || !theId.hasIdPart()) { throw new InvalidRequestException("Can not perform delete, no ID provided"); } @@ -206,12 +210,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B T resourceToDelete = toResource(myResourceType, entity, null, false); // Notify IServerOperationInterceptors about pre-action call - if (theReques != null) { - theReques.getRequestOperationCallback().resourcePreDelete(resourceToDelete); + if (theRequest != null) { + theRequest.getRequestOperationCallback().resourcePreDelete(resourceToDelete); } for (IServerInterceptor next : getConfig().getInterceptors()) { if (next instanceof IServerOperationInterceptor) { - ((IServerOperationInterceptor) next).resourcePreDelete(theReques, resourceToDelete); + ((IServerOperationInterceptor) next).resourcePreDelete(theRequest, resourceToDelete); } } @@ -220,25 +224,33 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B preDelete(resourceToDelete, entity); // Notify interceptors - if (theReques != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theReques, getContext(), theId.getResourceType(), theId); + if (theRequest != null) { + ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId); notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); } Date updateTime = new Date(); - ResourceTable savedEntity = updateEntity(theReques, null, entity, updateTime, updateTime); + ResourceTable savedEntity = updateEntity(theRequest, null, entity, updateTime, updateTime); resourceToDelete.setId(entity.getIdDt()); // Notify JPA interceptors - if (theReques != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theReques, getContext(), theId.getResourceType(), theId); - theReques.getRequestOperationCallback().resourceDeleted(resourceToDelete); + if (theRequest != null) { + ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theId.getResourceType(), theId); + theRequest.getRequestOperationCallback().resourceDeleted(resourceToDelete); } for (IServerInterceptor next : getConfig().getInterceptors()) { if (next instanceof IServerOperationInterceptor) { - ((IServerOperationInterceptor) next).resourceDeleted(theReques, resourceToDelete); + ((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete); } } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void beforeCommit(boolean readOnly) { + HookParams hookParams = new HookParams() + .add(IBaseResource.class, resourceToDelete); + myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams); + } + }); DaoMethodOutcome outcome = toMethodOutcome(savedEntity, resourceToDelete).setCreated(true); @@ -321,6 +333,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B ((IServerOperationInterceptor) next).resourceDeleted(theRequest, resourceToDelete); } } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void beforeCommit(boolean readOnly) { + HookParams hookParams = new HookParams() + .add(IBaseResource.class, resourceToDelete); + myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED, hookParams); + } + }); } IBaseOperationOutcome oo; @@ -423,6 +443,9 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B ((IServerOperationInterceptor) next).resourcePreCreate(theRequest, theResource); } } + HookParams hookParams = new HookParams() + .add(IBaseResource.class, theResource); + myInterceptorBroadcaster.callHooks(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED, hookParams); // Perform actual DB update ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, thePerformIndexing, theUpdateTime, false, thePerformIndexing); @@ -466,6 +489,14 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B } } } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void beforeCommit(boolean readOnly) { + HookParams hookParams = new HookParams() + .add(IBaseResource.class, theResource); + myInterceptorBroadcaster.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, hookParams); + } + }); DaoMethodOutcome outcome = toMethodOutcome(entity, theResource).setCreated(true); if (!thePerformIndexing) { @@ -753,7 +784,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B return retVal; } - @SuppressWarnings("JpaQlInspection") @Override public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) { // Notify interceptors @@ -944,8 +974,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B if (entity == null) { if (theId.hasVersionIdPart()) { - TypedQuery<ResourceHistoryTable> q = myEntityManager - .createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class); + TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class); q.setParameter("RID", pid); q.setParameter("RTYP", myResourceName); q.setParameter("RVER", theId.getVersionIdPartAsLong()); @@ -1305,7 +1334,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B /* * Otherwise, we're not in a transaction */ - ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, theRequestDetails, entity, resourceId, oldResource); + ResourceTable savedEntity = updateInternal(theRequestDetails, theResource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource); DaoMethodOutcome outcome = toMethodOutcome(savedEntity, theResource).setCreated(false); if (!thePerformIndexing) { @@ -1320,13 +1349,13 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B } @Override - public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { - return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails); + public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { + return update(theResource, theMatchUrl, true, theRequestDetails); } @Override - public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { - return update(theResource, theMatchUrl, true, theRequestDetails); + public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { + return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails); } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 1d4e7456b9e..9db169e2236 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -492,7 +492,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> { InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(nextResource); Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; if (theUpdatedEntities.contains(nextOutcome.getEntity())) { - updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); + updateInternal(theRequestDetails, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); } else if (!theNonUpdatedEntities.contains(nextOutcome.getEntity())) { updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index cdcb55bb342..e5c0a909037 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -100,7 +100,6 @@ import static org.apache.commons.lang3.StringUtils.*; * The SearchBuilder is responsible for actually forming the SQL query that handles * searches for resources */ -@SuppressWarnings("JpaQlInspection") @Component @Scope("prototype") public class SearchBuilder implements ISearchBuilder { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index a8fd3445431..ea8b95368d5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -89,6 +89,52 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { @Autowired private DaoRegistry myDaoRegistry; + public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) { + if (theRequestDetails != null) { + IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); + myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); + } + + String actionName = "Transaction"; + BUNDLE response = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName); + + return response; + } + + public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) { + String transactionType = myVersionAdapter.getBundleType(theRequest); + + if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) { + throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType); + } + + ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size()); + long start = System.currentTimeMillis(); + + TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); + + List<IBaseResource> resources = new ArrayList<>(); + for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) { + IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry); + resources.add(resource); + } + + BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction"); + for (IBaseResource next : resources) { + BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle); + myVersionAdapter.setResource(entry, next); + myVersionAdapter.setRequestVerb(entry, "PUT"); + myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue()); + } + + transaction(theRequestDetails, transactionBundle); + + return resp; + } + private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) { myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry); } @@ -160,16 +206,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { myDao = theDao; } - public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) { - if (theRequestDetails != null) { - IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); - myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); - } - - String actionName = "Transaction"; - return processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName); - } - private BUNDLE processTransactionAsSubRequest(ServletRequestDetails theRequestDetails, BUNDLE theRequest, String theActionName) { BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails); try { @@ -179,40 +215,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { } } - public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) { - String transactionType = myVersionAdapter.getBundleType(theRequest); - - if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) { - throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType); - } - - ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size()); - long start = System.currentTimeMillis(); - - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - - BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); - - List<IBaseResource> resources = new ArrayList<>(); - for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) { - IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry); - resources.add(resource); - } - - BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction"); - for (IBaseResource next : resources) { - BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle); - myVersionAdapter.setResource(entry, next); - myVersionAdapter.setRequestVerb(entry, "PUT"); - myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue()); - } - - transaction(theRequestDetails, transactionBundle); - - return resp; - } - private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) { ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size()); long start = System.currentTimeMillis(); @@ -234,8 +236,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { BUNDLE subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); myVersionAdapter.addEntry(subRequestBundle, nextRequestEntry); - BUNDLE subResponseBundle = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); - return subResponseBundle; + return processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); }; try { @@ -472,10 +473,6 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); } - public void setEntityManager(EntityManager theEntityManager) { - myEntityManager = theEntityManager; - } - private void validateDependencies() { Validate.notNull(myEntityManager); Validate.notNull(myContext); @@ -526,7 +523,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { } } - if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { + if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) { throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); } @@ -631,7 +628,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); } res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); - outcome = resourceDao.update(res, null, false, theRequestDetails); + outcome = resourceDao.update(res, null, false, false, theRequestDetails); } else { res.setId((String) null); String matchUrl; @@ -641,7 +638,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { matchUrl = parts.getResourceType(); } matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); + outcome = resourceDao.update(res, matchUrl, false, false, theRequestDetails); if (Boolean.TRUE.equals(outcome.getCreated())) { conditionalRequestUrls.put(matchUrl, res.getClass()); } @@ -727,7 +724,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> { Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; if (updatedEntities.contains(nextOutcome.getEntity())) { - myDao.updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); + myDao.updateInternal(theRequestDetails, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java index 0990ac67708..00eaf381447 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/SearchParamWithInlineReferencesExtractor.java @@ -99,7 +99,7 @@ public class SearchParamWithInlineReferencesExtractor { extractInlineReferences(theResource); - myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver); + myResourceLinkExtractor.extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, myDaoResourceLinkResolver, true); /* * If the existing resource already has links and those match links we still want, use them instead of removing them and re adding them diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java index 2ab5fccfcfc..1ba5db9825d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/reindex/ResourceReindexingSvcImpl.java @@ -391,7 +391,6 @@ public class ResourceReindexingSvcImpl implements IResourceReindexingSvc { }); } - @SuppressWarnings("JpaQlInspection") private void markResourceAsIndexingFailed(final long theId) { TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java index 19b6e3ba1c1..49fb15d6895 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java @@ -26,6 +26,9 @@ import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.model.interceptor.api.Hook; +import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; @@ -68,7 +71,8 @@ import java.util.concurrent.TimeUnit; */ @Service @Lazy -public class SubscriptionActivatingInterceptor extends ServerOperationInterceptorAdapter { +@Interceptor(manualRegistration = true) +public class SubscriptionActivatingInterceptor { private Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingInterceptor.class); private static boolean ourWaitForSubscriptionActivationSynchronouslyForUnitTest; @@ -160,6 +164,7 @@ public class SubscriptionActivatingInterceptor extends ServerOperationIntercepto private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) { IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement()); + subscription.setId(subscription.getIdElement().toVersionless()); ourLog.info("Activating subscription {} from status {} to {}", subscription.getIdElement().toUnqualified().getValue(), theRequestedStatus, theActiveStatus); try { @@ -180,18 +185,18 @@ public class SubscriptionActivatingInterceptor extends ServerOperationIntercepto submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); } - @Override - public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED) + public void resourceCreated(IBaseResource theResource) { submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); } - @Override - public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED) + public void resourceDeleted(IBaseResource theResource) { submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); } - @Override - public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED) + public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java index feaad036450..5af1faf0591 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription; */ import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionLoader; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import com.google.common.annotations.VisibleForTesting; @@ -47,6 +48,8 @@ public class SubscriptionInterceptorLoader { private SubscriptionRegistry mySubscriptionRegistry; @Autowired private ApplicationContext myAppicationContext; + @Autowired + private IInterceptorRegistry myInterceptorRegistry; public void registerInterceptors() { Set<Subscription.SubscriptionChannelType> supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes(); @@ -54,12 +57,12 @@ public class SubscriptionInterceptorLoader { if (!supportedSubscriptionTypes.isEmpty()) { loadSubscriptions(); ourLog.info("Registering subscription activating interceptor"); - myDaoConfig.registerInterceptor(mySubscriptionActivatingInterceptor); + myInterceptorRegistry.registerInterceptor(mySubscriptionActivatingInterceptor); } if (myDaoConfig.isSubscriptionMatchingEnabled()) { mySubscriptionMatcherInterceptor.start(); ourLog.info("Registering subscription matcher interceptor"); - myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); + myInterceptorRegistry.registerInterceptor(mySubscriptionMatcherInterceptor); } } @@ -72,7 +75,7 @@ public class SubscriptionInterceptorLoader { @VisibleForTesting void unregisterInterceptorsForUnitTest() { - myDaoConfig.unregisterInterceptor(mySubscriptionActivatingInterceptor); - myDaoConfig.unregisterInterceptor(mySubscriptionMatcherInterceptor); + myInterceptorRegistry.unregisterInterceptor(mySubscriptionActivatingInterceptor); + myInterceptorRegistry.unregisterInterceptor(mySubscriptionMatcherInterceptor); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java index c6f34c017c0..94d4fc65600 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionMatcherInterceptor.java @@ -1,13 +1,14 @@ package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.model.interceptor.api.Hook; +import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.module.LinkedBlockingQueueSubscribableChannel; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.module.subscriber.SubscriptionMatchingSubscriber; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -42,7 +43,8 @@ import javax.annotation.PreDestroy; @Component @Lazy -public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter implements IResourceModifiedConsumer { +@Interceptor(manualRegistration = true) +public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer { private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class); private static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching"; @@ -82,18 +84,18 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd } } - @Override - public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED) + public void resourceCreated(IBaseResource theResource) { submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); } - @Override - public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED) + public void resourceDeleted(IBaseResource theResource) { submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); } - @Override - public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource theNewResource) { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED) + public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); } @@ -115,6 +117,7 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd /** * This is an internal API - Use with caution! */ + @Override public void submitResourceModified(final ResourceModifiedMessage theMsg) { sendToProcessingChannel(theMsg); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index b8ac0fee849..0da4e00e6d0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; @@ -150,6 +151,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { protected IFhirResourceDao<RiskAssessment> myRiskAssessmentDao; protected IServerInterceptor myInterceptor; @Autowired + protected IInterceptorRegistry myInterceptorRegistry; + @Autowired @Qualifier("myLocationDaoR4") protected IFhirResourceDao<Location> myLocationDao; @Autowired @@ -285,6 +288,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); myDaoConfig.setSuppressUpdatesWithNoChange(new DaoConfig().isSuppressUpdatesWithNoChange()); myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); + + myInterceptorRegistry.clearAnonymousHookForUnitTest(); } @After diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java deleted file mode 100644 index de384608297..00000000000 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/test/InterceptorRegistryTest.java +++ /dev/null @@ -1,159 +0,0 @@ -package ca.uhn.fhir.jpa.interceptor.test; - -import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; -import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; -import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; -import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.model.interceptor.api.Hook; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import java.util.ArrayList; -import java.util.List; - -import static org.hamcrest.Matchers.contains; -import static org.junit.Assert.*; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {InterceptorRegistryTest.InterceptorRegistryTestCtxConfig.class}) -public class InterceptorRegistryTest { - - private static boolean ourNext_beforeRestHookDelivery_Return1; - private static List<String> ourInvocations = new ArrayList<>(); - private static CanonicalSubscription ourLastCanonicalSubscription; - private static ResourceDeliveryMessage ourLastResourceDeliveryMessage0; - private static ResourceDeliveryMessage ourLastResourceDeliveryMessage1; - - @Autowired - private InterceptorRegistry myInterceptorRegistry; - - @Test - public void testGlobalInterceptorsAreFound() { - List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest(); - assertEquals(2, globalInterceptors.size()); - assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne); - assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo); - } - - @Test - public void testInvokeGlobalInterceptorMethods() { - ResourceDeliveryMessage msg = new ResourceDeliveryMessage(); - CanonicalSubscription subs = new CanonicalSubscription(); - HookParams params = new HookParams(msg, subs); - boolean outcome = myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params); - assertTrue(outcome); - - assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery", "MyTestInterceptorTwo.beforeRestHookDelivery")); - assertSame(msg, ourLastResourceDeliveryMessage0); - assertNull(ourLastResourceDeliveryMessage1); - assertSame(subs, ourLastCanonicalSubscription); - } - - @Test - public void testInvokeGlobalInterceptorMethods_MethodAbortsProcessing() { - ourNext_beforeRestHookDelivery_Return1 = false; - - ResourceDeliveryMessage msg = new ResourceDeliveryMessage(); - CanonicalSubscription subs = new CanonicalSubscription(); - HookParams params = new HookParams(msg, subs); - boolean outcome = myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params); - assertFalse(outcome); - - assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery")); - } - - @Test - public void testCallHooksInvokedWithWrongParameters() { - Integer msg = 123; - CanonicalSubscription subs = new CanonicalSubscription(); - HookParams params = new HookParams(msg, subs); - try { - myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params); - fail(); - } catch (AssertionError e) { - assertEquals("Wrong hook parameters, wanted [CanonicalSubscription, ResourceDeliveryMessage] and found [CanonicalSubscription, Integer]", e.getMessage()); - } - } - - - @Before - public void before() { - ourNext_beforeRestHookDelivery_Return1 = true; - ourLastCanonicalSubscription = null; - ourLastResourceDeliveryMessage0 = null; - ourLastResourceDeliveryMessage1 = null; - ourInvocations.clear(); - } - - @Configuration - @ComponentScan(basePackages = "ca.uhn.fhir.jpa.model") - static class InterceptorRegistryTestCtxConfig { - - /** - * Note: Orders are deliberately reversed to make sure we get the orders right - * using the @Order annotation - */ - @Bean - public MyTestInterceptorTwo interceptor1() { - return new MyTestInterceptorTwo(); - } - - /** - * Note: Orders are deliberately reversed to make sure we get the orders right - * using the @Order annotation - */ - @Bean - public MyTestInterceptorOne interceptor2() { - return new MyTestInterceptorOne(); - } - - } - - @Interceptor - @Order(100) - public static class MyTestInterceptorOne { - - public MyTestInterceptorOne() { - super(); - } - - @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) - public boolean beforeRestHookDelivery(CanonicalSubscription theCanonicalSubscription) { - ourLastCanonicalSubscription = theCanonicalSubscription; - ourInvocations.add("MyTestInterceptorOne.beforeRestHookDelivery"); - return ourNext_beforeRestHookDelivery_Return1; - } - - } - - @Interceptor - @Order(200) - public static class MyTestInterceptorTwo { - @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) - public void beforeRestHookDelivery(ResourceDeliveryMessage theResourceDeliveryMessage0, ResourceDeliveryMessage theResourceDeliveryMessage1) { - ourLastResourceDeliveryMessage0 = theResourceDeliveryMessage0; - ourLastResourceDeliveryMessage1 = theResourceDeliveryMessage1; - ourInvocations.add("MyTestInterceptorTwo.beforeRestHookDelivery"); - } - } - - /** - * Just a make-believe version of this class for the unit test - */ - private static class CanonicalSubscription { - } - - /** - * Just a make-believe version of this class for the unit test - */ - private static class ResourceDeliveryMessage { - } -} 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 new file mode 100644 index 00000000000..74c9f355e40 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/HookInterceptorR4Test.java @@ -0,0 +1,110 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; +import ca.uhn.fhir.rest.api.MethodOutcome; +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.Patient; +import org.junit.AfterClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class HookInterceptorR4Test extends BaseResourceProviderR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HookInterceptorR4Test.class); + + @Test + public void testOP_PRESTORAGE_RESOURCE_CREATED_ModifyResource() { + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED, t->{ + Patient contents = (Patient) t.get(IBaseResource.class, 0); + contents.getNameFirstRep().setFamily("NEWFAMILY"); + }); + + Patient p = new Patient(); + p.getNameFirstRep().setFamily("OLDFAMILY"); + MethodOutcome outcome = ourClient.create().resource(p).execute(); + + // Response reflects change, stored resource also does + Patient responsePatient = (Patient) outcome.getResource(); + assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily()); + responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute(); + assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily()); + + } + + @Test + public void testOP_PRECOMMIT_RESOURCE_CREATED_ModifyResource() { + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, t->{ + Patient contents = (Patient) t.get(IBaseResource.class, 0); + contents.getNameFirstRep().setFamily("NEWFAMILY"); + }); + + Patient p = new Patient(); + p.getNameFirstRep().setFamily("OLDFAMILY"); + MethodOutcome outcome = ourClient.create().resource(p).execute(); + + // Response reflects change, stored resource does not + Patient responsePatient = (Patient) outcome.getResource(); + assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily()); + responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute(); + assertEquals("OLDFAMILY", responsePatient.getNameFirstRep().getFamily()); + + } + + @Test + public void testOP_PRESTORAGE_RESOURCE_UPDATED_ModifyResource() { + Patient p = new Patient(); + p.setActive(true); + IIdType id = ourClient.create().resource(p).execute().getId(); + + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED, t->{ + Patient contents = (Patient) t.get(IBaseResource.class, 1); + contents.getNameFirstRep().setFamily("NEWFAMILY"); + }); + + p = new Patient(); + p.setId(id); + p.getNameFirstRep().setFamily("OLDFAMILY"); + MethodOutcome outcome = ourClient.update().resource(p).execute(); + + // Response reflects change, stored resource also does + Patient responsePatient = (Patient) outcome.getResource(); + assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily()); + responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute(); + assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily()); + + } + + @Test + public void testOP_PRECOMMIT_RESOURCE_UPDATED_ModifyResource() { + Patient p = new Patient(); + p.setActive(true); + IIdType id = ourClient.create().resource(p).execute().getId(); + + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, t->{ + Patient contents = (Patient) t.get(IBaseResource.class, 1); + contents.getNameFirstRep().setFamily("NEWFAMILY"); + }); + + p = new Patient(); + p.setId(id); + p.getNameFirstRep().setFamily("OLDFAMILY"); + MethodOutcome outcome = ourClient.update().resource(p).execute(); + + // Response reflects change, stored resource does not + Patient responsePatient = (Patient) outcome.getResource(); + assertEquals("NEWFAMILY", responsePatient.getNameFirstRep().getFamily()); + responsePatient = ourClient.read().resource(Patient.class).withId(outcome.getId()).execute(); + assertEquals("OLDFAMILY", responsePatient.getNameFirstRep().getFamily()); + + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java index 4b644ca3458..138a175e572 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderInterceptorR4Test.java @@ -366,6 +366,7 @@ public class ResourceProviderInterceptorR4Test extends BaseResourceProviderR4Tes } + private void transaction(Bundle theBundle) throws IOException { String resource = myFhirCtx.newXmlParser().encodeResourceToString(theBundle); HttpPost post = new HttpPost(ourServerBase + "/"); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java index 87d52768498..46f47e283a0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java @@ -153,9 +153,8 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test observation.setStatus(Observation.ObservationStatus.FINAL); - MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); - - observation.setId(methodOutcome.getId()); + IIdType id = myObservationDao.create(observation).getId(); + observation.setId(id); return observation; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java index 2a79125a84e..e03232a251b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu2Test.java @@ -113,10 +113,8 @@ public class RestHookTestDstu2Test extends BaseResourceProviderDstu2Test { observation.setStatus(ObservationStatusEnum.FINAL); - MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); - - String observationId = methodOutcome.getId().getIdPart(); - observation.setId(observationId); + IIdType id = myObservationDao.create(observation).getId(); + observation.setId(id); return observation; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java index 735b051a2ad..6299816a0df 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.matchesPattern; import static org.junit.Assert.*; /** @@ -112,6 +113,39 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { } + @Test + public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception { + + String payload = "application/fhir+json"; + String code = "1000000050"; + String criteria1 = "Observation?"; + createSubscription(criteria1, payload); + waitForActivatedSubscriptionCount(1); + + // Create a transaction that should match + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.getIdentifierFirstRep().setSystem("foo").setValue("AAA"); + bundle.addEntry().setResource(patient).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Patient"); + + Observation observation = new Observation(); + observation.getIdentifierFirstRep().setSystem("foo").setValue("1"); + observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT"); + observation.setStatus(Observation.ObservationStatus.FINAL); + observation.getSubject().setReference(patient.getId()); + bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation"); + + // Send the transaction + mySystemDao.transaction(null, bundle); + + waitForSize(1, ourUpdatedObservations); + + assertThat(ourUpdatedObservations.get(0).getSubject().getReference(), matchesPattern("Patient/[0-9]+")); + } + @Test public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception { String payload = "application/fhir+json"; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java index 4382cf5c80f..441802a9f4a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java @@ -136,9 +136,14 @@ public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { @Configuration static class MyTestCtxConfig { + @Autowired + private IInterceptorRegistry myInterceptorRegistry; + @Bean public MyTestInterceptor interceptor() { - return new MyTestInterceptor(); + MyTestInterceptor retVal = new MyTestInterceptor(); + myInterceptorRegistry.registerInterceptor(retVal); + return retVal; } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java index 5156e67c66a..11ec5e61beb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java @@ -22,11 +22,11 @@ package ca.uhn.fhir.jpa.model.interceptor.api; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; public class HookParams { @@ -69,10 +69,11 @@ public class HookParams { } /** - * Multivalued parameters will be returned twice in this list + * Returns an unmodifiable multimap of the params, where the + * key is the param type and the value is the actual instance */ - public List<String> getTypesAsSimpleName() { - return myParams.values().stream().map(t -> t.getClass().getSimpleName()).collect(Collectors.toList()); + public ListMultimap<Class<?>, Object> getParamsForType() { + return Multimaps.unmodifiableListMultimap(myParams); } public Collection<Object> values() { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorBroadcaster.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorBroadcaster.java new file mode 100644 index 00000000000..6204991ad93 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorBroadcaster.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.jpa.model.interceptor.api; + +/*- + * #%L + * HAPI FHIR Model + * %% + * Copyright (C) 2014 - 2019 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% + */ + +public interface IInterceptorBroadcaster { + + /** + * Invoke the interceptor methods + */ + boolean callHooks(Pointcut thePointcut, HookParams theParams); + + /** + * Invoke the interceptor methods + */ + boolean callHooks(Pointcut thePointcut, Object... theParams); + +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java index 6661bd90948..f302c316d39 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -26,6 +26,34 @@ public interface IInterceptorRegistry { int DEFAULT_ORDER = 0; + /** + * Register an interceptor. This method has no effect if the given interceptor is already registered. + * + * @param theInterceptor The interceptor to register + * @return Returns <code>true</code> if at least one valid hook method was found on this interceptor + */ + boolean registerInterceptor(Object theInterceptor); + + /** + * Unregister an interceptor. This method has no effect if the given interceptor is not already registered. + * + * @param theInterceptor The interceptor to unregister + */ + void unregisterInterceptor(Object theInterceptor); + + /** + * @deprecated to be removed + */ + @Deprecated + boolean registerGlobalInterceptor(Object theInterceptor); + + /** + * @deprecated to be removed + */ + @Deprecated + void unregisterGlobalInterceptor(Object theInterceptor); + + @VisibleForTesting void registerAnonymousHookForUnitTest(Pointcut thePointcut, IAnonymousLambdaHook theHook); @@ -34,23 +62,4 @@ public interface IInterceptorRegistry { @VisibleForTesting void clearAnonymousHookForUnitTest(); - - /** - * Register an interceptor - * - * @param theInterceptor The interceptor to register - * @return Returns <code>true</code> if at least one valid hook method was found on this interceptor - */ - boolean registerGlobalInterceptor(Object theInterceptor); - - /** - * Invoke the interceptor methods - */ - boolean callHooks(Pointcut thePointcut, HookParams theParams); - - /** - * Invoke the interceptor methods - */ - boolean callHooks(Pointcut thePointcut, Object... theParams); - } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java index 669e6833e82..27015c25eae 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java @@ -31,4 +31,11 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Interceptor { + + /** + * @return Declares that an interceptor should be manually registered with the registry, + * and should not auto-register using Spring autowiring. + */ + boolean manualRegistration() default false; + } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java index 979d4331c02..146adafb1e8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.api; * 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. @@ -41,7 +41,7 @@ public enum Pointcut { * <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li> * </ul> */ - SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY("CanonicalSubscription", "ResourceDeliveryMessage"), + SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription", "ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage"), /** * Invoked immediately before the delivery of a REST HOOK subscription. @@ -56,7 +56,7 @@ public enum Pointcut { * <li>ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage</li> * </ul> */ - SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY("CanonicalSubscription", "ResourceDeliveryMessage"), + SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription", "ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage"), /** * Invoked whenever a persisted resource (a resource that has just been stored in the @@ -67,7 +67,7 @@ public enum Pointcut { * <li>ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage</li> * </ul> */ - SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ResourceModifiedMessage"), + SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage"), /** @@ -83,7 +83,89 @@ public enum Pointcut { * <li>ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription</li> * </ul> */ - SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("CanonicalSubscription"); + SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"), + + /** + * Invoked before a resource will be updated, immediately before the resource + * is persisted to the database. + * <p> + * Hooks will have access to the contents of the resource being created + * and may choose to make modifications to it. These changes will be + * reflected in permanent storage. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>org.hl7.fhir.instance.model.api.IBaseResource</li> + * </ul> + */ + OP_PRESTORAGE_RESOURCE_CREATED("org.hl7.fhir.instance.model.api.IBaseResource"), + + /** + * Invoked before a resource will be created, immediately before the transaction + * is committed (after all validation and other business rules have successfully + * completed, and any other database activity is complete. + * <p> + * Hooks will have access to the contents of the resource being created + * but should generally not make any + * changes as storage has already occurred. Changes will not be reflected + * in storage, but may be reflected in the HTTP response. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>org.hl7.fhir.instance.model.api.IBaseResource</li> + * </ul> + */ + OP_PRECOMMIT_RESOURCE_CREATED("org.hl7.fhir.instance.model.api.IBaseResource"), + + /** + * Invoked before a resource will be created + * <p> + * Hooks will have access to the contents of the resource being deleted + * but should not make any changes as storage has already occurred + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>org.hl7.fhir.instance.model.api.IBaseResource</li> + * </ul> + */ + OP_PRECOMMIT_RESOURCE_DELETED("org.hl7.fhir.instance.model.api.IBaseResource"), + + /** + * Invoked before a resource will be updated, immediately before the transaction + * is committed (after all validation and other business rules have successfully + * completed, and any other database activity is complete. + * <p> + * Hooks will have access to the contents of the resource being updated + * (both the previous and new contents) but should generally not make any + * changes as storage has already occurred. Changes will not be reflected + * in storage, but may be reflected in the HTTP response. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>org.hl7.fhir.instance.model.api.IBaseResource (previous contents)</li> + * <li>org.hl7.fhir.instance.model.api.IBaseResource (new contents)</li> + * </ul> + */ + OP_PRECOMMIT_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"), + + /** + * Invoked before a resource will be updated, immediately before the resource + * is persisted to the database. + * <p> + * Hooks will have access to the contents of the resource being updated + * (both the previous and new contents) and may choose to make modifications + * to the new contents of the resource. These changes will be reflected in + * permanent storage. + * </p> + * Hooks may accept the following parameters: + * <ul> + * <li>org.hl7.fhir.instance.model.api.IBaseResource (previous contents)</li> + * <li>org.hl7.fhir.instance.model.api.IBaseResource (new contents)</li> + * </ul> + */ + OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"), + + ; private final List<String> myParameterTypes; @@ -94,4 +176,4 @@ public enum Pointcut { public List<String> getParameterTypes() { return myParameterTypes; } -} + } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java similarity index 58% rename from hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java rename to hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java index da86d6212a0..7fc8fd9c4fd 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorRegistry.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.executor; * 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. @@ -21,45 +21,43 @@ package ca.uhn.fhir.jpa.model.interceptor.executor; */ import ca.uhn.fhir.jpa.model.interceptor.api.*; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimaps; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; @Component -public class InterceptorRegistry implements IInterceptorRegistry, ApplicationContextAware { - private static final Logger ourLog = LoggerFactory.getLogger(InterceptorRegistry.class); - private ApplicationContext myAppCtx; - private final List<Object> myGlobalInterceptors = new ArrayList<>(); +public class InterceptorService implements IInterceptorRegistry, IInterceptorBroadcaster { + private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class); + private final List<Object> myInterceptors = new ArrayList<>(); private final ListMultimap<Pointcut, BaseInvoker> myInvokers = ArrayListMultimap.create(); - private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create(); + private final Object myRegistryMutex = new Object(); /** * Constructor */ - public InterceptorRegistry() { + public InterceptorService() { super(); } @VisibleForTesting - public List<Object> getGlobalInterceptorsForUnitTest() { - return myGlobalInterceptors; + List<Object> getGlobalInterceptorsForUnitTest() { + return myInterceptors; } @@ -83,34 +81,43 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon myAnonymousInvokers.clear(); } - @PostConstruct - public void start() { + @Override + public boolean registerInterceptor(Object theInterceptor) { + synchronized (myRegistryMutex) { - // Grab the global interceptors - String[] globalInterceptorNames = myAppCtx.getBeanNamesForAnnotation(Interceptor.class); - for (String nextName : globalInterceptorNames) { - Object nextInterceptor = myAppCtx.getBean(nextName); - registerGlobalInterceptor(nextInterceptor); + if (isInterceptorAlreadyRegistered(theInterceptor)) { + return false; + } + + Class<?> interceptorClass = theInterceptor.getClass(); + int typeOrder = determineOrder(interceptorClass); + + if (!scanInterceptorForHookMethodsAndAddThem(theInterceptor, typeOrder)) { + return false; + } + + myInterceptors.add(theInterceptor); + + // Make sure we're always sorted according to the order declared in + // @Order + sortByOrderAnnotation(myInterceptors); + for (Pointcut nextPointcut : myInvokers.keys()) { + List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut); + nextInvokerList.sort(Comparator.naturalOrder()); + } + + return true; } - } - @Override - public boolean registerGlobalInterceptor(Object theInterceptor) { + private boolean scanInterceptorForHookMethodsAndAddThem(Object theInterceptor, int theTypeOrder) { boolean retVal = false; - - int typeOrder = DEFAULT_ORDER; - Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theInterceptor.getClass(), Order.class); - if (typeOrderAnnotation != null) { - typeOrder = typeOrderAnnotation.value(); - } - for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) { Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); if (hook != null) { - int methodOrder = typeOrder; + int methodOrder = theTypeOrder; Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class); if (methodOrderAnnotation != null) { methodOrder = methodOrderAnnotation.value(); @@ -124,20 +131,45 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon retVal = true; } } - - myGlobalInterceptors.add(theInterceptor); - - // Make sure we're always sorted according to the order declared in - // @Order - sortByOrderAnnotation(myGlobalInterceptors); - for (Pointcut nextPointcut : myInvokers.keys()) { - List<BaseInvoker> nextInvokerList = myInvokers.get(nextPointcut); - nextInvokerList.sort(Comparator.naturalOrder()); - } - return retVal; } + private int determineOrder(Class<?> theInterceptorClass) { + int typeOrder = DEFAULT_ORDER; + Order typeOrderAnnotation = AnnotationUtils.findAnnotation(theInterceptorClass, Order.class); + if (typeOrderAnnotation != null) { + typeOrder = typeOrderAnnotation.value(); + } + return typeOrder; + } + + private boolean isInterceptorAlreadyRegistered(Object theInterceptor) { + for (Object next : myInterceptors) { + if (next == theInterceptor) { + return true; + } + } + return false; + } + + @Override + public void unregisterInterceptor(Object theInterceptor) { + synchronized (myRegistryMutex) { + myInterceptors.removeIf(t -> t == theInterceptor); + myInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); + } + } + + @Override + public boolean registerGlobalInterceptor(Object theInterceptor) { + return registerInterceptor(theInterceptor); + } + + @Override + public void unregisterGlobalInterceptor(Object theInterceptor) { + unregisterInterceptor(theInterceptor); + } + private void sortByOrderAnnotation(List<Object> theObjects) { IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>(); for (Object next : theObjects) { @@ -154,24 +186,15 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon } @Override - public void setApplicationContext(@Nonnull ApplicationContext theApplicationContext) throws BeansException { - myAppCtx = theApplicationContext; + public boolean callHooks(Pointcut thePointcut, Object... theParams) { + return callHooks(thePointcut, new HookParams(theParams)); } @Override public boolean callHooks(Pointcut thePointcut, HookParams theParams) { assert haveAppropriateParams(thePointcut, theParams); - List<BaseInvoker> globalInvokers = myInvokers.get(thePointcut); - List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut); - - List<BaseInvoker> invokers = globalInvokers; - if (anonymousInvokers.isEmpty() == false) { - invokers = ListUtils.union( - anonymousInvokers, - globalInvokers); - invokers.sort(Comparator.naturalOrder()); - } + List<BaseInvoker> invokers = getInvokersForPointcut(thePointcut); /* * Call each hook in order @@ -186,33 +209,68 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon return true; } + @VisibleForTesting + List<Object> getInterceptorsWithInvokersForPointcut(Pointcut thePointcut) { + return getInvokersForPointcut(thePointcut) + .stream() + .map(BaseInvoker::getInterceptor) + .collect(Collectors.toList()); + } + + /** + * Returns an ordered list of invokers for the given pointcut. Note that + * a new and stable list is returned to.. do whatever you want with it. + */ + private List<BaseInvoker> getInvokersForPointcut(Pointcut thePointcut) { + List<BaseInvoker> invokers; + boolean haveAnonymousInvokers; + synchronized (myRegistryMutex) { + List<BaseInvoker> globalInvokers = myInvokers.get(thePointcut); + List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut); + invokers = ListUtils.union(anonymousInvokers, globalInvokers); + haveAnonymousInvokers = anonymousInvokers.isEmpty() == false; + } + + if (haveAnonymousInvokers) { + invokers.sort(Comparator.naturalOrder()); + } + return invokers; + } + /** * Only call this when assertions are enabled, it's expensive */ - private boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) { - List<String> givenTypes = theParams.getTypesAsSimpleName(); - List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); - givenTypes.sort(Comparator.naturalOrder()); - wantedTypes.sort(Comparator.naturalOrder()); - if (!givenTypes.equals(wantedTypes)) { - throw new AssertionError("Wrong hook parameters, wanted " + wantedTypes + " and found " + givenTypes); - } - return true; - } + boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) { + Validate.isTrue(theParams.getParamsForType().values().size() == thePointcut.getParameterTypes().size(), "Wrong number of params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), theParams.getParamsForType().values().stream().map(t -> t.getClass().getSimpleName()).sorted().collect(Collectors.toList())); - @Override - public boolean callHooks(Pointcut thePointcut, Object... theParams) { - return callHooks(thePointcut, new HookParams(theParams)); + List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); + + ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType(); + for (Class<?> nextTypeClass : givenTypes.keySet()) { + String nextTypeName = nextTypeClass.getName(); + for (Object nextParamValue : givenTypes.get(nextTypeClass)) { + Validate.isTrue(nextTypeClass.isAssignableFrom(nextParamValue.getClass()), "Invalid params for pointcut %s - %s is not of type %s", thePointcut.name(), nextParamValue.getClass(), nextTypeClass); + Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but missing %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName); + } + } + + return true; } private abstract class BaseInvoker implements Comparable<BaseInvoker> { private final int myOrder; + private final Object myInterceptor; - protected BaseInvoker(int theOrder) { + BaseInvoker(Object theInterceptor, int theOrder) { + myInterceptor = theInterceptor; myOrder = theOrder; } + public Object getInterceptor() { + return myInterceptor; + } + abstract boolean invoke(HookParams theParams); @Override @@ -225,7 +283,7 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon private final IAnonymousLambdaHook myHook; public AnonymousLambdaInvoker(IAnonymousLambdaHook theHook, int theOrder) { - super(theOrder); + super(theHook, theOrder); myHook = theHook; } @@ -238,7 +296,6 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon private class HookInvoker extends BaseInvoker { - private final Object myInterceptor; private final boolean myReturnsBoolean; private final Method myMethod; private final Class<?>[] myParameterTypes; @@ -248,8 +305,7 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon * Constructor */ private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { - super(theOrder); - myInterceptor = theInterceptor; + super(theInterceptor, theOrder); myParameterTypes = theHookMethod.getParameterTypes(); myMethod = theHookMethod; @@ -285,19 +341,32 @@ public class InterceptorRegistry implements IInterceptorRegistry, ApplicationCon // Invoke the method try { - Object returnValue = myMethod.invoke(myInterceptor, args); + Object returnValue = myMethod.invoke(getInterceptor(), args); if (myReturnsBoolean) { return (boolean) returnValue; } else { return true; } + } catch (InvocationTargetException e) { + Throwable targetException = e.getTargetException(); + if (targetException instanceof RuntimeException) { + throw ((RuntimeException) targetException); + } else { + throw new InternalErrorException(targetException); + } } catch (Exception e) { - ourLog.error("Failure executing interceptor method[{}]: {}", myMethod, e.toString(), e); - return true; + throw new InternalErrorException(e); } } } + private static String toErrorString(List<String> theParameterTypes) { + return theParameterTypes + .stream() + .sorted() + .collect(Collectors.joining(",")); + } + } diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java new file mode 100644 index 00000000000..eede79c7d30 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java @@ -0,0 +1,268 @@ +package ca.uhn.fhir.jpa.model.interceptor.executor; + +import ca.uhn.fhir.jpa.model.interceptor.api.*; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Patient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {InterceptorServiceTest.InterceptorRegistryTestCtxConfig.class}) +public class InterceptorServiceTest { + + private static boolean ourNext_beforeRestHookDelivery_Return1; + private static List<String> ourInvocations = new ArrayList<>(); + private static IBaseResource ourLastResourceOne; + private static IBaseResource ourLastResourceTwoA; + private static IBaseResource ourLastResourceTwoB; + + @Autowired + private InterceptorService myInterceptorRegistry; + + @Autowired + private MyTestInterceptorOne myInterceptorOne; + @Autowired + private MyTestInterceptorTwo myInterceptorTwo; + @Autowired + private MyTestInterceptorManual myInterceptorManual; + + @Test + public void testGlobalInterceptorsAreFound() { + List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest(); + assertEquals(2, globalInterceptors.size()); + assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne); + assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo); + } + + @Test + public void testManuallyRegisterGlobalInterceptor() { + + // Register the manual interceptor (has @Order right in the middle) + myInterceptorRegistry.registerInterceptor(myInterceptorManual); + List<Object> globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest(); + assertEquals(3, globalInterceptors.size()); + assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne); + assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual); + assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo); + + // Try to register again (should have no effect + myInterceptorRegistry.registerInterceptor(myInterceptorManual); + globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest(); + assertEquals(3, globalInterceptors.size()); + assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne); + assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorManual); + assertTrue(globalInterceptors.get(2).getClass().toString(), globalInterceptors.get(2) instanceof MyTestInterceptorTwo); + + // Make sure we have the right invokers in the right order + List<Object> invokers = myInterceptorRegistry.getInterceptorsWithInvokersForPointcut(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED); + assertSame(myInterceptorOne, invokers.get(0)); + assertSame(myInterceptorManual, invokers.get(1)); + assertSame(myInterceptorTwo, invokers.get(2)); + + // Finally, unregister it + myInterceptorRegistry.unregisterInterceptor(myInterceptorManual); + globalInterceptors = myInterceptorRegistry.getGlobalInterceptorsForUnitTest(); + assertEquals(2, globalInterceptors.size()); + assertTrue(globalInterceptors.get(0).getClass().toString(), globalInterceptors.get(0) instanceof MyTestInterceptorOne); + assertTrue(globalInterceptors.get(1).getClass().toString(), globalInterceptors.get(1) instanceof MyTestInterceptorTwo); + + } + + @Test + public void testInvokeGlobalInterceptorMethods() { + Patient patient = new Patient(); + HookParams params = new HookParams() + .add(IBaseResource.class, patient); + boolean outcome = myInterceptorRegistry.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, params); + assertTrue(outcome); + + assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery", "MyTestInterceptorTwo.beforeRestHookDelivery")); + assertSame(patient, ourLastResourceTwoA); + assertNull(ourLastResourceTwoB); + assertSame(patient, ourLastResourceOne); + } + + @Test + public void testInvokeGlobalInterceptorMethods_MethodAbortsProcessing() { + ourNext_beforeRestHookDelivery_Return1 = false; + + Patient patient = new Patient(); + HookParams params = new HookParams() + .add(IBaseResource.class, patient); + boolean outcome = myInterceptorRegistry.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, params); + assertFalse(outcome); + + assertThat(ourInvocations, contains("MyTestInterceptorOne.beforeRestHookDelivery")); + } + + @Test + public void testCallHooksInvokedWithWrongParameters() { + Integer msg = 123; + CanonicalSubscription subs = new CanonicalSubscription(); + HookParams params = new HookParams(msg, subs); + try { + myInterceptorRegistry.callHooks(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED, params); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Wrong number of params for pointcut OP_PRECOMMIT_RESOURCE_CREATED - Wanted org.hl7.fhir.instance.model.api.IBaseResource but found [CanonicalSubscription, Integer]", e.getMessage()); + } + } + + @Test + public void testValidateParamTypes() { + HookParams params = new HookParams(); + params.add(IBaseResource.class, new Patient()); + params.add(IBaseResource.class, new Patient()); + boolean validated = myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params); + assertTrue(validated); + } + + @Test + public void testValidateParamTypesMissingParam() { + HookParams params = new HookParams(); + params.add(IBaseResource.class, new Patient()); + try { + myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Wrong number of params for pointcut OP_PRECOMMIT_RESOURCE_UPDATED - Wanted org.hl7.fhir.instance.model.api.IBaseResource,org.hl7.fhir.instance.model.api.IBaseResource but found [Patient]", e.getMessage()); + } + } + + @Test + public void testValidateParamTypesExtraParam() { + HookParams params = new HookParams(); + params.add(IBaseResource.class, new Patient()); + params.add(IBaseResource.class, new Patient()); + params.add(IBaseResource.class, new Patient()); + try { + myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Wrong number of params for pointcut OP_PRECOMMIT_RESOURCE_UPDATED - Wanted org.hl7.fhir.instance.model.api.IBaseResource,org.hl7.fhir.instance.model.api.IBaseResource but found [Patient, Patient, Patient]", e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testValidateParamTypesWrongParam() { + HookParams params = new HookParams(); + Class clazz = IBaseResource.class; + params.add(clazz, "AAA"); + params.add(clazz, "BBB"); + try { + myInterceptorRegistry.haveAppropriateParams(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED, params); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Invalid params for pointcut OP_PRECOMMIT_RESOURCE_UPDATED - class java.lang.String is not of type interface org.hl7.fhir.instance.model.api.IBaseResource", e.getMessage()); + } + } + + @Before + public void before() { + ourNext_beforeRestHookDelivery_Return1 = true; + ourLastResourceOne = null; + ourLastResourceTwoA = null; + ourLastResourceTwoB = null; + ourInvocations.clear(); + } + + @Configuration + @ComponentScan(basePackages = "ca.uhn.fhir.jpa.model") + static class InterceptorRegistryTestCtxConfig { + + @Autowired + private IInterceptorRegistry myInterceptorRegistry; + + /** + * Note: Orders are deliberately reversed to make sure we get the orders right + * using the @Order annotation + */ + @Bean + public MyTestInterceptorTwo interceptor1() { + MyTestInterceptorTwo retVal = new MyTestInterceptorTwo(); + myInterceptorRegistry.registerInterceptor(retVal); + return retVal; + } + + /** + * Note: Orders are deliberately reversed to make sure we get the orders right + * using the @Order annotation + */ + @Bean + public MyTestInterceptorOne interceptor2() { + MyTestInterceptorOne retVal = new MyTestInterceptorOne(); + myInterceptorRegistry.registerInterceptor(retVal); + return retVal; + } + + @Bean + public MyTestInterceptorManual interceptorManual() { + return new MyTestInterceptorManual(); + } + + } + + @Interceptor + @Order(100) + public static class MyTestInterceptorOne { + + public MyTestInterceptorOne() { + super(); + } + + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED) + public boolean beforeRestHookDelivery(IBaseResource theResource) { + ourLastResourceOne = theResource; + ourInvocations.add("MyTestInterceptorOne.beforeRestHookDelivery"); + return ourNext_beforeRestHookDelivery_Return1; + } + + } + + @Interceptor + @Order(300) + public static class MyTestInterceptorTwo { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED) + public void beforeRestHookDelivery(IBaseResource theResource0, IBaseResource theResource1) { + ourLastResourceTwoA = theResource0; + ourLastResourceTwoB = theResource1; + ourInvocations.add("MyTestInterceptorTwo.beforeRestHookDelivery"); + } + } + + @Interceptor(manualRegistration = true) + @Order(200) + public static class MyTestInterceptorManual { + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED) + public void beforeRestHookDelivery() { + ourInvocations.add("MyTestInterceptorManual.beforeRestHookDelivery"); + } + } + + /** + * Just a make-believe version of this class for the unit test + */ + private static class CanonicalSubscription { + } + + /** + * Just a make-believe version of this class for the unit test + */ + private static class ResourceDeliveryMessage { + } +} diff --git a/hapi-fhir-jpaserver-model/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-model/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e40a4e73953 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ +<configuration> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <level>INFO</level> + </filter> + <encoder> + <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="STDOUT" /> + </root> + +</configuration> diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java index bb131d7006c..afdc1b7f408 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.searchparam.extractor; * 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. @@ -58,7 +58,7 @@ public class ResourceLinkExtractor { @Autowired private ISearchParamExtractor mySearchParamExtractor; - public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver) { + public void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, boolean theFailOnInvalidReference) { String resourceType = theEntity.getResourceType(); /* @@ -71,13 +71,13 @@ public class ResourceLinkExtractor { Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(toResourceName(theResource.getClass())); for (RuntimeSearchParam nextSpDef : searchParams.values()) { - extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, theResourceLinkResolver, resourceType, nextSpDef); + extractResourceLinks(theParams, theEntity, theResource, theUpdateTime, theResourceLinkResolver, resourceType, nextSpDef, theFailOnInvalidReference); } theEntity.setHasLinks(theParams.links.size() > 0); } - private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef) { + private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, IBaseResource theResource, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, boolean theFailOnInvalidReference) { if (nextSpDef.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { return; } @@ -94,11 +94,11 @@ public class ResourceLinkExtractor { List<PathAndRef> refs = mySearchParamExtractor.extractResourceLinks(theResource, nextSpDef); for (PathAndRef nextPathAndRef : refs) { - extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, theResourceType, nextSpDef, nextPathsUnsplit, multiType, nextPathAndRef); + extractResourceLinks(theParams, theEntity, theUpdateTime, theResourceLinkResolver, theResourceType, nextSpDef, nextPathsUnsplit, multiType, nextPathAndRef, theFailOnInvalidReference); } } - private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, boolean theMultiType, PathAndRef nextPathAndRef) { + private void extractResourceLinks(ResourceIndexedSearchParams theParams, ResourceTable theEntity, Date theUpdateTime, IResourceLinkResolver theResourceLinkResolver, String theResourceType, RuntimeSearchParam nextSpDef, String theNextPathsUnsplit, boolean theMultiType, PathAndRef nextPathAndRef, boolean theFailOnInvalidReference) { Object nextObject = nextPathAndRef.getRef(); /* @@ -168,14 +168,25 @@ public class ResourceLinkExtractor { String baseUrl = nextId.getBaseUrl(); String typeString = nextId.getResourceType(); if (isBlank(typeString)) { - throw new InvalidRequestException("Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue()); + String msg = "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource type - " + nextId.getValue(); + if (theFailOnInvalidReference) { + throw new InvalidRequestException(msg); + } else { + ourLog.debug(msg); + return; + } } RuntimeResourceDefinition resourceDefinition; try { resourceDefinition = myContext.getResourceDefinition(typeString); } catch (DataFormatException e) { - throw new InvalidRequestException( - "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue()); + String msg = "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Resource type is unknown or not supported on this server - " + nextId.getValue(); + if (theFailOnInvalidReference) { + throw new InvalidRequestException(msg); + } else { + ourLog.debug(msg); + return; + } } if (isNotBlank(baseUrl)) { @@ -194,7 +205,13 @@ public class ResourceLinkExtractor { Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass(); String id = nextId.getIdPart(); if (StringUtils.isBlank(id)) { - throw new InvalidRequestException("Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue()); + String msg = "Invalid resource reference found at path[" + theNextPathsUnsplit + "] - Does not contain resource ID - " + nextId.getValue(); + if (theFailOnInvalidReference) { + throw new InvalidRequestException(msg); + } else { + ourLog.debug(msg); + return; + } } theResourceLinkResolver.validateTypeOrThrowException(type); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java index cc11e2c8a43..4cb987d3127 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java @@ -47,6 +47,7 @@ public class Retrier<T> { try { return mySupplier.get(); } catch(RuntimeException e) { + ourLog.trace("Failure during retry: {}", e.getMessage(), e); // with stacktrace if it's ever needed ourLog.info("Failed to {}. Attempt {} / {}: {}", myDescription, retryCount, myMaxRetries, e.getMessage()); lastException = e; try { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java index 42a2122b403..4ad69d5647d 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module; * 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. @@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.subscription.module; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.subscription.module.subscriber.IResourceMessage; +import ca.uhn.fhir.util.ResourceReferenceInfo; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; @@ -29,6 +30,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -112,6 +116,15 @@ public class ResourceModifiedMessage implements IResourceMessage { } private void setNewPayload(FhirContext theCtx, IBaseResource theNewPayload) { + /* + * References with placeholders would be invalid by the time we get here, and + * would be caught before we even get here. This check is basically a last-ditch + * effort to make sure nothing has broken in the various safeguards that + * should prevent this from happening (hence it only being an assert as + * opposed to something executed all the time). + */ + assert payloadContainsNoPlaceholderReferences(theCtx, theNewPayload); + /* * Note: Don't set myPayloadDecoded in here- This is a false optimization since * it doesn't actually get used if anyone is doing subscriptions at any @@ -123,7 +136,6 @@ public class ResourceModifiedMessage implements IResourceMessage { myPayloadId = theNewPayload.getIdElement().toUnqualified().getValue(); } - public enum OperationTypeEnum { CREATE, UPDATE, @@ -132,4 +144,23 @@ public class ResourceModifiedMessage implements IResourceMessage { } + private static boolean payloadContainsNoPlaceholderReferences(FhirContext theCtx, IBaseResource theNewPayload) { + List<ResourceReferenceInfo> refs = theCtx.newTerser().getAllResourceReferences(theNewPayload); + for (ResourceReferenceInfo next : refs) { + String ref = next.getResourceReference().getReferenceElement().getValue(); + if (isBlank(ref)) { + ref = next.getResourceReference().getResource().getIdElement().getValue(); + } + if (isNotBlank(ref)) { + if (ref.startsWith("#")) { + continue; + } + if (ref.startsWith("urn:uuid:")) { + throw new AssertionError("Reference at " + next.getName() + " is invalid: " + next.getResourceReference()); + } + } + } + return true; + } + } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java index 7c9b9b5da50..65eda9b430a 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionRegistry.java @@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.subscription.module.cache; */ import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import com.google.common.annotations.VisibleForTesting; @@ -59,7 +59,7 @@ public class SubscriptionRegistry { @Autowired ModelConfig myModelConfig; @Autowired - private IInterceptorRegistry myInterceptorRegistry; + private IInterceptorBroadcaster myInterceptorBroadcaster; public ActiveSubscription get(String theIdPart) { return myActiveSubscriptionCache.get(theIdPart); @@ -101,7 +101,7 @@ public class SubscriptionRegistry { myActiveSubscriptionCache.put(subscriptionId, activeSubscription); // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED - myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, canonicalized); + myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, canonicalized); return canonicalized; } @@ -117,7 +117,7 @@ public class SubscriptionRegistry { unregisterAllSubscriptionsNotInCollection(Collections.emptyList()); } - public void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) { + void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) { myActiveSubscriptionCache.unregisterAllSubscriptionsNotInCollection(theAllIds); } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java index c7e789b0c37..6418495dcf0 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcher.java @@ -59,7 +59,7 @@ public class InMemorySubscriptionMatcher implements ISubscriptionMatcher { entity.setResourceType(resourceType); ResourceIndexedSearchParams searchParams = new ResourceIndexedSearchParams(); mySearchParamExtractorService.extractFromResource(searchParams, entity, resource); - myResourceLinkExtractor.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), myInlineResourceLinkResolver); + myResourceLinkExtractor.extractResourceLinks(searchParams, entity, resource, resource.getMeta().getLastUpdated(), myInlineResourceLinkResolver, false); return myCriteriaResourceMatcher.match(criteria, resource, searchParams); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java index 0667f7be9f2..a481e678ae1 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionDeliveringRestHookSubscriber.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; * #L% */ +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; @@ -52,7 +53,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe IResourceRetriever myResourceRetriever; private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); @Autowired - private IInterceptorRegistry myInterceptorRegistry; + private IInterceptorBroadcaster myInterceptorBroadcaster; protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) { IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription); @@ -133,7 +134,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe CanonicalSubscription subscription = theMessage.getSubscription(); // Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY - if (!myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) { + if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) { return; } @@ -169,7 +170,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe deliverPayload(theMessage, subscription, payloadType, client); // Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY - if (!myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) { + if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) { //noinspection UnnecessaryReturnStatement return; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index 20a64112d0a..d2aab38685a 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription; @@ -33,9 +33,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. @@ -55,7 +55,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { @Autowired private SubscriptionRegistry mySubscriptionRegistry; @Autowired - private IInterceptorRegistry myInterceptorRegistry; + private IInterceptorBroadcaster myInterceptorBroadcaster; @Override public void handleMessage(Message<?> theMessage) throws MessagingException { @@ -75,7 +75,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { try { doMatchActiveSubscriptionsAndDeliver(theMsg); } finally { - myInterceptorRegistry.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, theMsg); + myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, theMsg); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java index 3605ae2c30f..bc118b2eaff 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java @@ -1,16 +1,12 @@ package ca.uhn.fhir.jpa.subscription.module.config; -import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; -import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.subscription.module.cache.ISubscriptionProvider; -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.context.annotation.*; @Configuration @Import(TestSubscriptionConfig.class) +@ComponentScan(basePackages = {"ca.uhn.fhir.jpa.model.interceptor.executor"}) public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config { @Bean @Primary @@ -24,9 +20,4 @@ public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config { return new MockFhirClientSubscriptionProvider(); } - @Bean - public IInterceptorRegistry interceptorRegistry() { - return new InterceptorRegistry(); - } - } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java index ff6b32d436c..c18641d72f9 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java @@ -39,6 +39,33 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test assertFalse(result.matched()); } + + /** + * Technically this is an invalid reference in most cases, but this shouldn't choke + * the matcher in the case that it gets used. + */ + @Test + public void testPlaceholderIdInReference() { + + ProcedureRequest pr = new ProcedureRequest(); + pr.setId("ProcedureRequest/123"); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER); + + pr.setSubject(new Reference("urn:uuid:aaaaaaaaaa")); + assertMatched(pr, "ProcedureRequest?intent=original-order"); + assertNotMatched(pr, "ProcedureRequest?subject=Patient/123"); + + pr.setSubject(new Reference("Foo/123")); + assertMatched(pr, "ProcedureRequest?intent=original-order"); + assertNotMatched(pr, "ProcedureRequest?subject=Patient/123"); + + pr.setSubject(new Reference("Patient/")); + assertMatched(pr, "ProcedureRequest?intent=original-order"); + assertNotMatched(pr, "ProcedureRequest?subject=Patient/123"); + + } + + @Test public void testResourceById() { diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index f2608b12f95..e44c8b2848d 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -2,8 +2,8 @@ package ca.uhn.fhir.jpa.subscription.module.standalone; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.model.interceptor.api.HookParams; +import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorRegistry; import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; import ca.uhn.fhir.jpa.subscription.module.PointcutLatch; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; @@ -52,7 +52,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base @Autowired SubscriptionChannelFactory mySubscriptionChannelFactory; @Autowired - InterceptorRegistry myInterceptorRegistry; + IInterceptorRegistry myInterceptorRegistry; @Autowired protected SubscriptionRegistry mySubscriptionRegistry; From f55be0b6d01ac25cc3c8e0627e87a59db75e5455 Mon Sep 17 00:00:00 2001 From: James Agnew <jamesagnew@gmail.com> Date: Fri, 25 Jan 2019 10:56:17 -0500 Subject: [PATCH 52/56] Test fix --- .../module/matcher/InMemorySubscriptionMatcherTestR4.java | 4 ++-- .../fhir/jpa/subscription/module/ResourceModifiedMessage.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java index 054670e9607..d98d10a153e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java @@ -389,8 +389,8 @@ public class InMemorySubscriptionMatcherTestR4 { msg.setId(new IdType("Patient/ABC")); SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(subscription, msg); fail(); - } catch (InternalErrorException e){ - assertEquals("Failure processing resource ID[Patient/ABC] for subscription ID[Subscription/123]: Invalid resource reference found at path[Patient.managingOrganization] - Does not contain resource type - urn:uuid:13720262-b392-465f-913e-54fb198ff954", e.getMessage()); + } catch (AssertionError e){ + assertEquals("Reference at managingOrganization is invalid: urn:uuid:13720262-b392-465f-913e-54fb198ff954", e.getMessage()); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java index 4ad69d5647d..d2bcffddc57 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java @@ -156,7 +156,7 @@ public class ResourceModifiedMessage implements IResourceMessage { continue; } if (ref.startsWith("urn:uuid:")) { - throw new AssertionError("Reference at " + next.getName() + " is invalid: " + next.getResourceReference()); + throw new AssertionError("Reference at " + next.getName() + " is invalid: " + ref); } } } From 10c59fceebbfc77d76d8ff3be8f6b3912611c41d Mon Sep 17 00:00:00 2001 From: Ken Stevens <ken.stevens@sympatico.ca> Date: Fri, 25 Jan 2019 13:01:04 -0500 Subject: [PATCH 53/56] Subscription strategy tag (#1178) tests pass --- .../main/java/ca/uhn/fhir/util/UrlUtil.java | 12 ++- .../jpa/search/warm/CacheWarmingSvcImpl.java | 22 +----- .../SubscriptionActivatingInterceptor.java | 74 +++++++++++-------- .../SubscriptionTriggeringSvcImpl.java | 4 +- ...mpositeInMemoryDaoSubscriptionMatcher.java | 7 +- .../dbmatcher/DaoSubscriptionMatcher.java | 6 +- .../InMemorySubscriptionMatcherTestR4.java | 60 ++++++++------- .../resthook/RestHookTestDstu3Test.java | 59 ++++++++++++++- .../jpa/model/interceptor/api/Pointcut.java | 2 +- .../cache/SubscriptionCanonicalizer.java | 31 ++++++++ .../module/cache/SubscriptionConstants.java | 13 ++-- .../matcher/CriteriaResourceMatcher.java | 67 +++++++++++------ .../matcher/SubscriptionMatchResult.java | 54 ++++++++++---- .../matcher/SubscriptionMatchingStrategy.java | 14 ++++ .../SubscriptionStrategyEvaluator.java | 19 +++++ .../SubscriptionMatchingSubscriber.java | 8 +- .../InMemorySubscriptionMatcherTestR3.java | 21 ++++-- .../SubscriptionStrategyEvaluatorTest.java | 53 +++++++++++++ src/changes/changes.xml | 6 ++ 19 files changed, 390 insertions(+), 142 deletions(-) create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java create mode 100644 hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluatorTest.java 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 10be04f97b3..d9bd5cec28c 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 @@ -1,6 +1,9 @@ package ca.uhn.fhir.util; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.escape.Escaper; @@ -172,8 +175,13 @@ public class UrlUtil { return true; } - public static void main(String[] args) { - System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005")); + public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException { + int paramIndex = theUrl.indexOf('?'); + String resourceName = theUrl.substring(0, paramIndex); + if (resourceName.contains("/")) { + resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1); + } + return theCtx.getResourceDefinition(resourceName); } public static Map<String, String[]> parseQueryString(String theQueryString) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java index ef8a738891e..d2970ec6af3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/warm/CacheWarmingSvcImpl.java @@ -26,9 +26,9 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; -import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.util.UrlUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -77,7 +77,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc { private void refreshNow(WarmCacheEntry theCacheEntry) { String nextUrl = theCacheEntry.getUrl(); - RuntimeResourceDefinition resourceDef = parseUrlResourceType(myCtx, nextUrl); + RuntimeResourceDefinition resourceDef = UrlUtil.parseUrlResourceType(myCtx, nextUrl); IFhirResourceDao<?> callingDao = myDaoRegistry.getResourceDao(resourceDef.getName()); String queryPart = parseWarmUrlParamPart(nextUrl); SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(queryPart, resourceDef); @@ -93,20 +93,6 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc { return theNextUrl.substring(paramIndex); } - /** - * TODO: this method probably belongs in a utility class, not here - * - * @throws DataFormatException If the resource type is not known - */ - public static RuntimeResourceDefinition parseUrlResourceType(FhirContext theCtx, String theUrl) throws DataFormatException { - int paramIndex = theUrl.indexOf('?'); - String resourceName = theUrl.substring(0, paramIndex); - if (resourceName.contains("/")) { - resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1); - } - return theCtx.getResourceDefinition(resourceName); - } - @PostConstruct public void start() { initCacheMap(); @@ -120,7 +106,7 @@ public class CacheWarmingSvcImpl implements ICacheWarmingSvc { // Validate parseWarmUrlParamPart(next.getUrl()); - parseUrlResourceType(myCtx, next.getUrl()); + UrlUtil.parseUrlResourceType(myCtx, next.getUrl()); myCacheEntryToNextRefresh.put(next, 0L); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java index 49fb15d6895..a86607801da 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingInterceptor.java @@ -29,17 +29,17 @@ import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.model.interceptor.api.Hook; import ca.uhn.fhir.jpa.model.interceptor.api.Interceptor; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; -import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; +import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy; +import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionStrategyEvaluator; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; -import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; import ca.uhn.fhir.util.SubscriptionUtil; import com.google.common.annotations.VisibleForTesting; import org.hl7.fhir.instance.model.Subscription; @@ -96,6 +96,8 @@ public class SubscriptionActivatingInterceptor { private MatchUrlService myMatchUrlService; @Autowired private DaoConfig myDaoConfig; + @Autowired + private SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator; public boolean activateOrRegisterSubscriptionIfRequired(final IBaseResource theSubscription) { // Grab the value for "Subscription.channel.type" so we can see if this @@ -160,7 +162,6 @@ public class SubscriptionActivatingInterceptor { } } - private boolean activateSubscription(String theActiveStatus, final IBaseResource theSubscription, String theRequestedStatus) { IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao(); IBaseResource subscription = subscriptionDao.read(theSubscription.getIdElement()); @@ -185,6 +186,36 @@ public class SubscriptionActivatingInterceptor { submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); } + @Hook(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED) + public void addStrategyTagCreated(IBaseResource theResource) { + if (isSubscription(theResource)) { + validateCriteriaAndAddStrategy(theResource); + } + } + + @Hook(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED) + public void addStrategyTagUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { + if (isSubscription(theNewResource)) { + validateCriteriaAndAddStrategy(theNewResource); + } + } + + // TODO KHS add third type of strategy DISABLED if that subscription type is disabled on this server + public void validateCriteriaAndAddStrategy(final IBaseResource theResource) { + String criteria = mySubscriptionCanonicalizer.getCriteria(theResource); + try { + SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(criteria); + mySubscriptionCanonicalizer.setMatchingStrategyTag(myFhirContext, theResource, strategy); + } catch (InvalidRequestException | DataFormatException e) { + throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage()); + } + } + + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED) + public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { + submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); + } + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_CREATED) public void resourceCreated(IBaseResource theResource) { submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); @@ -195,46 +226,31 @@ public class SubscriptionActivatingInterceptor { submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); } - @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_UPDATED) - public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { - submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); + private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) { + if (isSubscription(theNewResource)) { + submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType)); + } } - private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) { - submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType)); + private boolean isSubscription(IBaseResource theNewResource) { + RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theNewResource); + return ResourceTypeEnum.SUBSCRIPTION.getCode().equals(resourceDefinition.getName()); } private void submitResourceModified(final ResourceModifiedMessage theMsg) { - IIdType id = theMsg.getId(myFhirContext); - if (!id.getResourceType().equals(ResourceTypeEnum.SUBSCRIPTION.getCode())) { - return; - } switch (theMsg.getOperationType()) { case DELETE: - mySubscriptionRegistry.unregisterSubscription(id); + mySubscriptionRegistry.unregisterSubscription(theMsg.getId(myFhirContext)); break; case CREATE: case UPDATE: - final IBaseResource subscription = theMsg.getNewPayload(myFhirContext); - validateCriteria(subscription); - activateAndRegisterSubscriptionIfRequiredInTransaction(subscription); + activateAndRegisterSubscriptionIfRequiredInTransaction(theMsg.getNewPayload(myFhirContext)); break; default: break; } } - public void validateCriteria(final IBaseResource theResource) { - CanonicalSubscription subscription = mySubscriptionCanonicalizer.canonicalize(theResource); - String criteria = subscription.getCriteriaString(); - try { - RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, criteria); - myMatchUrlService.translateMatchUrl(criteria, resourceDef); - } catch (InvalidRequestException e) { - throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + criteria + " " + e.getMessage()); - } - } - private void activateAndRegisterSubscriptionIfRequiredInTransaction(IBaseResource theSubscription) { TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); txTemplate.execute(new TransactionCallbackWithoutResult() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java index a3ee7aa5358..4eed304ff7c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionTriggeringSvcImpl.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.search.warm.CacheWarmingSvcImpl; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; @@ -42,6 +41,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.StopWatch; +import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.ValidateUtil; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; @@ -209,7 +209,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc // If we don't have an active search started, and one needs to be.. start it if (isBlank(theJobDetails.getCurrentSearchUuid()) && theJobDetails.getRemainingSearchUrls().size() > 0 && totalSubmitted < myMaxSubmitPerPass) { String nextSearchUrl = theJobDetails.getRemainingSearchUrls().remove(0); - RuntimeResourceDefinition resourceDef = CacheWarmingSvcImpl.parseUrlResourceType(myFhirContext, nextSearchUrl); + RuntimeResourceDefinition resourceDef = UrlUtil.parseUrlResourceType(myFhirContext, nextSearchUrl); String queryPart = nextSearchUrl.substring(nextSearchUrl.indexOf('?')); String resourceType = resourceDef.getName(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java index b5cf2c969b8..4e084ba642b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java @@ -24,8 +24,8 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; -import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; import ca.uhn.fhir.jpa.subscription.module.matcher.InMemorySubscriptionMatcher; +import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -48,7 +48,10 @@ public class CompositeInMemoryDaoSubscriptionMatcher implements ISubscriptionMat SubscriptionMatchResult result; if (myDaoConfig.isEnableInMemorySubscriptionMatching()) { result = myInMemorySubscriptionMatcher.match(theSubscription, theMsg); - if (!result.supported()) { + if (result.supported()) { + // TODO KHS test + result.setInMemory(true); + } else { ourLog.info("Criteria {} for Subscription {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", theSubscription.getCriteriaString(), theSubscription.getIdElementString(), result.getUnsupportedReason()); result = myDaoSubscriptionMatcher.match(theSubscription, theMsg); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java index ad8f7acfaac..8b5281bffb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/DaoSubscriptionMatcher.java @@ -24,7 +24,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; @@ -32,7 +31,6 @@ import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; @@ -63,13 +61,13 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher { ourLog.debug("Subscription check found {} results for query: {}", results.size(), criteria); - return new SubscriptionMatchResult(results.size() > 0, "DATABASE"); + return SubscriptionMatchResult.fromBoolean(results.size() > 0); } /** * Search based on a query criteria */ - protected IBundleProvider performSearch(String theCriteria) { + private IBundleProvider performSearch(String theCriteria) { IFhirResourceDao<?> subscriptionDao = myDaoRegistry.getSubscriptionDao(); RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria); SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java index d98d10a153e..290f2300a9b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java @@ -7,7 +7,6 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; -import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.apache.commons.lang3.StringUtils; @@ -36,28 +35,41 @@ public class InMemorySubscriptionMatcherTestR4 { @Autowired InMemorySubscriptionMatcher myInMemorySubscriptionMatcher; @Autowired + SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator; + @Autowired FhirContext myContext; - private SubscriptionMatchResult match(IBaseResource resource, SearchParameterMap params) { - String criteria = params.toNormalizedQueryString(myContext); - ourLog.info("Criteria: <{}>", criteria); - return myInMemorySubscriptionMatcher.match(criteria, resource); - } - - private void assertUnsupported(IBaseResource resource, SearchParameterMap params) { - assertFalse(match(resource, params).supported()); - } - - private void assertMatched(IBaseResource resource, SearchParameterMap params) { + private void assertMatched(Resource resource, SearchParameterMap params) { SubscriptionMatchResult result = match(resource, params); assertTrue(result.getUnsupportedReason(), result.supported()); assertTrue(result.matched()); + assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(getCriteria(resource, params))); } - private void assertNotMatched(IBaseResource resource, SearchParameterMap params) { + private void assertNotMatched(Resource resource, SearchParameterMap params) { SubscriptionMatchResult result = match(resource, params); assertTrue(result.getUnsupportedReason(), result.supported()); assertFalse(result.matched()); + assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(getCriteria(resource, params))); + } + + private SubscriptionMatchResult match(Resource theResource, SearchParameterMap theParams) { + return match(getCriteria(theResource, theParams), theResource); + } + + private String getCriteria(Resource theResource, SearchParameterMap theParams) { + return theResource.getResourceType().name() + theParams.toNormalizedQueryString(myContext); + } + + private SubscriptionMatchResult match(String criteria, Resource theResource) { + ourLog.info("Criteria: <{}>", criteria); + return myInMemorySubscriptionMatcher.match(criteria, theResource); + } + + private void assertUnsupported(Resource resource, SearchParameterMap theParams) { + SubscriptionMatchResult result = match(resource, theParams); + assertFalse(result.supported()); + assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(getCriteria(resource, theParams))); } /* @@ -93,7 +105,6 @@ public class InMemorySubscriptionMatcherTestR4 { SearchParameterMap params = new SearchParameterMap(); params.add("_has", new HasParam("Observation", "subject", "identifier", "urn:system|FOO")); - String criteria = params.toNormalizedQueryString(myContext); assertUnsupported(patient, params); } @@ -130,7 +141,7 @@ public class InMemorySubscriptionMatcherTestR4 { TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01"); StringParam v1 = new StringParam("testSearchCompositeParamS01"); - CompositeParam<TokenParam, StringParam> val = new CompositeParam<TokenParam, StringParam>(v0, v1); + CompositeParam<TokenParam, StringParam> val = new CompositeParam<>(v0, v1); SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val); assertUnsupported(o1, params); } @@ -170,11 +181,11 @@ public class InMemorySubscriptionMatcherTestR4 { } @Test - public void testIdNotSupported() { + public void testIdSupported() { Observation o1 = new Observation(); SearchParameterMap params = new SearchParameterMap(); params.add("_id", new StringParam("testSearchForUnknownAlphanumericId")); - assertUnsupported(o1, params); + assertNotMatched(o1, params); } @Test @@ -190,7 +201,7 @@ public class InMemorySubscriptionMatcherTestR4 { } @Test - public void testSearchLastUpdatedParamUnsupported() throws InterruptedException { + public void testSearchLastUpdatedParamUnsupported() { String methodName = "testSearchLastUpdatedParam"; DateTimeType today = new DateTimeType(new Date(), TemporalPrecisionEnum.DAY); Patient patient = new Patient(); @@ -294,12 +305,12 @@ public class InMemorySubscriptionMatcherTestR4 { @Test public void testSearchQuantityWrongParam() { Condition c1 = new Condition(); - c1.setAbatement(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L))); + c1.setAbatement(new Range().setLow(new SimpleQuantity().setValue(1L)).setHigh(new SimpleQuantity().setValue(1L))); SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Condition.SP_ABATEMENT_AGE, new QuantityParam("1")); assertMatched(c1, params); Condition c2 = new Condition(); - c2.setOnset(new Range().setLow((SimpleQuantity) new SimpleQuantity().setValue(1L)).setHigh((SimpleQuantity) new SimpleQuantity().setValue(1L))); + c2.setOnset(new Range().setLow(new SimpleQuantity().setValue(1L)).setHigh(new SimpleQuantity().setValue(1L))); params = new SearchParameterMap().add(Condition.SP_ONSET_AGE, new QuantityParam("1")); assertMatched(c2, params); @@ -414,7 +425,7 @@ public class InMemorySubscriptionMatcherTestR4 { } @Test - public void testSearchStringParam() throws Exception { + public void testSearchStringParam() { Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("001"); patient.addName().setFamily("Tester_testSearchStringParam").addGiven("Joe"); @@ -569,13 +580,11 @@ public class InMemorySubscriptionMatcherTestR4 { @Test public void testSearchTokenWithNotModifierUnsupported() { - String male, female; Patient patient = new Patient(); patient.addIdentifier().setSystem("urn:system").setValue("001"); patient.addName().setFamily("Tester").addGiven("Joe"); patient.setGender(Enumerations.AdministrativeGender.MALE); - List<String> patients; SearchParameterMap params; params = new SearchParameterMap(); @@ -640,7 +649,6 @@ public class InMemorySubscriptionMatcherTestR4 { o2.setValue(q2); SearchParameterMap map; - IBundleProvider found; QuantityParam param; map = new SearchParameterMap(); @@ -682,9 +690,7 @@ public class InMemorySubscriptionMatcherTestR4 { Patient pt1 = new Patient(); pt1.addName().setFamily("ABCDEFGHIJK"); - List<String> ids; SearchParameterMap map; - IBundleProvider results; // Contains = true map = new SearchParameterMap(); @@ -875,7 +881,7 @@ public class InMemorySubscriptionMatcherTestR4 { map.add(Observation.SP_DATE, new DateParam("2011-01-02")); for (Observation obs : nlist) { -// assertNotMatched(obs, map); + assertNotMatched(obs, map); } for (Observation obs : ylist) { ourLog.info("Obs {} has time {}", obs.getId(), obs.getEffectiveDateTimeType().getValue().toString()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java index 871934e7692..42bbad344fc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java @@ -6,6 +6,8 @@ import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; import ca.uhn.fhir.jpa.subscription.NotificationServlet; import ca.uhn.fhir.jpa.subscription.SubscriptionTestUtil; +import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionConstants; +import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Update; @@ -106,12 +108,11 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { subscription.setChannel(channel); MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute(); - subscription.setId(methodOutcome.getId().getIdPart()); mySubscriptionIds.add(methodOutcome.getId()); waitForQueueToDrain(); - return subscription; + return (Subscription)methodOutcome.getResource(); } private Observation sendObservation(String code, String system) { @@ -352,7 +353,7 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { waitForSize(0, ourCreatedObservations); waitForSize(5, ourUpdatedObservations); - Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); + Assert.assertNotEquals(subscription1.getId(), subscription2.getId()); Assert.assertFalse(observation1.getId().isEmpty()); Assert.assertFalse(observation2.getId().isEmpty()); } @@ -414,6 +415,58 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { mySubscriptionTestUtil.waitForQueueToDrain(); } + @Test + public void testSubscriptionActivatesInMemoryTag() throws Exception { + String payload = "application/fhir+xml"; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + + Subscription subscriptionOrig = createSubscription(criteria1, payload, ourListenerServerBase); + IdType subscriptionId = subscriptionOrig.getIdElement(); + + assertEquals(Subscription.SubscriptionStatus.REQUESTED, subscriptionOrig.getStatus()); + List<Coding> tags = subscriptionOrig.getMeta().getTag(); + assertEquals(1, tags.size()); + Coding tag = tags.get(0); + assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem()); + assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.getCode()); + assertEquals("In-memory", tag.getDisplay()); + + Subscription subscriptionActivated = ourClient.read().resource(Subscription.class).withId(subscriptionId.toUnqualifiedVersionless()).execute(); + assertEquals(Subscription.SubscriptionStatus.ACTIVE, subscriptionActivated.getStatus()); + tags = subscriptionActivated.getMeta().getTag(); + assertEquals(1, tags.size()); + tag = tags.get(0); + assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem()); + assertEquals(SubscriptionMatchingStrategy.IN_MEMORY.toString(), tag.getCode()); + assertEquals("In-memory", tag.getDisplay()); + } + + @Test + public void testSubscriptionActivatesDatabaseTag() throws Exception { + String payload = "application/fhir+xml"; + + Subscription subscriptionOrig = createSubscription("Observation?code=17861-6&context.type=IHD", payload, ourListenerServerBase); + IdType subscriptionId = subscriptionOrig.getIdElement(); + + List<Coding> tags = subscriptionOrig.getMeta().getTag(); + assertEquals(1, tags.size()); + Coding tag = tags.get(0); + assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem()); + assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.getCode()); + assertEquals("Database", tag.getDisplay()); + + Subscription subscription = ourClient.read().resource(Subscription.class).withId(subscriptionId.toUnqualifiedVersionless()).execute(); + assertEquals(Subscription.SubscriptionStatus.ACTIVE, subscription.getStatus()); + tags = subscription.getMeta().getTag(); + assertEquals(1, tags.size()); + tag = tags.get(0); + assertEquals(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY, tag.getSystem()); + assertEquals(SubscriptionMatchingStrategy.DATABASE.toString(), tag.getCode()); + assertEquals("Database", tag.getDisplay()); + } + @BeforeClass public static void startListenerServer() throws Exception { ourListenerPort = PortUtil.findFreePort(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java index 146adafb1e8..d8e51f6f5fb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -86,7 +86,7 @@ public enum Pointcut { SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"), /** - * Invoked before a resource will be updated, immediately before the resource + * Invoked before a resource will be created, immediately before the resource * is persisted to the database. * <p> * Hooks will have access to the contents of the resource being created diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index 0c87b0d0076..cd0802bf645 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java @@ -24,12 +24,14 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscriptionChannelType; +import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchingStrategy; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.IPrimitiveDatatype; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseMetaType; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Extension; @@ -261,4 +263,33 @@ public class SubscriptionCanonicalizer<S extends IBaseResource> { return retVal; } + + public String getCriteria(IBaseResource theSubscription) { + switch (myFhirContext.getVersion().getVersion()) { + case DSTU2: + return ((ca.uhn.fhir.model.dstu2.resource.Subscription)theSubscription).getCriteria(); + case DSTU3: + return ((org.hl7.fhir.dstu3.model.Subscription)theSubscription).getCriteria(); + case R4: + return ((org.hl7.fhir.r4.model.Subscription)theSubscription).getCriteria(); + default: + throw new ConfigurationException("Subscription not supported for version: " + myFhirContext.getVersion().getVersion()); + } + } + + + public void setMatchingStrategyTag(FhirContext theFhirContext, IBaseResource theSubscription, SubscriptionMatchingStrategy theStrategy) { + IBaseMetaType meta = theSubscription.getMeta(); + String value = theStrategy.toString(); + String display; + + if (theStrategy == SubscriptionMatchingStrategy.DATABASE) { + display = "Database"; + } else if (theStrategy == SubscriptionMatchingStrategy.IN_MEMORY) { + display = "In-memory"; + } else { + throw new IllegalStateException("Unknown " + SubscriptionMatchingStrategy.class.getSimpleName() + ": "+theStrategy); + } + meta.addTag().setSystem(SubscriptionConstants.EXT_SUBSCRIPTION_MATCHING_STRATEGY).setCode(value).setDisplay(display); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java index 5d9f3f1ef20..66f4cd58d18 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionConstants.java @@ -67,6 +67,13 @@ public class SubscriptionConstants { */ public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version"; + /** + * Indicate which strategy will be used to match this subscription + */ + + public static final String EXT_SUBSCRIPTION_MATCHING_STRATEGY = "http://hapifhir.io/fhir/StructureDefinition/subscription-matching-strategy"; + + /** * The number of threads used in subscription channel processing */ @@ -79,12 +86,8 @@ public class SubscriptionConstants { public static final int MAX_SUBSCRIPTION_RESULTS = 1000; /** - * The size of the queue used for sending resources to the subscription matching processor + * The size of the queue used for sending resources to the subscription matching processor and by each subscription delivery queue */ - public static final int PROCESSING_EXECUTOR_QUEUE_SIZE = 1000; - /** - * The size of the queue used by each subscription delivery queue - */ public static final int DELIVERY_EXECUTOR_QUEUE_SIZE = 1000; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java index 34f1793fede..e93e23a6f4e 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.param.BaseParamWithPrefix; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.UrlUtil; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -54,79 +55,93 @@ public class CriteriaResourceMatcher { @Autowired FhirContext myFhirContext; + /** + * This method is called in two different scenarios. With a null theResource, it determines whether database matching might be required. + * Otherwise, it tries to perform the match in-memory, returning UNSUPPORTED if it's not possible. + * + * Note that there will be cases where it returns UNSUPPORTED with a null resource, but when a non-null resource it returns supported and no match. + * This is because an earlier parameter may be matchable in-memory in which case processing stops and we never get to the parameter + * that would have required a database call. + * + */ + public SubscriptionMatchResult match(String theCriteria, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) { + RuntimeResourceDefinition resourceDefinition; + if (theResource == null) { + resourceDefinition = UrlUtil.parseUrlResourceType(myFhirContext, theCriteria); + } else { + resourceDefinition = myFhirContext.getResourceDefinition(theResource); + } SearchParameterMap searchParameterMap; try { - searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, myFhirContext.getResourceDefinition(theResource)); + searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, resourceDefinition); } catch (UnsupportedOperationException e) { - return new SubscriptionMatchResult(theCriteria, CRITERIA); + return SubscriptionMatchResult.unsupportedFromReason(SubscriptionMatchResult.PARSE_FAIL); } searchParameterMap.clean(); if (searchParameterMap.getLastUpdated() != null) { - return new SubscriptionMatchResult(Constants.PARAM_LASTUPDATED, "Standard Parameters not supported"); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(Constants.PARAM_LASTUPDATED, SubscriptionMatchResult.STANDARD_PARAMETER); } for (Map.Entry<String, List<List<? extends IQueryParameterType>>> entry : searchParameterMap.entrySet()) { String theParamName = entry.getKey(); List<List<? extends IQueryParameterType>> theAndOrParams = entry.getValue(); - SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResource, theSearchParams); + SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, resourceDefinition, theResource, theSearchParams); if (!result.matched()){ return result; } } - return new SubscriptionMatchResult(true, CRITERIA); + return SubscriptionMatchResult.successfulMatch(); } // This method is modelled from SearchBuilder.searchForIdsWithAndOr() - private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) { + private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, IBaseResource theResource, ResourceIndexedSearchParams theSearchParams) { if (theAndOrParams.isEmpty()) { - return new SubscriptionMatchResult(true, CRITERIA); + return SubscriptionMatchResult.successfulMatch(); } if (hasQualifiers(theAndOrParams)) { - - return new SubscriptionMatchResult(theParamName, "Standard Parameters not supported."); - + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.STANDARD_PARAMETER); } if (hasPrefixes(theAndOrParams)) { - return new SubscriptionMatchResult(theParamName, "Prefixes not supported."); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PREFIX); } if (hasChain(theAndOrParams)) { - return new SubscriptionMatchResult(theParamName, "Chained references are not supported"); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.CHAIN); } switch (theParamName) { case IAnyResource.SP_RES_ID: - return new SubscriptionMatchResult(matchIdsAndOr(theAndOrParams, theResource), CRITERIA); + return SubscriptionMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource)); case IAnyResource.SP_RES_LANGUAGE: - - return new SubscriptionMatchResult(theParamName, CRITERIA); - case Constants.PARAM_HAS: - - return new SubscriptionMatchResult(theParamName, CRITERIA); - case Constants.PARAM_TAG: case Constants.PARAM_PROFILE: case Constants.PARAM_SECURITY: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM); default: - String resourceName = myFhirContext.getResourceDefinition(theResource).getName(); + String resourceName = theResourceDefinition.getName(); RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); } } private boolean matchIdsAndOr(List<List<? extends IQueryParameterType>> theAndOrParams, IBaseResource theResource) { + if (theResource == null) { + return true; + } return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource)); } private boolean matchIdsOr(List<? extends IQueryParameterType> theOrParams, IBaseResource theResource) { + if (theResource == null) { + return true; + } return theOrParams.stream().anyMatch(param -> param instanceof StringParam && matchId(((StringParam)param).getValue(), theResource.getIdElement())); } @@ -144,16 +159,20 @@ public class CriteriaResourceMatcher { case URI: case DATE: case REFERENCE: - return new SubscriptionMatchResult(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)), CRITERIA); + if (theSearchParams == null) { + return SubscriptionMatchResult.successfulMatch(); + } else { + return SubscriptionMatchResult.fromBoolean(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams))); + } case COMPOSITE: case HAS: case SPECIAL: default: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM); } } else { if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { - return new SubscriptionMatchResult(theParamName, CRITERIA); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM); } else { throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName); } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java index 4ed696989cb..4e45d9fe684 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; * 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. @@ -21,26 +21,47 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; */ public class SubscriptionMatchResult { + public static final String PARSE_FAIL = "Failed to translate parse query string"; + public static final String STANDARD_PARAMETER = "Standard parameters not supported"; + public static final String PREFIX = "Prefixes not supported"; + public static final String CHAIN = "Chained references are not supported"; + public static final String PARAM = "Param not supported"; + private final boolean myMatch; private final boolean mySupported; private final String myUnsupportedParameter; private final String myUnsupportedReason; - private final String myMatcherShortName; - public SubscriptionMatchResult(boolean theMatch, String theMatcherShortName) { + private boolean myInMemory = false; + + private SubscriptionMatchResult(boolean theMatch) { this.myMatch = theMatch; this.mySupported = true; this.myUnsupportedParameter = null; this.myUnsupportedReason = null; - this.myMatcherShortName = theMatcherShortName; } - public SubscriptionMatchResult(String theUnsupportedParameter, String theMatcherShortName) { + private SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) { this.myMatch = false; this.mySupported = false; this.myUnsupportedParameter = theUnsupportedParameter; - this.myUnsupportedReason = "Parameter not supported"; - this.myMatcherShortName = theMatcherShortName; + this.myUnsupportedReason = theUnsupportedReason; + } + + public static SubscriptionMatchResult successfulMatch() { + return new SubscriptionMatchResult(true); + } + + public static SubscriptionMatchResult fromBoolean(boolean theMatched) { + return new SubscriptionMatchResult(theMatched); + } + + public static SubscriptionMatchResult unsupportedFromReason(String theUnsupportedReason) { + return new SubscriptionMatchResult(null, theUnsupportedReason); + } + + public static SubscriptionMatchResult unsupportedFromParameterAndReason(String theUnsupportedParameter, String theUnsupportedReason) { + return new SubscriptionMatchResult(theUnsupportedParameter, theUnsupportedReason); } public boolean supported() { @@ -52,14 +73,17 @@ public class SubscriptionMatchResult { } public String getUnsupportedReason() { - return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason; + if (myUnsupportedParameter != null) { + return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason; + } + return myUnsupportedReason; } - /** - * Returns a short name of the matcher that generated this - * response, for use in logging - */ - public String matcherShortName() { - return myMatcherShortName; + public boolean isInMemory() { + return myInMemory; + } + + public void setInMemory(boolean theInMemory) { + myInMemory = theInMemory; } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java new file mode 100644 index 00000000000..d2532363999 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.jpa.subscription.module.matcher; + +public enum SubscriptionMatchingStrategy { + /** + * Resources can be matched against this subcription in-memory without needing to make a call out to a FHIR Repository + */ + IN_MEMORY, + + /** + * Resources cannot be matched against this subscription in-memory. We need to make a call to a FHIR Repository to determine a match + */ + DATABASE +} + diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java new file mode 100644 index 00000000000..94bd2d4dc88 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java @@ -0,0 +1,19 @@ +package ca.uhn.fhir.jpa.subscription.module.matcher; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SubscriptionStrategyEvaluator { + + @Autowired + private CriteriaResourceMatcher myCriteriaResourceMatcher; + + public SubscriptionMatchingStrategy determineStrategy(String theCriteria) { + SubscriptionMatchResult result = myCriteriaResourceMatcher.match(theCriteria, null, null); + if (result.supported()) { + return SubscriptionMatchingStrategy.IN_MEMORY; + } + return SubscriptionMatchingStrategy.DATABASE; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index d2aab38685a..ec9958a39e4 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -117,8 +117,10 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { if (!matchResult.matched()) { continue; } - - ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), resourceId.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName()); + ourLog.debug("Subscription {} was matched by resource {} {}", + nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), + resourceId.toUnqualifiedVersionless().getValue(), + matchResult.isInMemory() ? "in-memory" : "by querying the repository"); IBaseResource payload = theMsg.getNewPayload(myFhirContext); @@ -161,7 +163,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { criteriaResource = criteriaResource.substring(0, criteriaResource.indexOf("?")); } - if (resourceType != null && criteriaString != null && !criteriaResource.equals(resourceType)) { + if (resourceType != null && !criteriaResource.equals(resourceType)) { ourLog.trace("Skipping subscription search for {} because it does not match the criteria {}", resourceType, criteriaString); return false; } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java index c18641d72f9..f7530c43172 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java @@ -7,22 +7,22 @@ import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.codesystems.MedicationRequestCategory; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.util.Arrays; import java.util.Collections; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test { + @Autowired + SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator; @Autowired InMemorySubscriptionMatcher myInMemorySubscriptionMatcher; private void assertUnsupported(IBaseResource resource, String criteria) { assertFalse(myInMemorySubscriptionMatcher.match(criteria, resource).supported()); + assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(criteria)); } private void assertMatched(IBaseResource resource, String criteria) { @@ -30,13 +30,20 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test assertTrue(result.supported()); assertTrue(result.matched()); + assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(criteria)); } private void assertNotMatched(IBaseResource resource, String criteria) { + assertNotMatched(resource, criteria, SubscriptionMatchingStrategy.IN_MEMORY); + } + + private void assertNotMatched(IBaseResource resource, String criteria, SubscriptionMatchingStrategy theSubscriptionMatchingStrategy) { SubscriptionMatchResult result = myInMemorySubscriptionMatcher.match(criteria, resource); assertTrue(result.supported()); assertFalse(result.matched()); + + assertEquals(theSubscriptionMatchingStrategy, mySubscriptionStrategyEvaluator.determineStrategy(criteria)); } @@ -152,7 +159,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test { Observation obs = new Observation(); obs.getCode().addCoding().setCode("XXX"); - assertNotMatched(obs, criteria); + assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE); } { Observation obs = new Observation(); @@ -168,7 +175,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test { Observation obs = new Observation(); obs.getCode().addCoding().setCode("XXX"); - assertNotMatched(obs, criteria); + assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE); } { Observation obs = new Observation(); @@ -266,7 +273,7 @@ public class InMemorySubscriptionMatcherTestR3 extends BaseSubscriptionDstu3Test { Observation obs = new Observation(); obs.getCode().addCoding().setCode("XXX"); - assertNotMatched(obs, criteria); + assertNotMatched(obs, criteria, SubscriptionMatchingStrategy.DATABASE); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluatorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluatorTest.java new file mode 100644 index 00000000000..1dbbc3159f7 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluatorTest.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.subscription.module.matcher; + +import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.Assert.assertEquals; +import static org.junit.matchers.JUnitMatchers.containsString; + +public class SubscriptionStrategyEvaluatorTest extends BaseSubscriptionDstu3Test { + @Autowired + SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testInMemory() { + assertInMemory("Observation?"); + assertInMemory("QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord"); + assertInMemory("CommunicationRequest?occurrence==2018-10-17"); + assertInMemory("ProcedureRequest?intent=original-order"); + assertInMemory("MedicationRequest?intent=instance-order&category=outpatient&date==2018-10-19"); + assertInMemory("MedicationRequest?intent=plan&category=outpatient&status=suspended,entered-in-error,cancelled,stopped"); + assertDatabase("Observation?code=FR_Org1Blood2nd,FR_Org1Blood3rd,FR_Org%201BldCult,FR_Org2Blood2nd,FR_Org2Blood3rd,FR_Org%202BldCult,FR_Org3Blood2nd,FR_Org3Blood3rd,FR_Org3BldCult,FR_Org4Blood2nd,FR_Org4Blood3rd,FR_Org4BldCult,FR_Org5Blood2nd,FR_Org5Blood3rd,FR_Org%205BldCult,FR_Org6Blood2nd,FR_Org6Blood3rd,FR_Org6BldCult,FR_Org7Blood2nd,FR_Org7Blood3rd,FR_Org7BldCult,FR_Org8Blood2nd,FR_Org8Blood3rd,FR_Org8BldCult,FR_Org9Blood2nd,FR_Org9Blood3rd,FR_Org9BldCult,FR_Bld2ndCulture,FR_Bld3rdCulture,FR_Blood%20Culture,FR_Com1Bld3rd,FR_Com1BldCult,FR_Com2Bld2nd,FR_Com2Bld3rd,FR_Com2BldCult,FR_CultureBld2nd,FR_CultureBld3rd,FR_CultureBldCul,FR_GmStainBldCul,FR_GramStain2Bld,FR_GramStain3Bld,FR_GramStNegBac&context.type=IHD"); + assertInMemory("Procedure?category=Hemodialysis"); + assertInMemory("Procedure?code=HD_Standard&status=completed&location=Lab123"); + assertInMemory("Procedure?code=HD_Standard&status=completed"); + assertInMemory("QuestionnaireResponse?questionnaire=HomeAbsenceHospitalizationRecord,ARIncenterAbsRecord,FMCSWDepressionSymptomsScreener,FMCAKIComprehensiveSW,FMCSWIntensiveScreener,FMCESRDComprehensiveSW,FMCNutritionProgressNote,FMCAKIComprehensiveRN"); + assertInMemory("EpisodeOfCare?status=active"); + assertInMemory("Observation?code=111111111&_format=xml"); + assertInMemory("Observation?code=SNOMED-CT|123&_format=xml"); + + assertDatabase("Observation?code=17861-6&context.type=IHD"); + assertDatabase("Observation?context.type=IHD&code=17861-6"); + + exception.expect(InvalidRequestException.class); + exception.expectMessage(containsString("Resource type Observation does not have a parameter with name: codeee")); + assertInMemory("Observation?codeee=SNOMED-CT|123&_format=xml"); + } + + private void assertDatabase(String theCriteria) { + assertEquals(SubscriptionMatchingStrategy.DATABASE, mySubscriptionStrategyEvaluator.determineStrategy(theCriteria)); + } + + private void assertInMemory(String theCriteria) { + assertEquals(SubscriptionMatchingStrategy.IN_MEMORY, mySubscriptionStrategyEvaluator.determineStrategy(theCriteria)); + } +} + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index aac1c8be1cc..5de3d69c584 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -327,6 +327,12 @@ The casing of the base64Binary datatype was incorrect in the DSTU3 and R4 model classes. This has been corrected. </action> + <action type="add"> + Add a "subscription-matching-strategy" meta tag to incoming subscriptions with value of IN_MEMORY + or DATABASE indicating whether the subscription can be matched against new resources in-memory or + whether a call out to the database may be required. I say "may" because subscription matches fail fast + so a negative match may be performed in-memory, but a positive match will require a database call. + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From 503d1d8affdf268ceea6b675cd269b1a0d9784bc Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sat, 26 Jan 2019 18:42:01 -0500 Subject: [PATCH 54/56] Subscription NPE fix --- .../jpa/model/interceptor/api/Pointcut.java | 4 +- .../executor/InterceptorService.java | 4 +- .../extractor/ResourceLinkExtractor.java | 4 +- .../module/ResourceModifiedMessage.java | 9 ++-- .../matcher/SubscriptionMatchResult.java | 4 +- .../matcher/SubscriptionMatchingStrategy.java | 20 +++++++++ .../SubscriptionStrategyEvaluator.java | 20 +++++++++ .../SubscriptionMatchingSubscriber.java | 4 +- ...kingQueueSubscribableChannelDstu3Test.java | 2 +- .../SubscriptionCheckingSubscriberTest.java | 43 ++++++++++++++++++- 10 files changed, 98 insertions(+), 16 deletions(-) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java index d8e51f6f5fb..22b92d3d390 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.api; * 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. diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java index 7fc8fd9c4fd..26e2831d361 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.model.interceptor.executor; * 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. diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java index afdc1b7f408..042237d9fd2 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/ResourceLinkExtractor.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.searchparam.extractor; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java index d2bcffddc57..2758a29569c 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/ResourceModifiedMessage.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module; * 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. @@ -149,7 +149,10 @@ public class ResourceModifiedMessage implements IResourceMessage { for (ResourceReferenceInfo next : refs) { String ref = next.getResourceReference().getReferenceElement().getValue(); if (isBlank(ref)) { - ref = next.getResourceReference().getResource().getIdElement().getValue(); + IBaseResource resource = next.getResourceReference().getResource(); + if (resource != null) { + ref = resource.getIdElement().getValue(); + } } if (isNotBlank(ref)) { if (ref.startsWith("#")) { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java index 4e45d9fe684..52237826630 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java index d2532363999..7c273a0d9ad 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2019 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% + */ + public enum SubscriptionMatchingStrategy { /** * Resources can be matched against this subcription in-memory without needing to make a call out to a FHIR Repository diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java index 94bd2d4dc88..2e5152db1ae 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.subscription.module.matcher; +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2019 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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java index ec9958a39e4..d6ab7a02460 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriber.java @@ -33,9 +33,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * 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. diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java index e44c8b2848d..995b03f1fe3 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/standalone/BaseBlockingQueueSubscribableChannelDstu3Test.java @@ -68,7 +68,7 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base protected static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>()); private static SubscribableChannel ourSubscribableChannel; private List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); - private static AtomicLong idCounter = new AtomicLong(); + protected static AtomicLong idCounter = new AtomicLong(); protected PointcutLatch mySubscriptionMatchingPost = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED); protected PointcutLatch mySubscriptionActivatedPost = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java index d3fe0ba2a38..2ff76686ceb 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionCheckingSubscriberTest.java @@ -1,12 +1,14 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; -import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.standalone.BaseBlockingQueueSubscribableChannelDstu3Test; import ca.uhn.fhir.rest.api.Constants; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.Observation; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import static org.junit.Assert.assertEquals; @@ -77,4 +79,41 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri assertEquals(0, ourContentTypes.size()); } + + @Test + public void testReferenceWithDisplayOnly() throws Exception { + String payload = "application/fhir+json"; + + String code = "1000000050"; + String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; + String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; + + sendSubscription(criteria1, payload, ourListenerServerBase); + sendSubscription(criteria2, payload, ourListenerServerBase); + + assertEquals(2, mySubscriptionRegistry.size()); + + ourObservationListener.setExpectedCount(1); + Observation observation = new Observation(); + IdType id = new IdType("Observation", idCounter.incrementAndGet()); + observation.setId(id); + + // Reference has display only! + observation.getSubject().setDisplay("Mr Jones"); + + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(code); + coding.setSystem("SNOMED-CT"); + + observation.setStatus(Observation.ObservationStatus.FINAL); + + sendResource(observation); + ourObservationListener.awaitExpected(); + + assertEquals(1, ourContentTypes.size()); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + } + } From 5d540d9208ea2f1253a6edd0c17ec2887c72165b Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Sun, 27 Jan 2019 20:03:48 -0500 Subject: [PATCH 55/56] Query optimization in JPA --- .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 148 ++++++++++-------- .../jpa/config/CaptureQueriesListener.java | 92 +++++++++++ .../ca/uhn/fhir/jpa/config/TestR4Config.java | 1 + .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 2 + .../r4/FhirResourceDaoR4SearchNoFtTest.java | 48 ++++++ pom.xml | 2 +- src/changes/changes.xml | 6 + 7 files changed, 234 insertions(+), 65 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/CaptureQueriesListener.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index e5c0a909037..a5187798ee0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -80,7 +80,6 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import org.thymeleaf.util.ListUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -385,7 +384,8 @@ public class SearchBuilder implements ISearchBuilder { List<Predicate> codePredicates = new ArrayList<>(); - for (IQueryParameterType nextOr : theList) { + for (int orIdx = 0; orIdx < theList.size(); orIdx++) { + IQueryParameterType nextOr = theList.get(orIdx); if (nextOr instanceof ReferenceParam) { ReferenceParam ref = (ReferenceParam) nextOr; @@ -496,15 +496,16 @@ public class SearchBuilder implements ISearchBuilder { boolean foundChainMatch = false; - String chain = ref.getChain(); - String remainingChain = null; - int chainDotIndex = chain.indexOf('.'); - if (chainDotIndex != -1) { - remainingChain = chain.substring(chainDotIndex + 1); - chain = chain.substring(0, chainDotIndex); - } - for (Class<? extends IBaseResource> nextType : resourceTypes) { + + String chain = ref.getChain(); + String remainingChain = null; + int chainDotIndex = chain.indexOf('.'); + if (chainDotIndex != -1) { + remainingChain = chain.substring(chainDotIndex + 1); + chain = chain.substring(0, chainDotIndex); + } + RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType); String subResourceName = typeDef.getName(); @@ -531,37 +532,29 @@ public class SearchBuilder implements ISearchBuilder { } } - IQueryParameterType chainValue; - if (remainingChain != null) { - if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { - ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain); + ArrayList<IQueryParameterType> orValues = Lists.newArrayList(); + + for (IQueryParameterType next : theList) { + String nextValue = next.getValueAsQueryToken(myContext); + IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue); + if (chainValue == null) { continue; } - - chainValue = new ReferenceParam(); - chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); - ((ReferenceParam) chainValue).setChain(remainingChain); - } else if (isMeta) { - IQueryParameterType type = myMatchUrlService.newInstanceType(chain); - type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); - chainValue = type; - } else { - chainValue = toParameterType(param, qualifier, resourceId); + foundChainMatch = true; + orValues.add(chainValue); } - foundChainMatch = true; - Subquery<Long> subQ = myResourceTableQuery.subquery(Long.class); Root<ResourceTable> subQfrom = subQ.from(ResourceTable.class); subQ.select(subQfrom.get("myId").as(Long.class)); List<List<? extends IQueryParameterType>> andOrParams = new ArrayList<>(); - andOrParams.add(Collections.singletonList(chainValue)); + andOrParams.add(orValues); /* * We're doing a chain call, so push the current query root * and predicate list down and put new ones at the top of the - * stack and run a subuery + * stack and run a subquery */ Root<ResourceTable> stackRoot = myResourceTableRoot; ArrayList<Predicate> stackPredicates = myPredicates; @@ -573,9 +566,11 @@ public class SearchBuilder implements ISearchBuilder { // Create the subquery predicates myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), subResourceName)); myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted"))); - searchForIdsWithAndOr(subResourceName, chain, andOrParams); - subQ.where(toArray(myPredicates)); + if (foundChainMatch) { + searchForIdsWithAndOr(subResourceName, chain, andOrParams); + subQ.where(toArray(myPredicates)); + } /* * Pop the old query root and predicate list back @@ -593,6 +588,10 @@ public class SearchBuilder implements ISearchBuilder { if (!foundChainMatch) { throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain())); } + + myPredicates.add(myBuilder.or(toArray(codePredicates))); + return; + } } else { @@ -604,6 +603,28 @@ public class SearchBuilder implements ISearchBuilder { myPredicates.add(myBuilder.or(toArray(codePredicates))); } + private IQueryParameterType mapReferenceChainToRawParamType(String remainingChain, RuntimeSearchParam param, String theParamName, String qualifier, Class<? extends IBaseResource> nextType, String chain, boolean isMeta, String resourceId) { + IQueryParameterType chainValue; + if (remainingChain != null) { + if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { + ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain); + return null; + } + + chainValue = new ReferenceParam(); + chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); + ((ReferenceParam) chainValue).setChain(remainingChain); + } else if (isMeta) { + IQueryParameterType type = myMatchUrlService.newInstanceType(chain); + type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); + chainValue = type; + } else { + chainValue = toParameterType(param, qualifier, resourceId); + } + + return chainValue; + } + private void addPredicateResourceId(List<List<? extends IQueryParameterType>> theValues) { for (List<? extends IQueryParameterType> nextValue : theValues) { Set<Long> orPids = new HashSet<>(); @@ -794,24 +815,27 @@ public class SearchBuilder implements ISearchBuilder { private void addPredicateToken(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList) { - Join<ResourceTable, ResourceIndexedSearchParamToken> join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); - if (theList.get(0).getMissing() != null) { + Join<ResourceTable, ResourceIndexedSearchParamToken> join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); return; } List<Predicate> codePredicates = new ArrayList<>(); + Join<ResourceTable, ResourceIndexedSearchParamToken> join = null; for (IQueryParameterType nextOr : theList) { if (nextOr instanceof TokenParam) { TokenParam id = (TokenParam) nextOr; if (id.isText()) { addPredicateString(theResourceName, theParamName, theList); - continue; + break; } } + if (join == null) { + join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); + } Predicate singleCode = createPredicateToken(nextOr, theResourceName, theParamName, myBuilder, join); codePredicates.add(singleCode); } @@ -972,38 +996,34 @@ public class SearchBuilder implements ISearchBuilder { @SuppressWarnings("unchecked") private <T> Join<ResourceTable, T> createOrReuseJoin(JoinEnum theType, String theSearchParameterName) { - Join<ResourceTable, ResourceIndexedSearchParamDate> join = null; - - switch (theType) { - case DATE: - join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); - break; - case NUMBER: - join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); - break; - case QUANTITY: - join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); - break; - case REFERENCE: - join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); - break; - case STRING: - join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); - break; - case URI: - join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); - break; - case TOKEN: - join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); - break; - } - JoinKey key = new JoinKey(theSearchParameterName, theType); - if (!myIndexJoins.containsKey(key)) { - myIndexJoins.put(key, join); - } - - return (Join<ResourceTable, T>) join; + return (Join<ResourceTable, T>) myIndexJoins.computeIfAbsent(key, k -> { + Join<ResourceTable, ResourceIndexedSearchParamDate> join = null; + switch (theType) { + case DATE: + join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); + break; + case NUMBER: + join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); + break; + case QUANTITY: + join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); + break; + case REFERENCE: + join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); + break; + case STRING: + join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); + break; + case URI: + join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); + break; + case TOKEN: + join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); + break; + } + return join; + }); } private Predicate createPredicateDate(IQueryParameterType theParam, String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamDate> theFrom) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/CaptureQueriesListener.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/CaptureQueriesListener.java new file mode 100644 index 00000000000..c4dc2cbe04c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/CaptureQueriesListener.java @@ -0,0 +1,92 @@ +package ca.uhn.fhir.jpa.config; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.proxy.ParameterSetOperation; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +public class CaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution { + + private static final LinkedList<Query> LAST_N_QUERIES = new LinkedList<>(); + + @Override + public void execute(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) { + synchronized (LAST_N_QUERIES) { + for (QueryInfo next : queryInfoList) { + String sql = next.getQuery(); + List<String> params; + if (next.getParametersList().size() > 0 && next.getParametersList().get(0).size() > 0) { + List<ParameterSetOperation> values = next + .getParametersList() + .get(0); + params = values.stream() + .map(t -> t.getArgs()[1]) + .map(t -> t != null ? t.toString() : "NULL") + .collect(Collectors.toList()); + } else { + params = new ArrayList<>(); + } + LAST_N_QUERIES.add(0, new Query(sql, params)); + } + while (LAST_N_QUERIES.size() > 100) { + LAST_N_QUERIES.removeLast(); + } + } + } + + public static class Query { + private final String myThreadName = Thread.currentThread().getName(); + private final String mySql; + private final List<String> myParams; + + Query(String theSql, List<String> theParams) { + mySql = theSql; + myParams = Collections.unmodifiableList(theParams); + } + + public String getThreadName() { + return myThreadName; + } + + public String getSql(boolean theInlineParams, boolean theFormat) { + String retVal = mySql; + if (theFormat) { + retVal = new BasicFormatterImpl().format(retVal); + } + + if (theInlineParams) { + List<String> nextParams = new ArrayList<>(myParams); + while (retVal.contains("?") && nextParams.size() > 0) { + int idx = retVal.indexOf("?"); + retVal = retVal.substring(0, idx) + nextParams.remove(0) + retVal.substring(idx + 1); + } + } + + return retVal; + + } + + } + + public static void clear() { + synchronized (LAST_N_QUERIES) { + LAST_N_QUERIES.clear(); + } + } + + /** + * Index 0 is newest! + */ + public static ArrayList<Query> getLastNQueries() { + synchronized (LAST_N_QUERIES) { + return new ArrayList<>(LAST_N_QUERIES); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index d9fcc5773cf..e432dd66a9c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -100,6 +100,7 @@ public class TestR4Config extends BaseJavaConfigR4 { // .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) // .countQuery(new ThreadQueryCountHolder()) .beforeQuery(new BlockLargeNumbersOfParamsListener()) + .afterQuery(new CaptureQueriesListener()) .countQuery(singleQueryCountHolder()) .build(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index dacf263e0ac..290c3194f31 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.config.CaptureQueriesListener; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorRegistry; import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; @@ -94,6 +95,7 @@ public abstract class BaseJpaTest { @After public void afterPerformCleanup() { BaseHapiFhirResourceDao.setDisableIncrementOnUpdateForUnitTest(false); + CaptureQueriesListener.clear(); } @After diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 221fbeaec8c..1491593d61d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao.r4; +import ca.uhn.fhir.jpa.config.CaptureQueriesListener; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; @@ -15,6 +16,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -39,6 +41,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -2160,6 +2163,51 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { } + @Test + public void testSearchLinkToken() { + // /fhirapi/MedicationRequest?category=community&identifier=urn:oid:2.16.840.1.113883.3.7418.12.3%7C&intent=order&medication.code:text=calcitriol,hectorol,Zemplar,rocaltrol,vectical,vitamin%20D,doxercalciferol,paricalcitol&status=active,completed + + Medication m = new Medication(); + m.getCode().setText("valueb"); + myMedicationDao.create(m); + + MedicationRequest mr = new MedicationRequest(); + mr.addCategory().addCoding().setCode("community"); + mr.addIdentifier().setSystem("urn:oid:2.16.840.1.113883.3.7418.12.3").setValue("1"); + mr.setIntent(MedicationRequest.MedicationRequestIntent.ORDER); + mr.setMedication(new Reference(m.getId())); + myMedicationRequestDao.create(mr); + + SearchParameterMap sp = new SearchParameterMap(); + sp.setLoadSynchronous(true); + sp.add("category", new TokenParam("community")); + sp.add("identifier", new TokenParam("urn:oid:2.16.840.1.113883.3.7418.12.3", "1")); + sp.add("intent", new TokenParam("order")); + ReferenceParam param1 = new ReferenceParam("valuea").setChain("code:text"); + ReferenceParam param2 = new ReferenceParam("valueb").setChain("code:text"); + ReferenceParam param3 = new ReferenceParam("valuec").setChain("code:text"); + sp.add("medication", new ReferenceOrListParam().addOr(param1).addOr(param2).addOr(param3)); + + IBundleProvider retrieved = myMedicationRequestDao.search(sp); + assertEquals(1, retrieved.size().intValue()); + + List<String> queries = CaptureQueriesListener + .getLastNQueries() + .stream() + .filter(t->t.getThreadName().equals("main")) + .filter(t -> t.getSql(false, false).toLowerCase().contains("select")) + .filter(t -> t.getSql(false, false).toLowerCase().contains("token")) + .map(t-> t.getSql(true, true)) + .collect(Collectors.toList()); + + ourLog.info("Queries:\n {}", queries.stream().findFirst()); + + String searchQuery = queries.get(0); + assertEquals(searchQuery,3, StringUtils.countMatches(searchQuery.toUpperCase(), "HFJ_SPIDX_TOKEN")); + assertEquals(searchQuery, 5, StringUtils.countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN")); + } + + @Test public void testSearchTokenParam() { Patient patient = new Patient(); diff --git a/pom.xml b/pom.xml index e99d52fdaf3..d9795272a1f 100644 --- a/pom.xml +++ b/pom.xml @@ -534,7 +534,7 @@ <jetty_version>9.4.14.v20181114</jetty_version> <jsr305_version>3.0.2</jsr305_version> <!--<hibernate_version>5.2.10.Final</hibernate_version>--> - <hibernate_version>5.4.0.Final</hibernate_version> + <hibernate_version>5.4.1.Final</hibernate_version> <!-- Update lucene version when you update hibernate-search version --> <hibernate_search_version>5.11.0.Final</hibernate_search_version> <lucene_version>5.5.5</lucene_version> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 5de3d69c584..93b4910ffc1 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -333,6 +333,12 @@ whether a call out to the database may be required. I say "may" because subscription matches fail fast so a negative match may be performed in-memory, but a positive match will require a database call. </action> + <action type="fix"> + When performing a JPA search with a chained :text modifier + (e.g. MedicationStatement?medication.code:text=aspirin,tylenol) a series + of unneccesary joins were introduced to the generated SQL query, harming + performance. This has been fixed. + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add"> From 5f29e4fbf337e253c187a6761415ef1ab0f08732 Mon Sep 17 00:00:00 2001 From: jamesagnew <jamesagnew@gmail.com> Date: Wed, 30 Jan 2019 05:49:45 -0500 Subject: [PATCH 56/56] Fix #1174 - Prevent serialization exception --- .../ca/uhn/fhir/rest/param/DateParam.java | 10 +++- .../r4/FhirResourceDaoR4SearchNoFtTest.java | 53 +++++++++++++++---- src/changes/changes.xml | 5 ++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java index d6c3ec9e9da..246d3b8d099 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateParam.java @@ -255,7 +255,15 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer return b.build(); } - public class DateParamDateTimeHolder extends BaseDateTimeDt { + public static class DateParamDateTimeHolder extends BaseDateTimeDt { + + /** + * Constructor + */ + public DateParamDateTimeHolder() { + super(); + } + @Override protected TemporalPrecisionEnum getDefaultPrecisionForDatatype() { return TemporalPrecisionEnum.SECOND; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index 1491593d61d..096aff201a7 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java @@ -2,9 +2,9 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.config.CaptureQueriesListener; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; -import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; @@ -14,9 +14,9 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; +import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -56,6 +56,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis()); myDaoConfig.setFetchSizeDefaultMaximum(new DaoConfig().getFetchSizeDefaultMaximum()); myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches()); + myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); } @Before @@ -617,7 +618,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { expect1.setResource(resource); expect1.calculateHashes(); - assertThat("Got: \"" + results.toString()+"\"", results, containsInAnyOrder(expect0, expect1)); + assertThat("Got: \"" + results.toString() + "\"", results, containsInAnyOrder(expect0, expect1)); } }); } @@ -1063,7 +1064,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { QuantityParam v1 = new QuantityParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, 150, "http://bar", "code1"); SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, v1); IBundleProvider result = myObservationDao.search(map); - assertThat("Got: "+ toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue())); + assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1.getValue())); } } @@ -1095,7 +1096,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { CompositeParam<TokenParam, QuantityParam> val = new CompositeParam<>(v0, v1); SearchParameterMap map = new SearchParameterMap().setLoadSynchronous(true).add(param, val); IBundleProvider result = myObservationDao.search(map); - assertThat("Got: "+ toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue())); + assertThat("Got: " + toUnqualifiedVersionlessIdValues(result), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id2.getValue())); } { TokenParam v0 = new TokenParam("http://foo", "code1"); @@ -1143,6 +1144,40 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { } + /** + * See #1174 + */ + @Test + public void testSearchDateInSavedSearch() { + for (int i = 1; i <= 9; i++) { + Patient p1 = new Patient(); + p1.getBirthDateElement().setValueAsString("1980-01-0" + i); + String id1 = myPatientDao.create(p1).getId().toUnqualifiedVersionless().getValue(); + } + + myDaoConfig.setSearchPreFetchThresholds(Lists.newArrayList(3, 6, 10)); + + { + // Don't load synchronous + SearchParameterMap map = new SearchParameterMap(); + map.setLastUpdated(new DateRangeParam().setUpperBound(new DateParam(ParamPrefixEnum.LESSTHAN, "2022-01-01"))); + IBundleProvider found = myPatientDao.search(map); + Set<String> dates = new HashSet<>(); + for (int i = 0; i < 9; i++) { + Patient nextResource = (Patient) found.getResources(i, i + 1).get(0); + dates.add(nextResource.getBirthDateElement().getValueAsString()); + } + + assertThat(dates, hasItems( + "1980-01-01", + "1980-01-09" + )); + + assertFalse(map.isLoadSynchronous()); + assertNull(map.getLoadSynchronousUpTo()); + } + } + /** * #222 */ @@ -2194,16 +2229,16 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { List<String> queries = CaptureQueriesListener .getLastNQueries() .stream() - .filter(t->t.getThreadName().equals("main")) + .filter(t -> t.getThreadName().equals("main")) .filter(t -> t.getSql(false, false).toLowerCase().contains("select")) .filter(t -> t.getSql(false, false).toLowerCase().contains("token")) - .map(t-> t.getSql(true, true)) + .map(t -> t.getSql(true, true)) .collect(Collectors.toList()); ourLog.info("Queries:\n {}", queries.stream().findFirst()); String searchQuery = queries.get(0); - assertEquals(searchQuery,3, StringUtils.countMatches(searchQuery.toUpperCase(), "HFJ_SPIDX_TOKEN")); + assertEquals(searchQuery, 3, StringUtils.countMatches(searchQuery.toUpperCase(), "HFJ_SPIDX_TOKEN")); assertEquals(searchQuery, 5, StringUtils.countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN")); } @@ -3362,7 +3397,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { "Observation/YES21", "Observation/YES22", "Observation/YES23" - )); + )); } private void createObservationWithEffective(String theId, String theEffective) { diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 93b4910ffc1..a41fac13d20 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -339,6 +339,11 @@ of unneccesary joins were introduced to the generated SQL query, harming performance. This has been fixed. </action> + <action type="fix"> + A serialization error when performing some searches in the JPA server + using data parameters has been fixed. Thanks to GitHub user + @PickOneFish for reporting! + </action> </release> <release version="3.6.0" date="2018-11-12" description="Food"> <action type="add">