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/examples/pom.xml b/examples/pom.xml index 15adf39931e..98034c83f21 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -92,6 +92,11 @@ org.springframework spring-web + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-base + ${project.version} + org.slf4j 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/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-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 @@ + + org.codehaus.mojo + buildnumber-maven-plugin + true + + + standard + validate + + create + + + + downstream + validate + + create-metadata + + + ${project.build.directory}/generated-sources/properties + ca/uhn/fhir/hapi-fhir-base-build.properties + hapifhir.buildnumber + hapifhir.timestamp + yyyy-MM-dd'T'HH:mm:ss.SXXX + hapifhir.version + + + + src/main/resources true + + ${project.build.directory}/generated-sources/properties + false + 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..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 @@ -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/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 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-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/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 parseQueryString(String theQueryString) { 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..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 @@ -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,46 @@ 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); + if (is != null) { + p.load(is); + } + + ourVersion = p.getProperty("hapifhir.version"); + ourVersion = defaultIfBlank(ourVersion, "(unknown)"); + + ourBuildNumber = p.getProperty("hapifhir.buildnumber"); + ourBuildTime = p.getProperty("hapifhir.timestamp"); + + if (System.getProperty("suppress_hapi_fhir_version_log") == null) { + 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/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-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-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/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-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-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..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 @@ -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-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..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; @@ -58,7 +61,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; @@ -70,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.*; @@ -116,7 +120,7 @@ public abstract class BaseHapiFhirDao 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 ourRetrievalContexts = new HashMap(); + private static final Map ourRetrievalContexts = new HashMap<>(); private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest"; private static boolean ourValidationDisabledForUnitTest; private static boolean ourDisableIncrementOnUpdateForUnitTest = false; @@ -126,6 +130,8 @@ public abstract class BaseHapiFhirDao implements IDao, @Autowired protected IdHelperService myIdHelperService; @Autowired + protected IInterceptorBroadcaster myInterceptorBroadcaster; + @Autowired protected IForcedIdDao myForcedIdDao; @Autowired protected ISearchResultDao mySearchResultDao; @@ -1421,9 +1427,8 @@ public abstract class BaseHapiFhirDao 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) { @@ -1440,9 +1445,13 @@ public abstract class BaseHapiFhirDao 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), @@ -1471,7 +1480,17 @@ public abstract class BaseHapiFhirDao 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; } @@ -1481,6 +1500,10 @@ public abstract class BaseHapiFhirDao 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 fbe42a91c8c..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; @@ -35,7 +37,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; @@ -57,10 +61,11 @@ 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; -import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.persistence.NoResultException; import javax.persistence.TypedQuery; @@ -172,7 +177,7 @@ public abstract class BaseHapiFhirResourceDao extends B } @Override - public DaoMethodOutcome delete(IIdType theId, List theDeleteConflicts, RequestDetails theReques) { + public DaoMethodOutcome delete(IIdType theId, List theDeleteConflicts, RequestDetails theRequest) { if (theId == null || !theId.hasIdPart()) { throw new InvalidRequestException("Can not perform delete, no ID provided"); } @@ -205,12 +210,12 @@ public abstract class BaseHapiFhirResourceDao 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); } } @@ -219,25 +224,33 @@ public abstract class BaseHapiFhirResourceDao 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); @@ -320,6 +333,14 @@ public abstract class BaseHapiFhirResourceDao 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; @@ -422,6 +443,9 @@ public abstract class BaseHapiFhirResourceDao 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); @@ -465,6 +489,14 @@ public abstract class BaseHapiFhirResourceDao 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) { @@ -752,7 +784,6 @@ public abstract class BaseHapiFhirResourceDao extends B return retVal; } - @SuppressWarnings("JpaQlInspection") @Override public MT metaGetOperation(Class theType, RequestDetails theRequestDetails) { // Notify interceptors @@ -943,8 +974,7 @@ public abstract class BaseHapiFhirResourceDao extends B if (entity == null) { if (theId.hasVersionIdPart()) { - TypedQuery q = myEntityManager - .createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class); + TypedQuery 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()); @@ -1304,7 +1334,7 @@ public abstract class BaseHapiFhirResourceDao 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) { @@ -1319,13 +1349,13 @@ public abstract class BaseHapiFhirResourceDao 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/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()); + 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 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/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 void refreshCache(BaseSearchParamRegistry theSearchParamRegistry, long theRefreshInterval) { + public int refreshCache(BaseSearchParamRegistry 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/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 32bdbe3a77f..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 @@ -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()); @@ -492,7 +492,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { 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..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; @@ -100,7 +99,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 { @@ -386,7 +384,8 @@ public class SearchBuilder implements ISearchBuilder { List 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; @@ -497,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 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(); @@ -532,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 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 subQ = myResourceTableQuery.subquery(Long.class); Root subQfrom = subQ.from(ResourceTable.class); subQ.select(subQfrom.get("myId").as(Long.class)); List> 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 stackRoot = myResourceTableRoot; ArrayList stackPredicates = myPredicates; @@ -574,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 @@ -594,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 { @@ -605,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 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> theValues) { for (List nextValue : theValues) { Set orPids = new HashSet<>(); @@ -795,24 +815,27 @@ public class SearchBuilder implements ISearchBuilder { private void addPredicateToken(String theResourceName, String theParamName, List theList) { - Join join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); - if (theList.get(0).getMissing() != null) { + Join join = createOrReuseJoin(JoinEnum.TOKEN, theParamName); addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); return; } List codePredicates = new ArrayList<>(); + Join 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); } @@ -973,38 +996,34 @@ public class SearchBuilder implements ISearchBuilder { @SuppressWarnings("unchecked") private Join createOrReuseJoin(JoinEnum theType, String theSearchParameterName) { - Join 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) join; + return (Join) myIndexJoins.computeIfAbsent(key, k -> { + Join 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 theFrom) { 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..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 { @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 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 { 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 { } } - 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 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 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 { @@ -384,7 +385,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()); @@ -472,10 +473,6 @@ public class TransactionProcessor { 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 { } } - 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 { 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 { 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 { 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/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/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/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/IResourceModifiedConsumer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java new file mode 100644 index 00000000000..20c31732dbd --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/IResourceModifiedConsumer.java @@ -0,0 +1,27 @@ +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 { + void submitResourceModified(ResourceModifiedMessage theMsg); +} 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..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 @@ -26,17 +26,20 @@ 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.search.warm.CacheWarmingSvcImpl; +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.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; @@ -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; @@ -92,6 +96,8 @@ public class SubscriptionActivatingInterceptor extends ServerOperationIntercepto 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 @@ -156,10 +162,10 @@ 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,56 +186,71 @@ public class SubscriptionActivatingInterceptor extends ServerOperationIntercepto submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE); } - @Override - public void resourceCreated(RequestDetails theRequest, IBaseResource theResource) { - submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE); + @Hook(Pointcut.OP_PRESTORAGE_RESOURCE_CREATED) + public void addStrategyTagCreated(IBaseResource theResource) { + if (isSubscription(theResource)) { + validateCriteriaAndAddStrategy(theResource); + } } - @Override - public void resourceDeleted(RequestDetails theRequest, IBaseResource theResource) { - submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); + @Hook(Pointcut.OP_PRESTORAGE_RESOURCE_UPDATED) + public void addStrategyTagUpdated(IBaseResource theOldResource, IBaseResource theNewResource) { + if (isSubscription(theNewResource)) { + validateCriteriaAndAddStrategy(theNewResource); + } } - @Override - public void resourceUpdated(RequestDetails theRequest, IBaseResource theOldResource, IBaseResource 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); + } + + @Hook(Pointcut.OP_PRECOMMIT_RESOURCE_DELETED) + public void resourceDeleted(IBaseResource theResource) { + submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE); + } + private void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType) { - submitResourceModified(new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType)); + if (isSubscription(theNewResource)) { + 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/SubscriptionInterceptorLoader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionInterceptorLoader.java index d3b68502b9a..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; @@ -37,52 +38,44 @@ import java.util.Set; public class SubscriptionInterceptorLoader { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionInterceptorLoader.class); + @Autowired private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + @Autowired private SubscriptionActivatingInterceptor mySubscriptionActivatingInterceptor; - @Autowired DaoConfig myDaoConfig; @Autowired + private SubscriptionRegistry mySubscriptionRegistry; + @Autowired private ApplicationContext myAppicationContext; @Autowired - private SubscriptionRegistry mySubscriptionRegistry; + private IInterceptorRegistry myInterceptorRegistry; public void registerInterceptors() { Set supportedSubscriptionTypes = myDaoConfig.getSupportedSubscriptionTypes(); 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"); - - if (mySubscriptionMatcherInterceptor == null) { - mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class); - } - - myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor); - + myInterceptorRegistry.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()); - - // Once subscriptions have been loaded, now - if (mySubscriptionActivatingInterceptor == null) { - mySubscriptionActivatingInterceptor = myAppicationContext.getBean(SubscriptionActivatingInterceptor.class); - } } @VisibleForTesting - public void unregisterInterceptorsForUnitTest() { - myDaoConfig.unregisterInterceptor(mySubscriptionActivatingInterceptor); - myDaoConfig.unregisterInterceptor(mySubscriptionMatcherInterceptor); + void unregisterInterceptorsForUnitTest() { + 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 988814d7349..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,14 +1,16 @@ 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,7 +19,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; /*- @@ -42,9 +43,11 @@ import javax.annotation.PreDestroy; @Component @Lazy -public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAdapter { +@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"; static final String SUBSCRIPTION_STATUS = "Subscription.status"; static final String SUBSCRIPTION_TYPE = "Subscription.channel.type"; private SubscribableChannel myProcessingChannel; @@ -63,32 +66,36 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd super(); } - @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") @PreDestroy public void preDestroy() { - myProcessingChannel.unsubscribe(mySubscriptionMatchingSubscriber); + + if (myProcessingChannel != null) { + myProcessingChannel.unsubscribe(mySubscriptionMatchingSubscriber); + } } - @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); } @@ -97,8 +104,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)); } @@ -109,12 +117,13 @@ public class SubscriptionMatcherInterceptor extends ServerOperationInterceptorAd /** * This is an internal API - Use with caution! */ + @Override public void submitResourceModified(final ResourceModifiedMessage theMsg) { sendToProcessingChannel(theMsg); } @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..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; @@ -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; @@ -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 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); @@ -210,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(); @@ -290,7 +289,7 @@ public class SubscriptionTriggeringSvcImpl implements ISubscriptionTriggeringSvc private Future submitResource(String theSubscriptionId, String theResourceIdToTrigger) { org.hl7.fhir.r4.model.IdType resourceId = new org.hl7.fhir.r4.model.IdType(theResourceIdToTrigger); - IFhirResourceDao 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 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 getRemainingResourceIds() { + List getRemainingResourceIds() { return myRemainingResourceIds; } - public void setRemainingResourceIds(List theRemainingResourceIds) { + void setRemainingResourceIds(List theRemainingResourceIds) { myRemainingResourceIds = theRemainingResourceIds; } - public List getRemainingSearchUrls() { + List getRemainingSearchUrls() { return myRemainingSearchUrls; } - public void setRemainingSearchUrls(List theRemainingSearchUrls) { + void setRemainingSearchUrls(List 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/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/CompositeInMemoryDaoSubscriptionMatcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/dbmatcher/CompositeInMemoryDaoSubscriptionMatcher.java index eec2462fd2c..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 @@ -21,10 +21,11 @@ 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; 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; @@ -43,16 +44,19 @@ 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); - if (!result.supported()) { - ourLog.info("Criteria {} not supported by InMemoryMatcher: {}. Reverting to DatabaseMatcher", criteria, result.getUnsupportedReason()); - result = myDaoSubscriptionMatcher.match(criteria, msg); + result = myInMemorySubscriptionMatcher.match(theSubscription, theMsg); + 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); } } 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 eca40b9b2b7..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,14 +24,13 @@ 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; 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; @@ -49,10 +48,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; @@ -61,23 +61,20 @@ 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); - 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/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..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 @@ -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 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/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/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 LAST_N_QUERIES = new LinkedList<>(); + + @Override + public void execute(ExecutionInfo execInfo, List queryInfoList) { + synchronized (LAST_N_QUERIES) { + for (QueryInfo next : queryInfoList) { + String sql = next.getQuery(); + List params; + if (next.getParametersList().size() > 0 && next.getParametersList().get(0).size() > 0) { + List 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 myParams; + + Query(String theSql, List 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 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 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 eb040fcba84..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,7 +1,10 @@ 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; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.ISearchCoordinatorSvc; @@ -51,7 +54,9 @@ 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; import static ca.uhn.fhir.util.TestUtil.randomizeLocale; @@ -84,10 +89,13 @@ public abstract class BaseJpaTest { protected IRequestOperationCallback myRequestOperationCallback = mock(IRequestOperationCallback.class); @Autowired protected DatabaseBackedPagingProvider myDatabaseBackedPagingProvider; + @Autowired + protected IInterceptorRegistry myInterceptorRegistry; @After public void afterPerformCleanup() { BaseHapiFhirResourceDao.setDisableIncrementOnUpdateForUnitTest(false); + CaptureQueriesListener.clear(); } @After @@ -128,6 +136,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(); @@ -431,6 +445,20 @@ public abstract class BaseJpaTest { } } + public static void waitForTrue(Supplier 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 theCallable) throws Exception { waitForSize(theTarget, 10000, theCallable); } 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 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/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java index 7bfd40cfb5c..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 @@ -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; @@ -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,11 +852,12 @@ 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; 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/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/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 5ebda85d4df..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 @@ -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(); @@ -1020,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; @@ -1196,10 +1199,9 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; - + TestUtil.sleepOneClick(); long start = System.currentTimeMillis(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); IIdType id1a; { @@ -1218,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()); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + TestUtil.sleepOneClick(); long end = System.currentTimeMillis(); SearchParameterMap map; @@ -1615,18 +1617,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 }); @@ -1853,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; @@ -2871,6 +2873,8 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenDate = new Date(); IIdType tag2id; @@ -3193,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(); @@ -3203,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(); @@ -3213,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/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/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 params = mySearchParamRegsitry.getActiveUniqueSearchParams("Patient"); + List 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/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/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index e020fb031d4..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,13 +8,14 @@ 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; 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; @@ -150,6 +151,8 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { protected IFhirResourceDao myRiskAssessmentDao; protected IServerInterceptor myInterceptor; @Autowired + protected IInterceptorRegistry myInterceptorRegistry; + @Autowired @Qualifier("myLocationDaoR4") protected IFhirResourceDao myLocationDao; @Autowired @@ -226,7 +229,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest { @Qualifier("mySearchParameterDaoR4") protected IFhirResourceDao mySearchParameterDao; @Autowired - protected ISearchParamRegistry mySearchParamRegsitry; + protected BaseSearchParamRegistry mySearchParamRegistry; @Autowired protected IStaleSearchDeletingSvc myStaleSearchDeletingSvc; @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 @@ -326,7 +331,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/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java index f49b54e57bf..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 @@ -1,9 +1,11 @@ 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; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -12,7 +14,7 @@ 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 com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -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.*; @@ -53,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 @@ -151,16 +155,16 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { List 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 +172,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 +220,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 +228,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 @@ -614,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)); } }); } @@ -889,7 +893,11 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { patient.addIdentifier().setSystem("urn:system").setValue("001"); id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + + TestUtil.sleepOneClick(); + long betweenTime = System.currentTimeMillis(); + IIdType id2; { Patient patient = new Patient(); @@ -1056,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())); } } @@ -1088,7 +1096,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { CompositeParam 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"); @@ -1136,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 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 */ @@ -1264,8 +1306,12 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { id1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenTime = new Date(); + TestUtil.sleepOneClick(); + IIdType id2; { Patient patient = new Patient(); @@ -1439,10 +1485,11 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; + TestUtil.sleepOneClick(); long start = System.currentTimeMillis(); - Thread.sleep(sleep); + + TestUtil.sleepOneClick(); IIdType id1a; { @@ -1450,6 +1497,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(); @@ -1461,7 +1511,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 +1538,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)); } @@ -1857,15 +1908,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); @@ -1988,15 +2039,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; @@ -2146,6 +2198,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 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(); @@ -2966,6 +3063,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { tag1id = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); } + TestUtil.sleepOneClick(); + Date betweenDate = new Date(); IIdType tag2id; @@ -3298,7 +3397,7 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test { "Observation/YES21", "Observation/YES22", "Observation/YES23" - )); + )); } private void createObservationWithEffective(String theId, String theEffective) { 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..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 @@ -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 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,11 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { id0 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); } - int sleep = 100; + TestUtil.sleepOneClick(); long start = System.currentTimeMillis(); - ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(sleep); + + 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(); @@ -1463,11 +1469,13 @@ 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; 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); @@ -1489,7 +1497,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 +1868,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); @@ -1990,15 +1998,16 @@ 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(); - 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 +2954,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/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/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 params = mySearchParamRegsitry.getActiveUniqueSearchParams("Patient"); + List 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/FhirResourceDaoR4UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UpdateTest.java index aa6008ccc73..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 @@ -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.*; @@ -313,12 +313,15 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test { assertEquals("1", outcome.getId().getVersionIdPart()); + TestUtil.sleepOneClick(); + Date now = new Date(); + 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"); @@ -335,17 +338,18 @@ 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); 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/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 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/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index ae52fbdfcd3..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 @@ -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 + 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); - 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,8 +227,16 @@ 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 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(); } } 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 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/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-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..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); @@ -204,7 +205,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/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/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 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/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/provider/r4/ResourceProviderR4CacheTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4CacheTest.java index 1792f9fe300..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 @@ -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; @@ -160,12 +160,18 @@ public class ResourceProviderR4CacheTest extends BaseResourceProviderR4Test { 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(results1.getMeta().getLastUpdated(), greaterThan(beforeFirst)); - assertThat(results1.getMeta().getLastUpdated(), 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-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..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 @@ -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,13 +117,13 @@ 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") 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(); @@ -198,6 +199,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() { @@ -945,6 +974,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"; @@ -1714,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)); @@ -1752,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'))); } } @@ -3646,6 +3688,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-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..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 @@ -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 + 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); try { @@ -298,6 +301,16 @@ public class SystemProviderR4Test extends BaseJpaR4Test { } } + 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 public void testSuggestKeywordsInvalid() throws Exception { Patient patient = new Patient(); 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 49a7ea6b1e9..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 @@ -89,16 +89,22 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test ourHeaders.clear(); // Delete all Subscriptions - Bundle allSubscriptions = ourClient.search().forResource(Subscription.class).returnBundle(Bundle.class).execute(); - for (IBaseResource next : BundleUtil.toListOfResources(myFhirCtx, allSubscriptions)) { - ourClient.delete().resource(next).execute(); + if (ourClient != null) { + Bundle allSubscriptions = ourClient.search().forResource(Subscription.class).returnBundle(Bundle.class).execute(); + for (IBaseResource next : BundleUtil.toListOfResources(myFhirCtx, allSubscriptions)) { + ourClient.delete().resource(next).execute(); + } + waitForActivatedSubscriptionCount(0); } - waitForActivatedSubscriptionCount(0); LinkedBlockingQueueSubscribableChannel processingChannel = mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest(); - processingChannel.clearInterceptorsForUnitTest(); + if (processingChannel != null) { + processingChannel.clearInterceptorsForUnitTest(); + } myCountingInterceptor = new CountingInterceptor(); - processingChannel.addInterceptorForUnitTest(myCountingInterceptor); + if (processingChannel != null) { + processingChannel.addInterceptorForUnitTest(myCountingInterceptor); + } } @@ -147,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/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/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 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 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/module/matcher/InMemorySubscriptionMatcherTestR4.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR4.java index 65d3daa91c1..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 @@ -4,9 +4,9 @@ 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; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.apache.commons.lang3.StringUtils; @@ -35,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))); } /* @@ -92,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); } @@ -129,7 +141,7 @@ public class InMemorySubscriptionMatcherTestR4 { TokenParam v0 = new TokenParam("foo", "testSearchCompositeParamN01"); StringParam v1 = new StringParam("testSearchCompositeParamS01"); - CompositeParam val = new CompositeParam(v0, v1); + CompositeParam val = new CompositeParam<>(v0, v1); SearchParameterMap params = new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE_VALUE_STRING, val); assertUnsupported(o1, params); } @@ -169,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 @@ -189,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(); @@ -293,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); @@ -380,13 +392,16 @@ 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()); + } catch (AssertionError e){ + assertEquals("Reference at managingOrganization is invalid: urn:uuid:13720262-b392-465f-913e-54fb198ff954", e.getMessage()); } } @@ -410,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"); @@ -565,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 patients; SearchParameterMap params; params = new SearchParameterMap(); @@ -636,7 +649,6 @@ public class InMemorySubscriptionMatcherTestR4 { o2.setValue(q2); SearchParameterMap map; - IBundleProvider found; QuantityParam param; map = new SearchParameterMap(); @@ -678,9 +690,7 @@ public class InMemorySubscriptionMatcherTestR4 { Patient pt1 = new Patient(); pt1.addName().setFamily("ABCDEFGHIJK"); - List ids; SearchParameterMap map; - IBundleProvider results; // Contains = true map = new SearchParameterMap(); @@ -871,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/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/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/RestHookTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestDstu3Test.java index 97716aac54f..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 @@ -4,7 +4,10 @@ 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.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; @@ -49,6 +52,8 @@ public class RestHookTestDstu3Test extends BaseResourceProviderDstu3Test { @Autowired private SubscriptionTestUtil mySubscriptionTestUtil; + private static NotificationServlet ourNotificationServlet; + private static String ourNotificationListenerServer; @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 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,15 +102,17 @@ 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(); - subscription.setId(methodOutcome.getId().getIdPart()); mySubscriptionIds.add(methodOutcome.getId()); waitForQueueToDrain(); - return subscription; + return (Subscription)methodOutcome.getResource(); } private Observation sendObservation(String code, String system) { @@ -120,6 +133,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"; @@ -291,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()); } @@ -353,16 +415,70 @@ 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 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 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(); 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 +486,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-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..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"; @@ -206,7 +240,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 +908,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-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 new file mode 100644 index 00000000000..441802a9f4a --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookWithInterceptorR4Test.java @@ -0,0 +1,186 @@ +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; +import ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.MethodOutcome; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +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.*; + +/** + * Test the rest-hook subscriptions + */ +@ContextConfiguration(classes = {RestHookWithInterceptorR4Test.MyTestCtxConfig.class}) +public class RestHookWithInterceptorR4Test extends BaseSubscriptionsR4Test { + + private static final Logger ourLog = LoggerFactory.getLogger(RestHookWithInterceptorR4Test.class); + private static boolean ourNextModifyResourceId; + private static boolean ourNextBeforeRestHookDeliveryReturn; + private static boolean ourHitBeforeRestHookDelivery; + private static boolean ourNextAfterRestHookDeliveryReturn; + private static boolean ourHitAfterRestHookDelivery; + private static boolean ourNextAddHeader; + private static FhirContext ourCtx = FhirContext.forR4(); + + @Autowired + StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; + + @After + public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() { + myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null); + myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); + } + + @Override + @Before + public void before() throws Exception { + super.before(); + ourNextModifyResourceId = false; + ourNextAddHeader = false; + ourNextBeforeRestHookDeliveryReturn = true; + ourNextAfterRestHookDeliveryReturn = true; + ourHitBeforeRestHookDelivery = false; + ourHitAfterRestHookDelivery = false; + } + + @Test + 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"); + 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); + + assertEquals(0, ourCreatedObservations.size()); + assertEquals(1, ourUpdatedObservations.size()); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + assertEquals("Observation/A", ourUpdatedObservations.get(0).getId()); + 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"); + 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); + + assertEquals(0, ourCreatedObservations.size()); + assertEquals(1, ourUpdatedObservations.size()); + assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); + assertTrue(ourHitBeforeRestHookDelivery); + assertTrue(ourHitAfterRestHookDelivery); + assertThat(ourHeaders, hasItem("X-Foo: Bar")); + } + + + @Test + 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"); + registerLatch.await(10, TimeUnit.SECONDS); + + sendObservation(); + + Thread.sleep(1000); + assertEquals(0, ourUpdatedObservations.size()); + } + + protected Observation sendObservation() { + Observation observation = new Observation(); + observation.setStatus(Observation.ObservationStatus.FINAL); + MethodOutcome methodOutcome = ourClient.create().resource(observation).execute(); + observation.setId(methodOutcome.getId()); + return observation; + } + + @Configuration + static class MyTestCtxConfig { + + @Autowired + private IInterceptorRegistry myInterceptorRegistry; + + @Bean + public MyTestInterceptor interceptor() { + MyTestInterceptor retVal = new MyTestInterceptor(); + myInterceptorRegistry.registerInterceptor(retVal); + return retVal; + } + + } + + /** + * Interceptor class + */ + @Interceptor + public static class MyTestInterceptor { + + /** + * Constructor + */ + public MyTestInterceptor() { + ourLog.info("Creating interceptor"); + } + + @Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY) + public boolean beforeRestHookDelivery(ResourceDeliveryMessage theDeliveryMessage, CanonicalSubscription theSubscription) { + if (ourNextModifyResourceId) { + theDeliveryMessage.getPayload(ourCtx).setId(new IdType("Observation/A")); + } + if (ourNextAddHeader) { + theSubscription.addHeader("X-Foo: Bar"); + } + + ourHitBeforeRestHookDelivery = true; + return ourNextBeforeRestHookDeliveryReturn; + } + + @Hook(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY) + public boolean afterRestHookDelivery(ResourceDeliveryMessage theDeliveryMessage, CanonicalSubscription theSubscription) { + ourHitAfterRestHookDelivery = true; + return ourNextAfterRestHookDeliveryReturn; + } + + } + + +} 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/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 { } if (isNotBlank(sql)) { - if (JdbcUtils.getSequenceNames(getConnectionProperties()).contains(myGeneratorName)) { + Set 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 { private static final Logger ourLog = LoggerFactory.getLogger(AddTableRawSqlTask.class); private Map> myDriverToSqls = new HashMap<>(); + private List myDriverNeutralSqls = new ArrayList<>(); public void addSql(DriverTypeEnum theDriverType, @Language("SQL") String theSql) { Validate.notNull(theDriverType); @@ -52,9 +53,11 @@ public class AddTableRawSqlTask extends BaseTableTask { return; } - List sqlStatements = myDriverToSqls.get(getDriverType()); + List 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 { }); } + + 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 { 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 { + + 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> 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()); + + } + } diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 1e63f2c8f15..74e84bfa687 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -72,12 +72,53 @@ org.hibernate hibernate-search-orm
+ + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + xml-apis + xml-apis + + + + + com.fasterxml.jackson.core + jackson-annotations + + + org.jscience + jscience + + + + org.apache.commons + commons-collections4 + + + + + javax.annotation + javax.annotation-api + + ch.qos.logback logback-classic test + + org.springframework + spring-test + test + 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 { *
  • "http://hl7.org/fhir/StructureDefinition/*"
  • * */ - public static final Set DEFAULT_LOGICAL_BASE_URLS = Collections.unmodifiableSet(new HashSet(Arrays.asList( + public static final Set 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 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 treatBaseUrlsAsLocal = new HashSet(); + HashSet treatBaseUrlsAsLocal = new HashSet<>(); for (String next : ObjectUtils.defaultIfNull(theTreatBaseUrlsAsLocal, new HashSet())) { while (next.endsWith("/")) { next = next.substring(0, next.length() - 1); @@ -320,6 +321,27 @@ public class ModelConfig { return Collections.unmodifiableSet(mySupportedSubscriptionTypes); } + /** + * If set to true (default is true) the server will match incoming resources against active subscriptions + * and send them to the subscription channel. If set to false no matching or sending occurs. + * @since 3.7.0 + */ + + public boolean isSubscriptionMatchingEnabled() { + return mySubscriptionMatchingEnabled; + } + + /** + * If set to true (default is true) the server will match incoming resources against active subscriptions + * and send them to the subscription channel. If set to false 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-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 new file mode 100644 index 00000000000..ea89c412852 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Hook.java @@ -0,0 +1,48 @@ +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; +import java.lang.annotation.Target; + +/** + * This annotation should be placed on + * {@link Interceptor Subscription Interceptor} + * bean methods. + *

    + * Methods with this annotation are invoked immediately before a REST HOOK + * subscription delivery + *

    + * + * @see Interceptor + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Hook { + + /** + * Provides the specific point where this method should be invoked + */ + Pointcut[] value(); + +} 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..11ec5e61beb --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/HookParams.java @@ -0,0 +1,82 @@ +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; +import com.google.common.collect.Multimaps; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class HookParams { + + private ListMultimap, Object> myParams = ArrayListMultimap.create(); + + /** + * Constructor + */ + public HookParams() { + } + + /** + * Constructor + */ + public HookParams(Object... theParams) { + for (Object next : theParams) { + add(next); + } + } + + @SuppressWarnings("unchecked") + private void add(T theNext) { + Class nextClass = (Class) theNext.getClass(); + add(nextClass, theNext); + } + + public HookParams add(Class theType, T theParam) { + myParams.put(theType, theParam); + return this; + } + + @SuppressWarnings("unchecked") + public T get(Class theParamType, int theIndex) { + List objects = (List) myParams.get(theParamType); + T retVal = null; + if (objects.size() > theIndex) { + retVal = objects.get(theIndex); + } + return retVal; + } + + /** + * Returns an unmodifiable multimap of the params, where the + * key is the param type and the value is the actual instance + */ + public ListMultimap, Object> getParamsForType() { + return Multimaps.unmodifiableListMultimap(myParams); + } + + public Collection values() { + return Collections.unmodifiableCollection(myParams.values()); + } +} 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..5ac153fe0ab --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IAnonymousLambdaHook.java @@ -0,0 +1,36 @@ +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; + +/** + * 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/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 new file mode 100644 index 00000000000..f302c316d39 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/IInterceptorRegistry.java @@ -0,0 +1,65 @@ +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 { + + 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 true 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); + + @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/Interceptor.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java new file mode 100644 index 00000000000..27015c25eae --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Interceptor.java @@ -0,0 +1,41 @@ +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; +import java.lang.annotation.Target; + +/** + * This annotation declares a bean as a subscription interceptor + */ +@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 new file mode 100644 index 00000000000..22b92d3d390 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java @@ -0,0 +1,179 @@ +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; + +/** + * Value for {@link Hook#value()} + */ +public enum Pointcut { + + /** + * Invoked immediately after the delivery of a REST HOOK subscription. + *

    + * When this hook is called, all processing is complete so this hook should not + * make any changes to the parameters. + *

    + * Hooks may accept the following parameters: + *
      + *
    • ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription
    • + *
    • ca.uhn.fhir.jpa.subscription.module.subscriber.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. + *

    + * Hooks may make changes to the delivery payload, or make changes to the + * canonical subscription such as adding headers, modifying the channel + * endpoint, etc. + *

    + * Hooks may accept the following parameters: + *
      + *
    • ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription
    • + *
    • ca.uhn.fhir.jpa.subscription.module.subscriber.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 + * 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: + *
      + *
    • ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage
    • + *
    + */ + SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED("ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage"), + + + /** + * Invoked immediately after an active subscription is "registered". In HAPI FHIR, when + * a subscription + *

    + * 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. + *

    + * Hooks may accept the following parameters: + *
      + *
    • ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription
    • + *
    + */ + SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED("ca.uhn.fhir.jpa.subscription.module.CanonicalSubscription"), + + /** + * Invoked before a resource will be created, immediately before the resource + * is persisted to the database. + *

    + * 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. + *

    + * Hooks may accept the following parameters: + *
      + *
    • org.hl7.fhir.instance.model.api.IBaseResource
    • + *
    + */ + 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. + *

    + * 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. + *

    + * Hooks may accept the following parameters: + *
      + *
    • org.hl7.fhir.instance.model.api.IBaseResource
    • + *
    + */ + OP_PRECOMMIT_RESOURCE_CREATED("org.hl7.fhir.instance.model.api.IBaseResource"), + + /** + * Invoked before a resource will be created + *

    + * Hooks will have access to the contents of the resource being deleted + * but should not make any changes as storage has already occurred + *

    + * Hooks may accept the following parameters: + *
      + *
    • org.hl7.fhir.instance.model.api.IBaseResource
    • + *
    + */ + 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. + *

    + * 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. + *

    + * Hooks may accept the following parameters: + *
      + *
    • org.hl7.fhir.instance.model.api.IBaseResource (previous contents)
    • + *
    • org.hl7.fhir.instance.model.api.IBaseResource (new contents)
    • + *
    + */ + 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. + *

    + * 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. + *

    + * Hooks may accept the following parameters: + *
      + *
    • org.hl7.fhir.instance.model.api.IBaseResource (previous contents)
    • + *
    • org.hl7.fhir.instance.model.api.IBaseResource (new contents)
    • + *
    + */ + OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"), + + ; + + private final List myParameterTypes; + + Pointcut(String... theParameterTypes) { + myParameterTypes = Collections.unmodifiableList(Arrays.asList(theParameterTypes)); + } + + public List getParameterTypes() { + return myParameterTypes; + } + } 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 new file mode 100644 index 00000000000..26e2831d361 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java @@ -0,0 +1,372 @@ +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 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 org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +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 InterceptorService implements IInterceptorRegistry, IInterceptorBroadcaster { + private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class); + private final List myInterceptors = new ArrayList<>(); + private final ListMultimap myInvokers = ArrayListMultimap.create(); + private final ListMultimap myAnonymousInvokers = ArrayListMultimap.create(); + private final Object myRegistryMutex = new Object(); + + /** + * Constructor + */ + public InterceptorService() { + super(); + } + + @VisibleForTesting + List getGlobalInterceptorsForUnitTest() { + return myInterceptors; + } + + + @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, theOrder)); + } + + @Override + @VisibleForTesting + public void clearAnonymousHookForUnitTest() { + myAnonymousInvokers.clear(); + } + + @Override + public boolean registerInterceptor(Object theInterceptor) { + synchronized (myRegistryMutex) { + + 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 nextInvokerList = myInvokers.get(nextPointcut); + nextInvokerList.sort(Comparator.naturalOrder()); + } + + return true; + } + } + + private boolean scanInterceptorForHookMethodsAndAddThem(Object theInterceptor, int theTypeOrder) { + boolean retVal = false; + for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) { + Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class); + + if (hook != null) { + + int methodOrder = theTypeOrder; + Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class); + if (methodOrderAnnotation != null) { + methodOrder = methodOrderAnnotation.value(); + } + + HookInvoker invoker = new HookInvoker(hook, theInterceptor, nextMethod, methodOrder); + for (Pointcut nextPointcut : hook.value()) { + myInvokers.put(nextPointcut, invoker); + } + + retVal = true; + } + } + 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 theObjects) { + IdentityHashMap interceptorToOrder = new IdentityHashMap<>(); + for (Object next : theObjects) { + Order orderAnnotation = next.getClass().getAnnotation(Order.class); + int order = orderAnnotation != null ? orderAnnotation.value() : 0; + interceptorToOrder.put(next, order); + } + + theObjects.sort((a, b) -> { + Integer orderA = interceptorToOrder.get(a); + Integer orderB = interceptorToOrder.get(b); + return orderA - orderB; + }); + } + + @Override + 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 invokers = getInvokersForPointcut(thePointcut); + + /* + * Call each hook in order + */ + for (BaseInvoker nextInvoker : invokers) { + boolean shouldContinue = nextInvoker.invoke(theParams); + if (!shouldContinue) { + return false; + } + } + + return true; + } + + @VisibleForTesting + List 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 getInvokersForPointcut(Pointcut thePointcut) { + List invokers; + boolean haveAnonymousInvokers; + synchronized (myRegistryMutex) { + List globalInvokers = myInvokers.get(thePointcut); + List 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 + */ + 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())); + + List wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); + + ListMultimap, 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 { + + private final int myOrder; + private final Object myInterceptor; + + BaseInvoker(Object theInterceptor, int theOrder) { + myInterceptor = theInterceptor; + myOrder = theOrder; + } + + public Object getInterceptor() { + return myInterceptor; + } + + 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, int theOrder) { + super(theHook, theOrder); + myHook = theHook; + } + + @Override + boolean invoke(HookParams theParams) { + myHook.invoke(theParams); + return true; + } + } + + private class HookInvoker extends BaseInvoker { + + private final boolean myReturnsBoolean; + private final Method myMethod; + private final Class[] myParameterTypes; + private final int[] myParameterIndexes; + + /** + * Constructor + */ + private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { + super(theInterceptor, theOrder); + myParameterTypes = theHookMethod.getParameterTypes(); + myMethod = theHookMethod; + + Class returnType = theHookMethod.getReturnType(); + if (returnType.equals(boolean.class)) { + myReturnsBoolean = true; + } else { + Validate.isTrue(void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); + myReturnsBoolean = false; + } + + myParameterIndexes = new int[myParameterTypes.length]; + Map, 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]; + int nextParamIndex = myParameterIndexes[i]; + Object nextParamValue = theParams.get(nextParamType, nextParamIndex); + args[i] = nextParamValue; + } + + // Invoke the method + try { + 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) { + throw new InternalErrorException(e); + } + + } + + } + + private static String toErrorString(List 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 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 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 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 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 @@ + + + + INFO + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n + + + + + + + + diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 51ddbfd0657..c384c0f967e 100644 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -87,36 +87,6 @@ javax.annotation-api - - - org.springframework - spring-beans - - - org.springframework - spring-context - - - xml-apis - xml-apis - - - - - com.fasterxml.jackson.core - jackson-annotations - - - org.jscience - jscience - - - - - javax.annotation - javax.annotation-api - - 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..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 @@ -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 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 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 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/registry/BaseSearchParamRegistry.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/BaseSearchParamRegistry.java index 33ab46ec183..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 @@ -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 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 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> myBuiltInSearchParams; private volatile Map> myActiveUniqueSearchParams = Collections.emptyMap(); private volatile Map, List>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap(); private volatile Map> 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 params = getActiveSearchParams().get(theResourceName); + Map params = myActiveSearchParams.get(theResourceName); if (params != null) { retVal = params.get(theParamName); } return retVal; } - - @Override - public Map> getActiveSearchParams() { - return myActiveSearchParams; - } - @Override public Map getActiveSearchParams(String theResourceName) { + requiresActiveSearchParams(); return myActiveSearchParams.get(theResourceName); } + void requiresActiveSearchParams() { + if (myActiveSearchParams == null) { + refreshCacheWithRetry(); + } + } + @Override public List getActiveUniqueSearchParams(String theResourceName) { List retVal = myActiveUniqueSearchParams.get(theResourceName); @@ -223,21 +221,10 @@ public abstract class BaseSearchParamRegistry 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 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 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 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> 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); - void refreshCache(BaseSearchParamRegistry theSPBaseSearchParamRegistry, long theRefreshInterval); + int refreshCache(BaseSearchParamRegistry 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 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 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..4cb987d3127 --- /dev/null +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/retry/Retrier.java @@ -0,0 +1,63 @@ +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; + +import java.util.function.Supplier; + +public class Retrier { + private static final Logger ourLog = LoggerFactory.getLogger(Retrier.class); + + private final Supplier mySupplier; + private final int myMaxRetries; + private final int mySecondsBetweenRetries; + private final String myDescription; + + public Retrier(Supplier 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.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 { + 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 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 supplier = () -> true; + Retrier retrier = new Retrier<>(supplier, 5, 0, "test"); + assertTrue(retrier.runWithRetry()); + } + + @Test + public void succeedBeforeMaxRetries() { + AtomicInteger counter = new AtomicInteger(); + Supplier supplier = () -> { + if (counter.incrementAndGet() < 3) throw new RetryRuntimeException("test"); + return true; + }; + Retrier retrier = new Retrier<>(supplier, 5, 0, "test"); + assertTrue(retrier.runWithRetry()); + assertEquals(3, counter.get()); + } + + @Test + public void failMaxRetries() { + AtomicInteger counter = new AtomicInteger(); + Supplier supplier = () -> { + if (counter.incrementAndGet() < 10) throw new RetryRuntimeException("test"); + return true; + }; + Retrier 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 supplier = () -> { + if (counter.incrementAndGet() < 10) throw new RetryRuntimeException("test"); + return true; + }; + Retrier 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 supplier = () -> { + if (counter.incrementAndGet() < 10) throw new RetryRuntimeException("test"); + return true; + }; + Retrier 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..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 @@ -28,9 +28,9 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.hl7.fhir.instance.model.api.IIdType; 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.*; @@ -38,7 +38,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) -public class CanonicalSubscription implements Serializable { +public class CanonicalSubscription implements Serializable, Cloneable { private static final long serialVersionUID = 1L; @@ -65,6 +65,13 @@ public class CanonicalSubscription implements Serializable { @JsonProperty("extensions") private Map myChannelExtensions; + /** + * Constructor + */ + public CanonicalSubscription() { + super(); + } + /** * For now we're using the R4 TriggerDefinition, but this * may change in the future when things stabilize @@ -105,8 +112,9 @@ public class CanonicalSubscription implements Serializable { myEndpointUrl = theEndpointUrl; } + @Nonnull public List getHeaders() { - return Collections.unmodifiableList(myHeaders); + return myHeaders != null ? Collections.unmodifiableList(myHeaders) : Collections.emptyList(); } public void setHeaders(List> theHeader) { @@ -131,7 +139,7 @@ public class CanonicalSubscription implements Serializable { public void setChannelExtensions(Map theChannelExtensions) { myChannelExtensions = new HashMap<>(); - for (String url: theChannelExtensions.keySet()) { + for (String url : theChannelExtensions.keySet()) { if (isNotBlank(url) && isNotBlank(theChannelExtensions.get(url))) { myChannelExtensions.put(url, theChannelExtensions.get(url)); } @@ -200,6 +208,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 +225,7 @@ public class CanonicalSubscription implements Serializable { .append(myTrigger) .append(myEmailDetails) .append(myRestHookDetails) + .append(myChannelExtensions) .toHashCode(); } @@ -226,14 +236,40 @@ public class CanonicalSubscription implements Serializable { } } + /** + * Adds a header + * + * @param theHeader The header, e.g. "Authorization: Bearer AAAAA" + */ + public void addHeader(String theHeader) { + if (isNotBlank(theHeader)) { + initHeaders(); + myHeaders.add(theHeader); + } + } + + private void initHeaders() { + if (myHeaders == null) { + myHeaders = new ArrayList<>(); + } + } + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public static class EmailDetails { + @JsonProperty("from") private String myFrom; @JsonProperty("subjectTemplate") private String mySubjectTemplate; + /** + * Construcor + */ + public EmailDetails() { + super(); + } + public String getFrom() { return myFrom; } @@ -254,11 +290,19 @@ public class CanonicalSubscription implements Serializable { @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public static class RestHookDetails { + @JsonProperty("stripVersionId") private boolean myStripVersionId; @JsonProperty("deliverLatestVersion") private boolean myDeliverLatestVersion; + /** + * Constructor + */ + public RestHookDetails() { + super(); + } + public boolean isDeliverLatestVersion() { return myDeliverLatestVersion; } @@ -267,6 +311,15 @@ public class CanonicalSubscription implements Serializable { myDeliverLatestVersion = theDeliverLatestVersion; } + + public boolean isStripVersionId() { + return myStripVersionId; + } + + public void setStripVersionId(boolean theStripVersionId) { + myStripVersionId = theStripVersionId; + } + @Override public boolean equals(Object theO) { if (this == theO) return true; @@ -289,23 +342,19 @@ public class CanonicalSubscription implements Serializable { .toHashCode(); } - public boolean isStripVersionId() { - return myStripVersionId; - } - - public void setStripVersionId(boolean theStripVersionId) { - myStripVersionId = theStripVersionId; - } - } @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public static class CanonicalEventDefinition { - public CanonicalEventDefinition(EventDefinition theDef) { + /** + * Constructor + */ + public CanonicalEventDefinition() { // nothing yet } + } } 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..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 @@ -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) @@ -52,11 +56,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) { @@ -108,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 @@ -119,7 +136,6 @@ public class ResourceModifiedMessage implements IResourceMessage { myPayloadId = theNewPayload.getIdElement().toUnqualified().getValue(); } - public enum OperationTypeEnum { CREATE, UPDATE, @@ -128,4 +144,26 @@ public class ResourceModifiedMessage implements IResourceMessage { } + private static boolean payloadContainsNoPlaceholderReferences(FhirContext theCtx, IBaseResource theNewPayload) { + List refs = theCtx.newTerser().getAllResourceReferences(theNewPayload); + for (ResourceReferenceInfo next : refs) { + String ref = next.getResourceReference().getReferenceElement().getValue(); + if (isBlank(ref)) { + IBaseResource resource = next.getResourceReference().getResource(); + if (resource != null) { + ref = resource.getIdElement().getValue(); + } + } + if (isNotBlank(ref)) { + if (ref.startsWith("#")) { + continue; + } + if (ref.startsWith("urn:uuid:")) { + throw new AssertionError("Reference at " + next.getName() + " is invalid: " + ref); + } + } + } + return true; + } + } 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/SubscriptionCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/cache/SubscriptionCanonicalizer.java index d8661af2127..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 @@ -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. @@ -24,11 +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; @@ -63,7 +66,7 @@ public class SubscriptionCanonicalizer { } } - 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 +85,7 @@ public class SubscriptionCanonicalizer { 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(); @@ -96,10 +99,10 @@ public class SubscriptionCanonicalizer { 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,6 +114,7 @@ public class SubscriptionCanonicalizer { } if (retVal.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) { + String stripVersionIds; String deliverLatestVersion; try { @@ -210,7 +214,7 @@ public class SubscriptionCanonicalizer { 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(); @@ -259,4 +263,33 @@ public class SubscriptionCanonicalizer { 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/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 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..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 @@ -20,7 +20,11 @@ package ca.uhn.fhir.jpa.subscription.module.cache; * #L% */ +import ca.uhn.fhir.jpa.model.entity.ModelConfig; +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; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -36,24 +40,26 @@ 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. */ +// 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); - + private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); @Autowired - SubscriptionCanonicalizer mySubscriptionCanonicalizer; + SubscriptionCanonicalizer mySubscriptionCanonicalizer; @Autowired SubscriptionDeliveryHandlerFactory mySubscriptionDeliveryHandlerFactory; @Autowired SubscriptionChannelFactory mySubscriptionDeliveryChannelFactory; - - private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); + @Autowired + ModelConfig myModelConfig; + @Autowired + private IInterceptorBroadcaster myInterceptorBroadcaster; public ActiveSubscription get(String theIdPart) { return myActiveSubscriptionCache.get(theIdPart); @@ -71,20 +77,31 @@ 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 deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(canonicalized); + SubscribableChannel deliveryChannel; + Optional 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(activeSubscription::register); + myActiveSubscriptionCache.put(subscriptionId, activeSubscription); - deliveryHandler.ifPresent(handler -> activeSubscription.register(handler)); + // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED + myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, canonicalized); return canonicalized; } @@ -100,7 +117,7 @@ public class SubscriptionRegistry { unregisterAllSubscriptionsNotInCollection(Collections.emptyList()); } - public void unregisterAllSubscriptionsNotInCollection(Collection theAllIds) { + void unregisterAllSubscriptionsNotInCollection(Collection theAllIds) { myActiveSubscriptionCache.unregisterAllSubscriptionsNotInCollection(theAllIds); } @@ -139,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/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/matcher/CriteriaResourceMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/CriteriaResourceMatcher.java index faad12db014..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 @@ -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,12 @@ 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 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; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -42,78 +47,108 @@ 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) { + /** + * 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, theResourceDefinition); + 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, "Qualifiers not supported"); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(Constants.PARAM_LASTUPDATED, SubscriptionMatchResult.STANDARD_PARAMETER); } for (Map.Entry>> entry : searchParameterMap.entrySet()) { String theParamName = entry.getKey(); List> theAndOrParams = entry.getValue(); - SubscriptionMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, 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> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) { + private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List> 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, "Qualifiers 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); } - if (theParamName.equals(IAnyResource.SP_RES_ID)) { + switch (theParamName) { + case IAnyResource.SP_RES_ID: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return SubscriptionMatchResult.fromBoolean(matchIdsAndOr(theAndOrParams, theResource)); - } else if (theParamName.equals(IAnyResource.SP_RES_LANGUAGE)) { + case IAnyResource.SP_RES_LANGUAGE: + case Constants.PARAM_HAS: + case Constants.PARAM_TAG: + case Constants.PARAM_PROFILE: + case Constants.PARAM_SECURITY: - return new SubscriptionMatchResult(theParamName, CRITERIA); + return SubscriptionMatchResult.unsupportedFromParameterAndReason(theParamName, SubscriptionMatchResult.PARAM); - } else if (theParamName.equals(Constants.PARAM_HAS)) { + default: - return new SubscriptionMatchResult(theParamName, CRITERIA); - - } else if (theParamName.equals(Constants.PARAM_TAG) || theParamName.equals(Constants.PARAM_PROFILE) || theParamName.equals(Constants.PARAM_SECURITY)) { - - return new SubscriptionMatchResult(theParamName, CRITERIA); - - } else { - - String resourceName = theResourceDefinition.getName(); - RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); - return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); + String resourceName = theResourceDefinition.getName(); + RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(resourceName, theParamName); + return matchResourceParam(theParamName, theAndOrParams, theSearchParams, resourceName, paramDef); } } + private boolean matchIdsAndOr(List> theAndOrParams, IBaseResource theResource) { + if (theResource == null) { + return true; + } + return theAndOrParams.stream().allMatch(nextAnd -> matchIdsOr(nextAnd, theResource)); + } + private boolean matchIdsOr(List theOrParams, IBaseResource theResource) { + if (theResource == null) { + return true; + } + 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> theAndOrParams, ResourceIndexedSearchParams theSearchParams, String theResourceName, RuntimeSearchParam theParamDef) { if (theParamDef != null) { switch (theParamDef.getParamType()) { @@ -124,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/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..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 @@ -21,11 +21,11 @@ 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; 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 +45,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); } } @@ -59,8 +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); - RuntimeResourceDefinition resourceDefinition = myContext.getResourceDefinition(resource); - return myCriteriaResourceMatcher.match(criteria, resourceDefinition, searchParams); + 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/matcher/SubscriptionMatchResult.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchResult.java index 074a9df81b6..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 @@ -21,34 +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) { - this.myMatch = false; - this.mySupported = false; - this.myUnsupportedParameter = theUnsupportedParameter; - this.myUnsupportedReason = "Parameter not supported"; - this.myMatcherShortName = theMatcherShortName; - } - - public SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason, String theMatcherShortName) { + private SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) { this.myMatch = false; this.mySupported = false; this.myUnsupportedParameter = theUnsupportedParameter; this.myUnsupportedReason = theUnsupportedReason; - this.myMatcherShortName = theMatcherShortName; + } + + 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() { @@ -60,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..7c273a0d9ad --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionMatchingStrategy.java @@ -0,0 +1,34 @@ +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 + */ + 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..2e5152db1ae --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/module/matcher/SubscriptionStrategyEvaluator.java @@ -0,0 +1,39 @@ +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; + +@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/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 void refreshCache(BaseSearchParamRegistry theSearchParamRegistry, long theRefreshInterval) { - theSearchParamRegistry.doRefresh(theRefreshInterval); + public int refreshCache(BaseSearchParamRegistry theSearchParamRegistry, long theRefreshInterval) { + return theSearchParamRegistry.doRefresh(theRefreshInterval); } } 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 64317e4b2ea..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 @@ -33,12 +33,11 @@ import org.hl7.fhir.instance.model.api.IIdType; import static org.apache.commons.lang3.StringUtils.isNotBlank; +@SuppressWarnings("WeakerAccess") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NONE, fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class ResourceDeliveryMessage implements IResourceMessage { - private static final long serialVersionUID = 1L; - @JsonIgnore private transient CanonicalSubscription mySubscription; @JsonProperty("subscription") @@ -52,10 +51,6 @@ public class ResourceDeliveryMessage implements IResourceMessage { @JsonProperty("operationType") private ResourceModifiedMessage.OperationTypeEnum myOperationType; - public ResourceModifiedMessage.OperationTypeEnum getOperationType() { - return myOperationType; - } - /** * Constructor */ @@ -63,6 +58,10 @@ public class ResourceDeliveryMessage implements IResourceMessage { super(); } + public ResourceModifiedMessage.OperationTypeEnum getOperationType() { + return myOperationType; + } + public void setOperationType(ResourceModifiedMessage.OperationTypeEnum theOperationType) { myOperationType = theOperationType; } @@ -104,14 +103,15 @@ public class ResourceDeliveryMessage implements IResourceMessage { myPayloadId = thePayload.getIdElement().toUnqualified().getValue(); } + @Override + public String getPayloadId() { + return myPayloadId; + } + public void setPayloadId(IIdType thePayloadId) { myPayloadId = null; if (thePayloadId != null) { myPayloadId = thePayloadId.getValue(); } } - @Override - public String getPayloadId() { - return myPayloadId; - } } 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..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,7 +20,10 @@ 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; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.*; @@ -28,6 +31,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; @@ -38,24 +42,24 @@ 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; @Component @Scope("prototype") public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDeliverySubscriber { - private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); - @Autowired IResourceRetriever myResourceRetriever; + private Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class); + @Autowired + private IInterceptorBroadcaster myInterceptorBroadcaster; 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,32 +68,16 @@ 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 ) { + if (thePayloadType != null) { operation = theClient.create().resource(thePayloadResource); } else { sendNotification(theMsg); return; } } else { - if (thePayloadType != null ) { + if (thePayloadType != null) { operation = theClient.update().resource(thePayloadResource); } else { sendNotification(theMsg); @@ -114,8 +102,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; } } @@ -143,54 +131,80 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe @Override public void handleMessage(ResourceDeliveryMessage theMessage) throws MessagingException { - CanonicalSubscription subscription = theMessage.getSubscription(); + CanonicalSubscription subscription = theMessage.getSubscription(); - // Grab the endpoint from the subscription - String endpointUrl = subscription.getEndpointUrl(); + // Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY + if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, theMessage, subscription)) { + return; + } - // Grab the payload type (encoding mimetype) from the subscription - String payloadString = subscription.getPayloadString(); - EncodingEnum payloadType = null; - if(payloadString != null) { - if (payloadString.contains(";")) { - payloadString = payloadString.substring(0, payloadString.indexOf(';')); - } - payloadString = payloadString.trim(); - payloadType = EncodingEnum.forContentType(payloadString); + // Grab the endpoint from the subscription + String endpointUrl = subscription.getEndpointUrl(); + + // Grab the payload type (encoding mimetype) from the subscription + String payloadString = subscription.getPayloadString(); + EncodingEnum payloadType = null; + if (payloadString != null) { + if (payloadString.contains(";")) { + payloadString = payloadString.substring(0, payloadString.indexOf(';')); } + payloadString = payloadString.trim(); + payloadType = EncodingEnum.forContentType(payloadString); + } - // Create the client request - myFhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - IGenericClient client = null; - if (isNotBlank(endpointUrl)) { - client = myFhirContext.newRestfulGenericClient(endpointUrl); + // Create the client request + myFhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + IGenericClient client = null; + if (isNotBlank(endpointUrl)) { + client = myFhirContext.newRestfulGenericClient(endpointUrl); - // Additional headers specified in the subscription - List headers = subscription.getHeaders(); - for (String next : headers) { - if (isNotBlank(next)) { - client.registerInterceptor(new SimpleRequestHeaderInterceptor(next)); - } + // Additional headers specified in the subscription + List headers = subscription.getHeaders(); + for (String next : headers) { + if (isNotBlank(next)) { + client.registerInterceptor(new SimpleRequestHeaderInterceptor(next)); } } + } + + deliverPayload(theMessage, subscription, payloadType, client); + + // Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY + if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, theMessage, subscription)) { + //noinspection UnnecessaryReturnStatement + return; + } - deliverPayload(theMessage, subscription, payloadType, client); } /** * Sends a POST notification without a payload - * @param theMsg */ protected void sendNotification(ResourceDeliveryMessage theMsg) { - Map> params = new HashMap(); + Map> params = new HashMap<>(); List
    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()); + ourLog.error("Error trying to reach " + theMsg.getSubscription().getEndpointUrl()); e.printStackTrace(); throw new ResourceNotFoundException(e.getMessage()); } 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..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 @@ -1,12 +1,15 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; import ca.uhn.fhir.context.FhirContext; +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; import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher; import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult; 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; import org.slf4j.LoggerFactory; @@ -51,6 +54,8 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { private FhirContext myFhirContext; @Autowired private SubscriptionRegistry mySubscriptionRegistry; + @Autowired + private IInterceptorBroadcaster myInterceptorBroadcaster; @Override public void handleMessage(Message theMessage) throws MessagingException { @@ -63,9 +68,18 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload(); matchActiveSubscriptionsAndDeliver(msg); + } public void matchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { + try { + doMatchActiveSubscriptionsAndDeliver(theMsg); + } finally { + myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, theMsg); + } + } + + private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { switch (theMsg.getOperationType()) { case CREATE: case UPDATE: @@ -78,8 +92,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { return; } - IIdType id = theMsg.getId(myFhirContext); - String resourceType = id.getResourceType(); + IIdType resourceId = theMsg.getId(myFhirContext); Collection subscriptions = mySubscriptionRegistry.getAll(); @@ -87,8 +100,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,35 +109,28 @@ 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.debug("Subscription {} was matched by resource {} {}", + nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), + resourceId.toUnqualifiedVersionless().getValue(), + matchResult.isInMemory() ? "in-memory" : "by querying the repository"); - ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), id.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName()); + IBaseResource payload = theMsg.getNewPayload(myFhirContext); ResourceDeliveryMessage deliveryMsg = new ResourceDeliveryMessage(); - deliveryMsg.setPayload(myFhirContext, theMsg.getNewPayload(myFhirContext)); + deliveryMsg.setPayload(myFhirContext, payload); deliveryMsg.setSubscription(nextActiveSubscription.getSubscription()); deliveryMsg.setOperationType(theMsg.getOperationType()); - deliveryMsg.setPayloadId(theMsg.getId(myFhirContext)); + if (payload == null) { + deliveryMsg.setPayloadId(theMsg.getId(myFhirContext)); + } ResourceDeliveryJsonMessage wrappedMsg = new ResourceDeliveryJsonMessage(deliveryMsg); MessageChannel deliveryChannel = nextActiveSubscription.getSubscribableChannel(); @@ -136,4 +141,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 && !criteriaResource.equals(resourceType)) { + ourLog.trace("Skipping subscription search for {} because it does not match the criteria {}", resourceType, criteriaString); + return false; + } + + return true; + } } 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..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,12 +1,12 @@ package ca.uhn.fhir.jpa.subscription.module; -import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; +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.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; 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; @@ -15,25 +15,34 @@ 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; + @Autowired + protected + IInterceptorRegistry myInterceptorRegistry; + + @After + public void afterClearAnonymousLambdas() { + myInterceptorRegistry.clearAnonymousHookForUnitTest(); + } + 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/PointcutLatch.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java new file mode 100644 index 00000000000..6c0a5108203 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/PointcutLatch.java @@ -0,0 +1,151 @@ +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.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +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 CountDownLatch myCountdownLatch; + private AtomicReference myFailure; + private AtomicReference> myCalledWith; + + public PointcutLatch(Pointcut thePointcut) { + this.name = thePointcut.name(); + } + + public PointcutLatch(String theName) { + this.name = theName; + } + + public void setExpectedCount(int count) throws InterruptedException { + if (myCountdownLatch != null) { + throw new PointcutLatchException("setExpectedCount() called before previous awaitExpected() completed."); + } + createLatch(count); + } + + private void createLatch(int count) { + myFailure = new AtomicReference<>(); + myCalledWith = new AtomicReference<>(new ArrayList<>()); + myCountdownLatch = new CountDownLatch(count); + } + + 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); + } + } + + 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; + } + + private String myCalledWithString() { + if (myCalledWith == null) { + return "[]"; + } + List 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 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/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/TestSubscriptionDstu3Config.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/config/TestSubscriptionDstu3Config.java index 883fa7c5233..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 @@ -2,13 +2,11 @@ package ca.uhn.fhir.jpa.subscription.module.config; 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 @@ -18,5 +16,8 @@ public class TestSubscriptionDstu3Config extends SubscriptionDstu3Config { @Bean @Primary - public ISubscriptionProvider subsriptionProvider() { return new MockFhirClientSubscriptionProvider();} + public ISubscriptionProvider subsriptionProvider() { + return new MockFhirClientSubscriptionProvider(); + } + } 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/matcher/InMemorySubscriptionMatcherTestR3.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/matcher/InMemorySubscriptionMatcherTestR3.java index 0e004ee603e..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 @@ -10,17 +10,19 @@ import org.hl7.fhir.instance.model.api.IIdType; 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) { @@ -28,15 +30,67 @@ 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)); } + + /** + * 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() { + + ProcedureRequest pr = new ProcedureRequest(); + pr.setId("ProcedureRequest/123"); + pr.setIntent(ProcedureRequest.ProcedureRequestIntent.ORIGINALORDER); + + assertMatched(pr, "ProcedureRequest?_id=123"); + 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"); + + } + + /* The following tests are copied from an e-mail from a site using HAPI FHIR */ @@ -105,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(); @@ -121,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(); @@ -219,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); } } @@ -281,7 +335,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); { @@ -313,7 +367,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); { @@ -405,7 +459,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/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/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..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 @@ -1,9 +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.IInterceptorRegistry; +import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut; 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; @@ -34,9 +39,11 @@ 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); + protected static ObservationListener ourObservationListener; @Autowired FhirContext myFhirContext; @@ -44,6 +51,13 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base StandaloneSubscriptionMessageHandler myStandaloneSubscriptionMessageHandler; @Autowired SubscriptionChannelFactory mySubscriptionChannelFactory; + @Autowired + IInterceptorRegistry myInterceptorRegistry; + @Autowired + protected SubscriptionRegistry mySubscriptionRegistry; + + + protected String myCode = "1000000050"; private static int ourListenerPort; private static RestfulServer ourListenerRestServer; @@ -54,35 +68,44 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base protected static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); private static SubscribableChannel ourSubscribableChannel; private List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); - private long idCounter = 0; - - @After - public void afterUnregisterRestHookListener() { - mySubscriptionIds.clear(); - } + 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); @Before public void beforeReset() { ourCreatedObservations.clear(); ourUpdatedObservations.clear(); ourContentTypes.clear(); + mySubscriptionRegistry.clearForUnitTests(); if (ourSubscribableChannel == null) { ourSubscribableChannel = mySubscriptionChannelFactory.newDeliveryChannel("test", Subscription.SubscriptionChannelType.RESTHOOK.toCode().toLowerCase()); ourSubscribableChannel.subscribe(myStandaloneSubscriptionMessageHandler); } + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, mySubscriptionMatchingPost); + myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, mySubscriptionActivatedPost); } - public T sendResource(T theResource) { + @After + public void cleanup() { + myInterceptorRegistry.clearAnonymousHookForUnitTest(); + } + + public 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; } 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) { @@ -90,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(); @@ -102,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(); @@ -119,15 +140,14 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base return sendResource(observation); } - @BeforeClass public static void startListenerServer() throws Exception { ourListenerPort = PortUtil.findFreePort(); 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); @@ -149,6 +169,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base public static class ObservationListener implements IResourceProvider { + private PointcutLatch updateLatch = new PointcutLatch("Observation Update"); + @Create public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { ourLog.info("Received Listener Create"); @@ -166,10 +188,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.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) throws InterruptedException { + updateLatch.setExpectedCount(count); + } + public void awaitExpected() throws InterruptedException { + updateLatch.awaitExpected(); + } + + public void expectNothing() { + updateLatch.expectNothing(); + } + } } 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..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,22 +1,27 @@ 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; import org.hl7.fhir.dstu3.model.Subscription; -import org.junit.Before; 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 { - private String myCode = "1000000050"; - @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"; @@ -30,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); @@ -37,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"; @@ -51,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); 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 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()); + } +} 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..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 @@ -2,6 +2,10 @@ package ca.uhn.fhir.jpa.subscription.module.subscriber; 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; @@ -25,10 +29,13 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); - sendObservation(code, "SNOMED-CT"); + assertEquals(2, mySubscriptionRegistry.size()); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); + ourObservationListener.setExpectedCount(1); + sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); + + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); } @@ -43,10 +50,13 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); - sendObservation(code, "SNOMED-CT"); + assertEquals(2, mySubscriptionRegistry.size()); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); + ourObservationListener.setExpectedCount(1); + sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); + + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); } @@ -61,9 +71,49 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); - sendObservation(code, "SNOMED-CT"); + assertEquals(2, mySubscriptionRegistry.size()); - waitForSize(0, ourCreatedObservations); - waitForSize(0, ourUpdatedObservations); + ourObservationListener.setExpectedCount(0); + sendObservation(code, "SNOMED-CT"); + ourObservationListener.expectNothing(); + + 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)); + } + } 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..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,10 +25,13 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); - sendObservation(code, "SNOMED-CT"); + assertEquals(2, mySubscriptionRegistry.size()); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); + ourObservationListener.setExpectedCount(1); + sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); + + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_JSON_NEW, ourContentTypes.get(0)); } @@ -43,10 +46,13 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); - sendObservation(code, "SNOMED-CT"); + assertEquals(2, mySubscriptionRegistry.size()); - waitForSize(0, ourCreatedObservations); - waitForSize(1, ourUpdatedObservations); + ourObservationListener.setExpectedCount(1); + sendObservation(code, "SNOMED-CT"); + ourObservationListener.awaitExpected(); + + assertEquals(1, ourContentTypes.size()); assertEquals(Constants.CT_FHIR_XML_NEW, ourContentTypes.get(0)); } @@ -61,9 +67,12 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri sendSubscription(criteria1, payload, ourListenerServerBase); sendSubscription(criteria2, payload, ourListenerServerBase); - sendObservation(code, "SNOMED-CT"); + assertEquals(2, mySubscriptionRegistry.size()); - waitForSize(0, ourCreatedObservations); - waitForSize(0, ourUpdatedObservations); + ourObservationListener.setExpectedCount(0); + sendObservation(code, "SNOMED-CT"); + ourObservationListener.expectNothing(); + + assertEquals(0, ourContentTypes.size()); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-subscription/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..dc857b12814 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] - %msg%n + + + + + + + + + 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/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 theProviders) { Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); @@ -1359,6 +1363,16 @@ public class RestfulServer extends HttpServlet implements IRestfulServerDocumentation 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..441e0fc4e06 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizedList.java @@ -0,0 +1,104 @@ +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; + +import java.util.ArrayList; +import java.util.List; + +/** + * Return type for {@link SearchNarrowingInterceptor#buildAuthorizedList(RequestDetails)} + */ +public class AuthorizedList { + + private List myAllowedCompartments; + private List myAllowedInstances; + + List getAllowedCompartments() { + return myAllowedCompartments; + } + + List getAllowedInstances() { + return myAllowedInstances; + } + + /** + * 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 (myAllowedCompartments == null) { + myAllowedCompartments = new ArrayList<>(); + } + myAllowedCompartments.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 (myAllowedInstances == null) { + myAllowedInstances = new ArrayList<>(); + } + myAllowedInstances.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/IAuthRuleBuilderRuleOp.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java index e9521ad5860..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 @@ -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 @@ -30,14 +32,14 @@ public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo *
      - *
    • http://example.com/Patient/123 - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)
    • + *
    • http://example.com/Patient/123 - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)
    • *
    • Patient/123 - Any Patient resource with the ID "123" will be matched
    • *
    • 123 - Any resource of any type with the ID "123" will be matched
    • *
    - * + * * @param theId The ID of the resource to apply (e.g. Patient/123) * @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); @@ -47,15 +49,31 @@ public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo *
      - *
    • http://example.com/Patient/123 - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)
    • + *
    • http://example.com/Patient/123 - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)
    • *
    • Patient/123 - Any Patient resource with the ID "123" will be matched
    • *
    • 123 - Any resource of any type with the ID "123" will be matched
    • *
    - * + * * @param theId The ID of the resource to apply (e.g. Patient/123) * @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); + /** + * Rule applies to the resource with the given ID (e.g. Patient/123) + *

    + * See the following examples which show how theId is interpreted: + *

    + *
      + *
    • http://example.com/Patient/123 - Any Patient resource with the ID "123" will be matched (note: the base URL part is ignored)
    • + *
    • Patient/123 - Any Patient resource with the ID "123" will be matched
    • + *
    • 123 - Any resource of any type with the ID "123" will be matched
    • + *
    + * + * @param theIds The IDs of the resource to apply (e.g. Patient/123) + * @throws IllegalArgumentException If theId does not contain an ID with at least an ID part + * @throws NullPointerException If theId is null + */ + IAuthRuleFinished instances(Collection 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 dc8d258392e..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 @@ -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 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,7 +181,8 @@ public class RuleBuilder implements IAuthRuleBuilder { private PolicyEnum myRuleMode; private String myRuleName; - private RuleOpEnum myRuleOp; + private RuleBuilderRuleOp myReadRuleBuilder; + private RuleBuilderRuleOp myWriteRuleBuilder; RuleBuilderRule(PolicyEnum theRuleMode, String theRuleName) { myRuleMode = theRuleMode; @@ -186,8 +196,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOp delete() { - myRuleOp = RuleOpEnum.DELETE; - return new RuleBuilderRuleOp(); + return new RuleBuilderRuleOp(RuleOpEnum.DELETE); } @Override @@ -211,14 +220,15 @@ 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(); + if (myReadRuleBuilder == null) { + myReadRuleBuilder = new RuleBuilderRuleOp(RuleOpEnum.READ); + } + return myReadRuleBuilder; } @Override @@ -233,8 +243,10 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOp write() { - myRuleOp = RuleOpEnum.WRITE; - return new RuleBuilderRuleOp(); + if (myWriteRuleBuilder == null) { + myWriteRuleBuilder = new RuleBuilderRuleOp(RuleOpEnum.WRITE); + } + return myWriteRuleBuilder; } @Override @@ -245,7 +257,6 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleConditional implements IAuthRuleBuilderRuleConditional { private AppliesTypeEnum myAppliesTo; - private Set myAppliesToTypes; private RestOperationTypeEnum myOperationType; @@ -291,13 +302,16 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRuleOp implements IAuthRuleBuilderRuleOp { - private AppliesTypeEnum myAppliesTo; - private Set myAppliesToTypes; + private final RuleOpEnum myRuleOp; + private RuleBuilderRuleOpClassifier myInstancesBuilder; + + 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,53 +326,72 @@ 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 instances = Lists.newArrayList(theId); + return instances(instances); + } + + @Override + public RuleBuilderFinished instances(Collection theInstances) { + Validate.notNull(theInstances, "theInstances must not be null"); + Validate.notEmpty(theInstances, "theInstances must not be empty"); + + if (myInstancesBuilder == null) { + RuleBuilderRuleOpClassifier instancesBuilder = new RuleBuilderRuleOpClassifier(theInstances); + myInstancesBuilder = instancesBuilder; + return instancesBuilder.finished(); + } else { + return myInstancesBuilder.addInstances(theInstances); + } } @Override public IAuthRuleBuilderRuleOpClassifier resourcesOfType(Class 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 { + private final AppliesTypeEnum myAppliesTo; + private final Set myAppliesToTypes; private ClassifierTypeEnum myClassifierType; private String myInCompartmentName; private Collection myInCompartmentOwners; - private List myAppliesToInstances; + private Collection myAppliesToInstances; + private RuleImplOp myRule; /** * Constructor */ - RuleBuilderRuleOpClassifier() { + RuleBuilderRuleOpClassifier(AppliesTypeEnum theAppliesTo, Set> theAppliesToTypes) { super(); + myAppliesTo = theAppliesTo; + myAppliesToTypes = theAppliesToTypes; } /** * Constructor */ - RuleBuilderRuleOpClassifier(List theAppliesToInstances) { + RuleBuilderRuleOpClassifier(Collection theAppliesToInstances) { myAppliesToInstances = theAppliesToInstances; myAppliesTo = AppliesTypeEnum.INSTANCES; + 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 @@ -397,6 +430,10 @@ public class RuleBuilder implements IAuthRuleBuilder { return finished(); } + RuleBuilderFinished addInstances(Collection theInstances) { + myAppliesToInstances.addAll(theInstances); + return new RuleBuilderFinished(myRule); + } } } @@ -416,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) { @@ -524,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); + } + } + } } @@ -554,6 +591,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..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 @@ -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; @@ -20,10 +23,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 +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. @@ -48,6 +48,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ +@SuppressWarnings("EnumSwitchStatementWhichMissesCases") class RuleImplOp extends BaseRule /* implements IAuthRule */ { private AppliesTypeEnum myAppliesTo; @@ -57,7 +58,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { private ClassifierTypeEnum myClassifierType; private RuleOpEnum myOp; private TransactionAppliesToEnum myTransactionAppliesToOp; - private List myAppliesToInstances; + private Collection myAppliesToInstances; /** * Constructor @@ -66,6 +67,15 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { super(theRuleName); } + @VisibleForTesting + Collection getAppliesToInstances() { + return myAppliesToInstances; + } + + public void setAppliesToInstances(Collection theAppliesToInstances) { + myAppliesToInstances = theAppliesToInstances; + } + @Override public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set theFlags) { @@ -77,7 +87,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); IBaseResource appliesToResource; - IIdType appliesToResourceId = null; + Collection appliesToResourceId = null; String appliesToResourceType = null; Map appliesToSearchParams = null; switch (myOp) { @@ -90,7 +100,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 +115,33 @@ 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 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); + } + } + } + } + if (appliesToResourceId.isEmpty()) { + appliesToResourceId = null; + } + } break; case HISTORY_TYPE: if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) { @@ -116,7 +153,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 +182,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 +197,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,22 +330,33 @@ 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(); } } + return null; case ALL_RESOURCES: if (appliesToResourceType != null) { @@ -326,10 +376,14 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { } } } - if (appliesToResourceId != null && appliesToResourceId.hasResourceType()) { - Class 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 type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(nextRequestAppliesToResourceId.getResourceType()).getImplementingClass(); + if (myAppliesToTypes.contains(type) == false) { + return null; + } + } } } if (appliesToResourceType != null) { @@ -356,6 +410,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 +427,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,10 +548,6 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ { myAppliesTo = theAppliesTo; } - public void setAppliesToInstances(List theAppliesToInstances) { - myAppliesToInstances = theAppliesToInstances; - } - public void setAppliesToTypes(Set theAppliesToTypes) { myAppliesToTypes = theAppliesToTypes; } 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..9317cfcdef7 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java @@ -0,0 +1,224 @@ +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; +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 + * @return The list of allowed compartments and instances that should be used + * for search narrowing. If this method returns null, no narrowing will + * be performed + */ + protected AuthorizedList buildAuthorizedList(@SuppressWarnings("unused") RequestDetails theRequestDetails) { + return null; + } + + + @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); + if (authorizedList == null) { + return true; + } + + /* + * Create a map of search parameter values that need to be added to the + * given request + */ + Collection compartments = authorizedList.getAllowedCompartments(); + if (compartments != null) { + processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, compartments, true); + } + Collection resources = authorizedList.getAllowedInstances(); + 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-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-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/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..243d7704d43 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilderTest.java @@ -0,0 +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 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 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-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..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; @@ -112,8 +105,6 @@ public class FhirAutoConfiguration { private final IPagingProvider pagingProvider; - private final List interceptors; - private final List customizers; public FhirRestfulServerConfiguration( @@ -127,7 +118,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 +144,6 @@ public class FhirAutoConfiguration { setFhirContext(this.fhirContext); setResourceProviders(this.resourceProviders); setPagingProvider(this.pagingProvider); - setInterceptors(this.interceptors); setServerAddressStrategy(new HardcodedServerAddressStrategy(this.properties.getServer().getPath())); 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/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 { 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()).setCareTeam(new ArrayList()); + 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-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..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 @@ -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,87 @@ public class AuthorizationInterceptorDstu3Test { } + @Test + public void testReadByInstanceAllowsTargetedSearch() throws Exception { + ourConditionalCreateId = "1"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + RuleBuilder ruleBuilder = new RuleBuilder(); + ruleBuilder.allow().read().instance("Patient/900").andThen(); + ruleBuilder.allow().read().instance("Patient/700").andThen(); + return ruleBuilder.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); + + ourHitMethod = false; + 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=900,777"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + @Test public void testReadPageRight() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -3205,45 +3287,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 +3357,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 +3440,9 @@ public class AuthorizationInterceptorDstu3Test { } @Search() - public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + public List search( + @OptionalParam(name = "_id") TokenAndListParam theIds, + @OptionalParam(name = "subject") ReferenceParam theSubject) { ourHitMethod = true; return ourReturn; } @@ -3529,7 +3574,7 @@ public class AuthorizationInterceptorDstu3Test { } @Search() - public List search(@OptionalParam(name = "_id") IdType theIdParam) { + public List search(@OptionalParam(name = "_id") TokenAndListParam theIdParam) { ourHitMethod = true; return ourReturn; } @@ -3591,7 +3636,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 +3646,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-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..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 @@ -1,56 +1,50 @@ 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.Ignore; +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 +82,47 @@ public class ServerCapabilityStatementProviderDstu3Test { return resource; } + @Test + @Ignore + 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 +270,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 +339,7 @@ public class ServerCapabilityStatementProviderDstu3Test { assertEquals("Patient", opDef.getParameter().get(0).getType()); } } - + @Test public void testOperationDocumentation() throws Exception { @@ -544,7 +581,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 +598,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 +757,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 +820,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 +881,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 +892,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 +913,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 +957,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 +970,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 +987,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 +1019,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 +1029,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 +1045,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 +1053,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } @SuppressWarnings("unused") - public static class SearchProviderWithListWithType implements IResourceProvider { + public static class SearchProviderWithListWithType implements IResourceProvider { @Override public Class getResourceType() { @@ -1017,15 +1061,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 +1105,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } } - + public static class TypeLevelOperationProvider implements IResourceProvider { public static final String OPERATION_NAME = "op"; @@ -1110,4 +1152,9 @@ public class ServerCapabilityStatementProviderDstu3Test { } + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + } 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 { - - 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 { + + 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/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/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<HttpUriRequest> 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<HttpUriRequest> 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<Include> includes = new HashSet<Include>(); - 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<Encounter> found = client.searchByList(params, includes); + ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo"); + Set<Include> includes = new HashSet<Include>(); + 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<Encounter> 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<HttpUriRequest> 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<Encounter> 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<Encounter> 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<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set<Include> theIncludes) throws BaseServerResponseException; + @Test + public void testReturnTypedList() throws Exception { - } + String resp = createBundle(); - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } + ArgumentCaptor<HttpUriRequest> 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<Encounter> 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<Encounter> search(); + + @Search + List<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set<Include> theIncludes) throws BaseServerResponseException; + + @Search + List<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenAndListParam tokenOrListParam, @IncludeParam Set<Include> 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/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/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..ccd6c11a2c2 --- /dev/null +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java @@ -0,0 +1,320 @@ +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<Resource> 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 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() { + + 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<String> toStrings(BaseAndListParam<? extends IQueryParameterOr<?>> theParams) { + List<? extends IQueryParameterOr<? extends IQueryParameterType>> 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<? extends IBaseResource> getResourceType() { + return Patient.class; + } + + @Search() + public List<Resource> 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<? extends IBaseResource> getResourceType() { + return Observation.class; + } + + + @Search() + public List<Resource> 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) { + if (ourNextCompartmentList == null) { + return null; + } + 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/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/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..48363fa60b9 --- /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% diff --git a/pom.xml b/pom.xml index 91b544b88fb..d9795272a1f 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> @@ -519,6 +523,7 @@ <commons_lang3_version>3.8.1</commons_lang3_version> <derby_version>10.14.2.0</derby_version> <error_prone_annotations_version>2.0.18</error_prone_annotations_version> + <error_prone_core_version>2.3.2</error_prone_core_version> <guava_version>25.0-jre</guava_version> <gson_version>2.8.5</gson_version> <jaxb_bundle_version>2.2.11_1</jaxb_bundle_version> @@ -529,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> @@ -639,7 +644,7 @@ <dependency> <groupId>com.google.errorprone</groupId> <artifactId>error_prone_core</artifactId> - <version>2.3.2</version> + <version>${error_prone_core_version}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> @@ -766,7 +771,8 @@ <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> - <version>6.2.2.jre8</version> + <!--<version>6.2.2.jre8</version>--> + <version>7.0.0.jre8</version> </dependency> <!-- <dependency> @@ -1479,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> @@ -2308,7 +2319,7 @@ <path> <groupId>com.google.errorprone</groupId> <artifactId>error_prone_core</artifactId> - <version>2.3.2</version> + <version>${error_prone_core_version}</version> </path> </annotationProcessorPaths> </configuration> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7f77730f380..a41fac13d20 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> @@ -264,6 +264,86 @@ OperationDefinitions are now created for named queries in server module. Thanks to Stig Døssing for the pull request! </action> + <action type="add"> + 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. + </action> + <action type="add"> + In a DSTU2 server, if search parameters are expressed with chains directly in the + parameter name (e.g. + <![CDATA[<code>@RequiredParam(name="subject.name.family")</code>]]>) the second + part of the chain was lost when the chain was described in the server + CapabilityStatement. This has been corrected. + </action> + <action type="fix"> + 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. + </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> + <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> + <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> + <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> + <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> + <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> + <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> + <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. + </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> + <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> + <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"> 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 @@ </p> <p class="doc_info_bubble"> - 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. </p> @@ -253,7 +253,37 @@ </subsection> </section> - + + <section name="Search Narrowind"> + + <p> + HAPI FHIR 3.7.0 introduced a new interceptor, the + <a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.html">SearchNarrowingInterceptor</a>. + </p> + <p> + 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. + </p> + <p> + This could be used, for example, to allow the user to perform a search for<br/> + <code>http://baseurl/Observation?category=laboratory</code><br/> + and then receive results as though they had requested<br/> + <code>http://baseurl/Observation?subject=Patient/123&category=laboratory</code>. + </p> + <p> + An example of this interceptor follows: + </p> + <macro name="snippet"> + <param name="id" value="narrowing" /> + <param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" /> + </macro> + + </section> + </body> </document>