Merge branch 'master' into support-versioned-docs
This commit is contained in:
commit
5166207f58
|
@ -37,6 +37,11 @@ public interface IRestfulClientFactory {
|
|||
*/
|
||||
public static final int DEFAULT_CONNECTION_REQUEST_TIMEOUT = 10000;
|
||||
|
||||
/**
|
||||
* Default value for {@link #getConnectionTimeToLive()}
|
||||
*/
|
||||
public static final int DEFAULT_CONNECTION_TTL = 5000;
|
||||
|
||||
/**
|
||||
* Default value for {@link #getServerValidationModeEnum()}
|
||||
*/
|
||||
|
@ -75,6 +80,16 @@ public interface IRestfulClientFactory {
|
|||
*/
|
||||
int getConnectTimeout();
|
||||
|
||||
/**
|
||||
* Gets the connection time to live, in milliseconds. This is the amount of time to keep connections alive for reuse.
|
||||
* <p>
|
||||
* The default value for this setting is defined by {@link #DEFAULT_CONNECTION_TTL}
|
||||
* </p>
|
||||
*/
|
||||
default int getConnectionTimeToLive() {
|
||||
return DEFAULT_CONNECTION_TTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP client instance. This method will not return null.
|
||||
* @param theUrl
|
||||
|
@ -179,6 +194,14 @@ public interface IRestfulClientFactory {
|
|||
*/
|
||||
void setConnectTimeout(int theConnectTimeout);
|
||||
|
||||
/**
|
||||
* Sets the connection time to live, in milliseconds. This is the amount of time to keep connections alive for reuse.
|
||||
* <p>
|
||||
* The default value for this setting is defined by {@link #DEFAULT_CONNECTION_TTL}
|
||||
* </p>
|
||||
*/
|
||||
default void setConnectionTimeToLive(int theConnectionTimeToLive) {}
|
||||
|
||||
/**
|
||||
* Sets the Apache HTTP client instance to be used by any new restful clients created by this factory. If set to
|
||||
* <code>null</code>, a new HTTP client with default settings will be created.
|
||||
|
|
|
@ -151,11 +151,13 @@ public enum VersionEnum {
|
|||
V7_0_0,
|
||||
V7_0_1,
|
||||
V7_0_2,
|
||||
V7_0_3,
|
||||
|
||||
V7_1_0,
|
||||
V7_2_0,
|
||||
V7_2_1,
|
||||
V7_2_2,
|
||||
V7_2_3,
|
||||
|
||||
V7_3_0,
|
||||
V7_4_0,
|
||||
|
|
|
@ -120,6 +120,13 @@ public interface IIdType extends IPrimitiveType<String> {
|
|||
*/
|
||||
boolean isVersionIdPartValidLong();
|
||||
|
||||
/**
|
||||
* @return true if the id begins with "urn:uuid:"
|
||||
*/
|
||||
default boolean isUuid() {
|
||||
return getValue() != null && getValue().startsWith("urn:uuid:");
|
||||
}
|
||||
|
||||
@Override
|
||||
IIdType setValue(String theString);
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.incomingNoopInTransaction=Transaction contai
|
|||
ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.invalidMatchUrlInvalidResourceType=Invalid match URL "{0}" - Unknown resource type: "{1}"
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.invalidMatchUrlNoMatches=Invalid match URL "{0}" - No resources match this search
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.inlineMatchNotSupported=Inline match URLs are not supported on this server. Cannot process reference: "{0}"
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithMultipleMatchFailure=Failed to {0} resource with match URL "{1}" because this search matched {2} resources
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithMultipleMatchFailure=Failed to {0} {1} with match URL "{2}" because this search matched {3} resources
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.deleteByUrlThresholdExceeded=Failed to DELETE resources with match URL "{0}" because the resolved number of resources: {1} exceeds the threshold of {2}
|
||||
ca.uhn.fhir.jpa.dao.BaseStorageDao.transactionOperationWithIdNotMatchFailure=Failed to {0} resource with match URL "{1}" because the matching resource does not match the provided ID
|
||||
ca.uhn.fhir.jpa.dao.BaseTransactionProcessor.multiplePartitionAccesses=Can not process transaction with {0} entries: Entries require access to multiple/conflicting partitions
|
||||
|
|
|
@ -105,10 +105,11 @@ public class HapiFhirCliRestfulClientFactory extends RestfulClientFactory {
|
|||
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
|
||||
.register("https", sslConnectionSocketFactory)
|
||||
.build();
|
||||
connectionManager =
|
||||
new PoolingHttpClientConnectionManager(registry, null, null, null, 5000, TimeUnit.MILLISECONDS);
|
||||
connectionManager = new PoolingHttpClientConnectionManager(
|
||||
registry, null, null, null, getConnectionTimeToLive(), TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
||||
connectionManager =
|
||||
new PoolingHttpClientConnectionManager(getConnectionTimeToLive(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
connectionManager.setMaxTotal(getPoolMaxTotal());
|
||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.cli;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.rest.client.apache.ApacheRestfulClientFactory;
|
||||
import ca.uhn.fhir.rest.client.api.IRestfulClientFactory;
|
||||
import ca.uhn.fhir.test.BaseFhirVersionParameterizedTest;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
|
@ -10,6 +11,7 @@ import org.apache.http.client.methods.HttpGet;
|
|||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
|
@ -79,4 +81,13 @@ public class ApacheRestfulClientFactoryTest extends BaseFhirVersionParameterized
|
|||
assertEquals(SSLHandshakeException.class, e.getCause().getCause().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectionTimeToLive() {
|
||||
ApacheRestfulClientFactory clientFactory = new ApacheRestfulClientFactory();
|
||||
|
||||
assertEquals(IRestfulClientFactory.DEFAULT_CONNECTION_TTL, clientFactory.getConnectionTimeToLive());
|
||||
clientFactory.setConnectionTimeToLive(25000);
|
||||
assertEquals(25000, clientFactory.getConnectionTimeToLive());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.cli.client;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.rest.client.api.IRestfulClientFactory;
|
||||
import ca.uhn.fhir.test.BaseFhirVersionParameterizedTest;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
|
@ -159,4 +160,15 @@ public class HapiFhirCliRestfulClientFactoryTest extends BaseFhirVersionParamete
|
|||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("baseParamsProvider")
|
||||
public void testConnectionTimeToLive(FhirVersionEnum theFhirVersion) {
|
||||
FhirVersionParams fhirVersionParams = getFhirVersionParams(theFhirVersion);
|
||||
HapiFhirCliRestfulClientFactory clientFactory = new HapiFhirCliRestfulClientFactory(fhirVersionParams.getFhirContext());
|
||||
|
||||
assertEquals(IRestfulClientFactory.DEFAULT_CONNECTION_TTL, clientFactory.getConnectionTimeToLive());
|
||||
clientFactory.setConnectionTimeToLive(25000);
|
||||
assertEquals(25000, clientFactory.getConnectionTimeToLive());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,23 +31,15 @@
|
|||
<classifier>classes</classifier>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>com.oracle</groupId>
|
||||
<artifactId>ojbc8g</artifactId>
|
||||
<version>12.2.0.1</version>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.client.api.Header;
|
|||
import ca.uhn.fhir.rest.client.api.IHttpClient;
|
||||
import ca.uhn.fhir.rest.client.impl.RestfulClientFactory;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
@ -65,6 +66,7 @@ public class OkHttpRestfulClientFactory extends RestfulClientFactory {
|
|||
myNativeClient = new OkHttpClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(getConnectTimeout(), TimeUnit.MILLISECONDS)
|
||||
.connectionPool(new ConnectionPool(5, getConnectionTimeToLive(), TimeUnit.MILLISECONDS))
|
||||
.readTimeout(getSocketTimeout(), TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(getSocketTimeout(), TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.okhttp;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.okhttp.client.OkHttpRestfulClientFactory;
|
||||
import ca.uhn.fhir.rest.client.api.IRestfulClientFactory;
|
||||
import ca.uhn.fhir.test.BaseFhirVersionParameterizedTest;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
@ -71,6 +72,13 @@ public class OkHttpRestfulClientFactoryTest extends BaseFhirVersionParameterized
|
|||
assertEquals(1516, ((OkHttpClient) clientFactory.getNativeClient()).connectTimeoutMillis());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectionTimeToLive() {
|
||||
assertEquals(IRestfulClientFactory.DEFAULT_CONNECTION_TTL, clientFactory.getConnectionTimeToLive());
|
||||
clientFactory.setConnectionTimeToLive(25000);
|
||||
assertEquals(25000, clientFactory.getConnectionTimeToLive());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("baseParamsProvider")
|
||||
public void testNativeClientHttp(FhirVersionEnum theFhirVersion) throws Exception {
|
||||
|
|
|
@ -103,7 +103,7 @@ public class ApacheRestfulClientFactory extends RestfulClientFactory {
|
|||
.disableCookieManagement();
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager =
|
||||
new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
||||
new PoolingHttpClientConnectionManager(getConnectionTimeToLive(), TimeUnit.MILLISECONDS);
|
||||
connectionManager.setMaxTotal(getPoolMaxTotal());
|
||||
connectionManager.setDefaultMaxPerRoute(getPoolMaxPerRoute());
|
||||
builder.setConnectionManager(connectionManager);
|
||||
|
|
|
@ -57,6 +57,7 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
|
|||
private final Set<String> myValidatedServerBaseUrls = Collections.synchronizedSet(new HashSet<>());
|
||||
private int myConnectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;
|
||||
private int myConnectTimeout = DEFAULT_CONNECT_TIMEOUT;
|
||||
private int myConnectionTimeToLive = DEFAULT_CONNECTION_TTL;
|
||||
private FhirContext myContext;
|
||||
private final Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers =
|
||||
new HashMap<>();
|
||||
|
@ -91,6 +92,11 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
|
|||
return myConnectTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getConnectionTimeToLive() {
|
||||
return myConnectionTimeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the proxy username to authenticate with the HTTP proxy
|
||||
*/
|
||||
|
@ -210,6 +216,12 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
|
|||
resetHttpClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setConnectionTimeToLive(int theConnectionTimeToLive) {
|
||||
myConnectionTimeToLive = theConnectionTimeToLive;
|
||||
resetHttpClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context associated with this client factory. Must not be called more than once.
|
||||
*/
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
release-date: "2024-03-20"
|
||||
codename: "Zed"
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
release-date: "2024-08-24"
|
||||
codename: "Zed"
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
release-date: "2024-08-25"
|
||||
codename: "Borealis"
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
type: add
|
||||
backport: 7.2.3
|
||||
issue: 6070
|
||||
jira: SMILE-8503
|
||||
title: "Added paging support for `$everything` operation in synchronous search mode."
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
---
|
||||
type: perf
|
||||
issue: 6099
|
||||
backport: 7.0.3,7.2.2
|
||||
title: "Database migrations that add or drop an index no longer lock tables when running on Azure Sql Server."
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
type: add
|
||||
issue: 6184
|
||||
title: "Added a configuration setting for the TTL of HTTP connections to IRestfulClientFactory.
|
||||
The following implementations have been updated to respect this new setting:
|
||||
1. ApacheRestfulClientFactory
|
||||
2. OkHttpRestfulClientFactory
|
||||
3. HapiFhirCliRestfulClientFactory
|
||||
Thanks to Alex Kopp and Alex Cote for the contribution!
|
||||
"
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 6203
|
||||
title: "Previously, the SubscriptionValidatingInterceptor would allow the creation/update of a REST hook subscription
|
||||
where the endpoint URL property is not prefixed with http[s]. This issue is fixed."
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 6206
|
||||
title: "A resource leak during database migration on Oracle could cause a failure `ORA-01000 maximum open cursors for session`. This has been corrected. Thanks to Jonas Beyer for the contribution!"
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
type: fix
|
||||
backport: 7.2.3
|
||||
issue: 6216
|
||||
jira: SMILE-8806
|
||||
title: "Previously, searches combining the `_text` query parameter (using Lucene/Elasticsearch) with query parameters
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 6231
|
||||
title: "The PatientIdPartitionInterceptor could on rare occasion select the incorrect
|
||||
partition for a resource. This has been corrected. In order for the wrong partition
|
||||
to be selected, the following three things need to be true:
|
||||
1) there are multiple values of a patient compartment for a resource (see https://hl7.org/fhir/R4/compartmentdefinition-patient.html)
|
||||
2) a patient compartment value is a non-Patient reference
|
||||
3) the search parameter of the incorrect value needs to come alphabetically before the search parameter of the correct
|
||||
value.
|
||||
|
||||
For example, if a QuestionnaireResponse has subject Patient/123 and author Organization/456,
|
||||
then since 'author' appears ahead of 'subject' alphabetically it would incorrectly determine the partition.
|
||||
The fix changed the partition selection so that it now only matches on Patient references."
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
- item:
|
||||
type: "add"
|
||||
title: "The version of a few dependencies have been bumped to more recent versions
|
||||
(dependent HAPI modules listed in brackets):
|
||||
<ul>
|
||||
<li>Bower/Moment.js (hapi-fhir-testpage-overlay): 2.27.0 -> 2.29.4</li>
|
||||
<li>htmlunit (Base): 3.9.0 -> 3.11.0</li>
|
||||
<li>Elasticsearch (Base): 8.11.1 -> 8.14.3</li>
|
||||
</ul>"
|
|
@ -420,6 +420,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
BaseStorageDao.class,
|
||||
"transactionOperationWithMultipleMatchFailure",
|
||||
"CREATE",
|
||||
myResourceName,
|
||||
theMatchUrl,
|
||||
match.size());
|
||||
throw new PreconditionFailedException(Msg.code(958) + msg);
|
||||
|
@ -861,6 +862,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
BaseStorageDao.class,
|
||||
"transactionOperationWithMultipleMatchFailure",
|
||||
"DELETE",
|
||||
myResourceName,
|
||||
theUrl,
|
||||
resourceIds.size()));
|
||||
}
|
||||
|
@ -2335,6 +2337,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
BaseStorageDao.class,
|
||||
"transactionOperationWithMultipleMatchFailure",
|
||||
"UPDATE",
|
||||
myResourceName,
|
||||
theMatchUrl,
|
||||
match.size());
|
||||
throw new PreconditionFailedException(Msg.code(988) + msg);
|
||||
|
|
|
@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.test.config.BlockLargeNumbersOfParamsListener;
|
|||
import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig;
|
||||
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
|
||||
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||
import co.elastic.clients.elasticsearch.indices.IndexSettings;
|
||||
import co.elastic.clients.elasticsearch.indices.PutTemplateResponse;
|
||||
import co.elastic.clients.json.JsonData;
|
||||
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
|
||||
|
@ -129,7 +130,7 @@ public class ElasticsearchWithPrefixConfig {
|
|||
.putTemplate(b -> b
|
||||
.name("ngram-template")
|
||||
.indexPatterns("*resourcetable-*", "*termconcept-*")
|
||||
.settings(Map.of("index.max_ngram_diff", JsonData.of(50))));
|
||||
.settings(new IndexSettings.Builder().maxNgramDiff(50).build()));
|
||||
assert acknowledgedResponse.acknowledged();
|
||||
} catch (IOException theE) {
|
||||
theE.printStackTrace();
|
||||
|
|
|
@ -27,9 +27,13 @@ import java.util.Collections;
|
|||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public abstract class BaseSubscriptionSettings {
|
||||
public static final String DEFAULT_EMAIL_FROM_ADDRESS = "noreply@unknown.com";
|
||||
public static final String DEFAULT_WEBSOCKET_CONTEXT_PATH = "/websocket";
|
||||
public static final String DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX =
|
||||
"((((http?|https?)://))([-%()_.!~*';/?:@&=+$,A-Za-z0-9])+)";
|
||||
|
||||
private final Set<Subscription.SubscriptionChannelType> mySupportedSubscriptionTypes = new HashSet<>();
|
||||
private String myEmailFromAddress = DEFAULT_EMAIL_FROM_ADDRESS;
|
||||
|
@ -45,6 +49,13 @@ public abstract class BaseSubscriptionSettings {
|
|||
*/
|
||||
private boolean myAllowOnlyInMemorySubscriptions = false;
|
||||
|
||||
/**
|
||||
* @since 7.6.0
|
||||
*
|
||||
* Regex To perform validation on the endpoint URL for Subscription of type RESTHOOK.
|
||||
*/
|
||||
private String myRestHookEndpointUrlValidationRegex = DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX;
|
||||
|
||||
/**
|
||||
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
|
||||
* to the server matching these types will be activated.
|
||||
|
@ -235,4 +246,32 @@ public abstract class BaseSubscriptionSettings {
|
|||
public void setTriggerSubscriptionsForNonVersioningChanges(boolean theTriggerSubscriptionsForNonVersioningChanges) {
|
||||
myTriggerSubscriptionsForNonVersioningChanges = theTriggerSubscriptionsForNonVersioningChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the regex expression to perform endpoint URL validation If rest-hook subscriptions are supported.
|
||||
* Default value is {@link #DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX}.
|
||||
* @since 7.6.0
|
||||
*/
|
||||
public String getRestHookEndpointUrlValidationRegex() {
|
||||
return myRestHookEndpointUrlValidationRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the regex expression that will be used to validate the endpoint URL.
|
||||
* Set to NULL or EMPTY for no endpoint URL validation.
|
||||
*
|
||||
* @since 7.6.0
|
||||
*/
|
||||
public void setRestHookEndpointUrlValidationRegex(String theRestHookEndpointUrlValidationgRegex) {
|
||||
myRestHookEndpointUrlValidationRegex = theRestHookEndpointUrlValidationgRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an endpoint validation Regex was set for URL validation.
|
||||
*
|
||||
* @since 7.6.0
|
||||
*/
|
||||
public boolean hasRestHookEndpointUrlValidationRegex() {
|
||||
return isNotBlank(myRestHookEndpointUrlValidationRegex);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.subscription.config;
|
|||
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
|
|
@ -30,6 +30,10 @@ import ca.uhn.fhir.jpa.subscription.model.config.SubscriptionModelConfig;
|
|||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.IChannelTypeValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RegexEndpointUrlValidationStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc;
|
||||
import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc;
|
||||
import ca.uhn.fhir.jpa.subscription.triggering.SubscriptionTriggeringSvcImpl;
|
||||
|
@ -43,6 +47,10 @@ import org.springframework.context.annotation.Configuration;
|
|||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator.noOpEndpointUrlValidationStrategy;
|
||||
|
||||
/**
|
||||
* This Spring config should be imported by a system that submits resources to the
|
||||
* matching queue for processing
|
||||
|
@ -103,4 +111,23 @@ public class SubscriptionSubmitterConfig {
|
|||
return new AsyncResourceModifiedSubmitterSvc(
|
||||
theIResourceModifiedMessagePersistenceSvc, theResourceModifiedConsumer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IChannelTypeValidator restHookChannelValidator(SubscriptionSettings theSubscriptionSettings) {
|
||||
RestHookChannelValidator.IEndpointUrlValidationStrategy iEndpointUrlValidationStrategy =
|
||||
noOpEndpointUrlValidationStrategy;
|
||||
|
||||
if (theSubscriptionSettings.hasRestHookEndpointUrlValidationRegex()) {
|
||||
String endpointUrlValidationRegex = theSubscriptionSettings.getRestHookEndpointUrlValidationRegex();
|
||||
iEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(endpointUrlValidationRegex);
|
||||
}
|
||||
|
||||
return new RestHookChannelValidator(iEndpointUrlValidationStrategy);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionChannelTypeValidatorFactory subscriptionChannelTypeValidatorFactory(
|
||||
List<IChannelTypeValidator> theValidorList) {
|
||||
return new SubscriptionChannelTypeValidatorFactory(theValidorList);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,8 +36,10 @@ import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyE
|
|||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.IChannelTypeValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
|
@ -87,6 +89,9 @@ public class SubscriptionValidatingInterceptor {
|
|||
@Autowired
|
||||
private SubscriptionQueryValidator mySubscriptionQueryValidator;
|
||||
|
||||
@Autowired
|
||||
private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory;
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
|
||||
public void resourcePreCreate(
|
||||
IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
|
||||
|
@ -149,7 +154,7 @@ public class SubscriptionValidatingInterceptor {
|
|||
break;
|
||||
}
|
||||
|
||||
validatePermissions(theSubscription, subscription, theRequestDetails, theRequestPartitionId, thePointcut);
|
||||
validatePermissions(theSubscription, theRequestDetails, theRequestPartitionId, thePointcut);
|
||||
|
||||
mySubscriptionCanonicalizer.setMatchingStrategyTag(theSubscription, null);
|
||||
|
||||
|
@ -167,7 +172,7 @@ public class SubscriptionValidatingInterceptor {
|
|||
|
||||
try {
|
||||
SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(subscription);
|
||||
if (!(SubscriptionMatchingStrategy.IN_MEMORY == strategy)
|
||||
if (SubscriptionMatchingStrategy.IN_MEMORY != strategy
|
||||
&& mySubscriptionSettings.isOnlyAllowInMemorySubscriptions()) {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2367)
|
||||
|
@ -236,7 +241,6 @@ public class SubscriptionValidatingInterceptor {
|
|||
|
||||
protected void validatePermissions(
|
||||
IBaseResource theSubscription,
|
||||
CanonicalSubscription theCanonicalSubscription,
|
||||
RequestDetails theRequestDetails,
|
||||
RequestPartitionId theRequestPartitionId,
|
||||
Pointcut thePointcut) {
|
||||
|
@ -319,27 +323,11 @@ public class SubscriptionValidatingInterceptor {
|
|||
protected void validateChannelType(CanonicalSubscription theSubscription) {
|
||||
if (theSubscription.getChannelType() == null) {
|
||||
throw new UnprocessableEntityException(Msg.code(20) + "Subscription.channel.type must be populated");
|
||||
} else if (theSubscription.getChannelType() == CanonicalSubscriptionChannelType.RESTHOOK) {
|
||||
validateChannelPayload(theSubscription);
|
||||
validateChannelEndpoint(theSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
protected void validateChannelEndpoint(CanonicalSubscription theResource) {
|
||||
if (isBlank(theResource.getEndpointUrl())) {
|
||||
throw new UnprocessableEntityException(
|
||||
Msg.code(21) + "Rest-hook subscriptions must have Subscription.channel.endpoint defined");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
protected void validateChannelPayload(CanonicalSubscription theResource) {
|
||||
if (!isBlank(theResource.getPayloadString())
|
||||
&& EncodingEnum.forContentType(theResource.getPayloadString()) == null) {
|
||||
throw new UnprocessableEntityException(Msg.code(1985) + "Invalid value for Subscription.channel.payload: "
|
||||
+ theResource.getPayloadString());
|
||||
}
|
||||
IChannelTypeValidator iChannelTypeValidator =
|
||||
mySubscriptionChannelTypeValidatorFactory.getValidatorForChannelType(theSubscription.getChannelType());
|
||||
iChannelTypeValidator.validateChannelType(theSubscription);
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
|
@ -371,4 +359,10 @@ public class SubscriptionValidatingInterceptor {
|
|||
mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator;
|
||||
mySubscriptionQueryValidator = new SubscriptionQueryValidator(myDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSubscriptionChannelTypeValidatorFactoryForUnitTest(
|
||||
SubscriptionChannelTypeValidatorFactory theSubscriptionChannelTypeValidatorFactory) {
|
||||
mySubscriptionChannelTypeValidatorFactory = theSubscriptionChannelTypeValidatorFactory;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator;
|
||||
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
|
||||
public interface IChannelTypeValidator {
|
||||
|
||||
void validateChannelType(CanonicalSubscription theSubscription);
|
||||
|
||||
CanonicalSubscriptionChannelType getSubscriptionChannelType();
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
public class RegexEndpointUrlValidationStrategy implements RestHookChannelValidator.IEndpointUrlValidationStrategy {
|
||||
|
||||
private final Pattern myEndpointUrlValidationPattern;
|
||||
|
||||
public RegexEndpointUrlValidationStrategy(@Nonnull String theEndpointUrlValidationRegex) {
|
||||
try {
|
||||
myEndpointUrlValidationPattern = Pattern.compile(theEndpointUrlValidationRegex);
|
||||
} catch (PatternSyntaxException e) {
|
||||
throw new IllegalArgumentException(
|
||||
Msg.code(2546) + " invalid synthax for provided regex " + theEndpointUrlValidationRegex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateEndpointUrl(String theEndpointUrl) {
|
||||
Matcher matcher = myEndpointUrlValidationPattern.matcher(theEndpointUrl);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
throw new UnprocessableEntityException(
|
||||
Msg.code(2545) + "Failed validation for endpoint URL: " + theEndpointUrl);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
/**
|
||||
*
|
||||
* Definition of a REST Hook channel validator that perform checks on the channel payload and endpoint URL.
|
||||
*
|
||||
* The channel payload will always evaluate in the same manner where endpoint URL validation can be extended beyond the
|
||||
* minimal validation perform by this class.
|
||||
*
|
||||
* At a minimum, this class ensures that the provided URL is not blank or null. Supplemental validation(s) should be
|
||||
* encapsulated into a {@link IEndpointUrlValidationStrategy} and provided with the arg constructor.
|
||||
*
|
||||
*/
|
||||
public class RestHookChannelValidator implements IChannelTypeValidator {
|
||||
|
||||
private final IEndpointUrlValidationStrategy myEndpointUrlValidationStrategy;
|
||||
|
||||
/**
|
||||
* Constructor for a validator where the endpoint URL will
|
||||
*/
|
||||
public RestHookChannelValidator() {
|
||||
this(noOpEndpointUrlValidationStrategy);
|
||||
}
|
||||
|
||||
public RestHookChannelValidator(@Nonnull IEndpointUrlValidationStrategy theEndpointUrlValidationStrategy) {
|
||||
myEndpointUrlValidationStrategy = theEndpointUrlValidationStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateChannelType(CanonicalSubscription theSubscription) {
|
||||
validateChannelPayload(theSubscription);
|
||||
validateChannelEndpoint(theSubscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CanonicalSubscriptionChannelType getSubscriptionChannelType() {
|
||||
return CanonicalSubscriptionChannelType.RESTHOOK;
|
||||
}
|
||||
|
||||
protected void validateChannelEndpoint(@Nonnull CanonicalSubscription theCanonicalSubscription) {
|
||||
String endpointUrl = theCanonicalSubscription.getEndpointUrl();
|
||||
|
||||
if (isBlank(endpointUrl)) {
|
||||
throw new UnprocessableEntityException(
|
||||
Msg.code(21) + "Rest-hook subscriptions must have Subscription.channel.endpoint defined");
|
||||
}
|
||||
|
||||
myEndpointUrlValidationStrategy.validateEndpointUrl(endpointUrl);
|
||||
}
|
||||
|
||||
protected void validateChannelPayload(CanonicalSubscription theResource) {
|
||||
if (!isBlank(theResource.getPayloadString())
|
||||
&& EncodingEnum.forContentType(theResource.getPayloadString()) == null) {
|
||||
throw new UnprocessableEntityException(Msg.code(1985) + "Invalid value for Subscription.channel.payload: "
|
||||
+ theResource.getPayloadString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A concrete instantiation of this interface should provide tailored validation of an endpoint URL
|
||||
* throwing {@link RuntimeException} upon validation failure.
|
||||
*/
|
||||
public interface IEndpointUrlValidationStrategy {
|
||||
void validateEndpointUrl(String theEndpointUrl);
|
||||
}
|
||||
|
||||
public static final IEndpointUrlValidationStrategy noOpEndpointUrlValidationStrategy = theEndpointUrl -> {};
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator;
|
||||
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SubscriptionChannelTypeValidatorFactory {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionChannelTypeValidatorFactory.class);
|
||||
|
||||
private final Map<CanonicalSubscriptionChannelType, IChannelTypeValidator> myValidators =
|
||||
new EnumMap<>(CanonicalSubscriptionChannelType.class);
|
||||
|
||||
public SubscriptionChannelTypeValidatorFactory(@Nonnull List<IChannelTypeValidator> theValidorList) {
|
||||
theValidorList.forEach(this::addChannelTypeValidator);
|
||||
}
|
||||
|
||||
public IChannelTypeValidator getValidatorForChannelType(CanonicalSubscriptionChannelType theChannelType) {
|
||||
return myValidators.getOrDefault(theChannelType, getNoopValidatorForChannelType(theChannelType));
|
||||
}
|
||||
|
||||
public void addChannelTypeValidator(IChannelTypeValidator theValidator) {
|
||||
myValidators.put(theValidator.getSubscriptionChannelType(), theValidator);
|
||||
}
|
||||
|
||||
private IChannelTypeValidator getNoopValidatorForChannelType(CanonicalSubscriptionChannelType theChannelType) {
|
||||
return new IChannelTypeValidator() {
|
||||
@Override
|
||||
public void validateChannelType(CanonicalSubscription theSubscription) {
|
||||
ourLog.debug(
|
||||
"No validator for channel type {} was registered, will perform no-op validation.",
|
||||
theChannelType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CanonicalSubscriptionChannelType getSubscriptionChannelType() {
|
||||
return theChannelType;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.jpa.subscription.submit.interceptor;
|
||||
package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
|
@ -23,7 +23,7 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import ca.uhn.fhir.jpa.subscription.config.SubscriptionConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.jpa.util.MemoryCacheService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
|
|
@ -25,7 +25,7 @@ import ca.uhn.fhir.interceptor.api.Hook;
|
|||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
|
|
|
@ -15,7 +15,7 @@ import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
|||
import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender;
|
||||
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
|
|
@ -4,15 +4,19 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
|
||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.IChannelTypeValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RegexEndpointUrlValidationStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionQueryValidator;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
|
@ -29,11 +33,13 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
@ -49,6 +55,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
|
@ -70,6 +78,9 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
private IFhirResourceDao<SubscriptionTopic> mySubscriptionTopicDao;
|
||||
private FhirContext myFhirContext;
|
||||
|
||||
@SpyBean
|
||||
private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
setFhirContext(FhirVersionEnum.R4B);
|
||||
|
@ -79,8 +90,9 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource("subscriptionByFhirVersion345")
|
||||
public void testEmptySub(IBaseResource theSubscription) {
|
||||
setFhirContext(theSubscription);
|
||||
|
||||
try {
|
||||
setFhirContext(theSubscription);
|
||||
mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
|
@ -92,8 +104,9 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource("subscriptionByFhirVersion34") // R5 subscriptions don't have criteria
|
||||
public void testEmptyCriteria(IBaseResource theSubscription) {
|
||||
initSubscription(theSubscription);
|
||||
|
||||
try {
|
||||
initSubscription(theSubscription);
|
||||
mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
|
@ -105,9 +118,10 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource("subscriptionByFhirVersion34")
|
||||
public void testBadCriteria(IBaseResource theSubscription) {
|
||||
initSubscription(theSubscription);
|
||||
SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient");
|
||||
|
||||
try {
|
||||
initSubscription(theSubscription);
|
||||
SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient");
|
||||
mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
|
@ -118,9 +132,10 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource("subscriptionByFhirVersion34")
|
||||
public void testBadChannel(IBaseResource theSubscription) {
|
||||
initSubscription(theSubscription);
|
||||
SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?");
|
||||
|
||||
try {
|
||||
initSubscription(theSubscription);
|
||||
SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?");
|
||||
mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
|
@ -131,10 +146,11 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
@ParameterizedTest
|
||||
@MethodSource("subscriptionByFhirVersion345")
|
||||
public void testEmptyEndpoint(IBaseResource theSubscription) {
|
||||
initSubscription(theSubscription);
|
||||
SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?");
|
||||
SubscriptionUtil.setChannelType(myFhirContext, theSubscription, "message");
|
||||
|
||||
try {
|
||||
initSubscription(theSubscription);
|
||||
SubscriptionUtil.setCriteria(myFhirContext, theSubscription, "Patient?");
|
||||
SubscriptionUtil.setChannelType(myFhirContext, theSubscription, "message");
|
||||
mySubscriptionValidatingInterceptor.resourcePreCreate(theSubscription, null, null);
|
||||
fail();
|
||||
} catch (UnprocessableEntityException e) {
|
||||
|
@ -223,8 +239,27 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
SimpleBundleProvider simpleBundleProvider = new SimpleBundleProvider(List.of(topic));
|
||||
when(mySubscriptionTopicDao.search(any(), any())).thenReturn(simpleBundleProvider);
|
||||
mySubscriptionValidatingInterceptor.validateSubmittedSubscription(badSub, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
|
||||
|
||||
verify(mySubscriptionChannelTypeValidatorFactory, times(1)).getValidatorForChannelType(CanonicalSubscriptionChannelType.MESSAGE);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"acme.corp",
|
||||
"https://acme.corp/badstuff-%%$^&& iuyi",
|
||||
"ftp://acme.corp"})
|
||||
public void testRestHookEndpointValidation_whenProvidedWithBadURLs(String theBadUrl) {
|
||||
try {
|
||||
Subscription subscriptionWithBadEndpoint = createSubscription();
|
||||
subscriptionWithBadEndpoint.getChannel().setEndpoint(theBadUrl);
|
||||
|
||||
mySubscriptionValidatingInterceptor.validateSubmittedSubscription(subscriptionWithBadEndpoint, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
|
||||
fail("");
|
||||
} catch (Exception e) {
|
||||
verify(mySubscriptionChannelTypeValidatorFactory, times(1)).getValidatorForChannelType(CanonicalSubscriptionChannelType.RESTHOOK);
|
||||
assertThat(e.getMessage()).startsWith(Msg.code(2545));
|
||||
}
|
||||
}
|
||||
|
||||
private void initSubscription(IBaseResource theSubscription) {
|
||||
setFhirContext(theSubscription);
|
||||
|
@ -295,9 +330,22 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
}
|
||||
|
||||
@Bean
|
||||
SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||
SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) {
|
||||
return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IChannelTypeValidator restHookChannelValidator() {
|
||||
String regex = new SubscriptionSettings().getRestHookEndpointUrlValidationRegex();
|
||||
RegexEndpointUrlValidationStrategy regexEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(regex);
|
||||
return new RestHookChannelValidator(regexEndpointUrlValidationStrategy);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubscriptionChannelTypeValidatorFactory subscriptionChannelTypeValidatorFactory(
|
||||
List<IChannelTypeValidator> theValidorList) {
|
||||
return new SubscriptionChannelTypeValidatorFactory(theValidorList);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
@ -307,7 +355,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
subscription.setCriteria("Patient?");
|
||||
final Subscription.SubscriptionChannelComponent channel = subscription.getChannel();
|
||||
channel.setType(Subscription.SubscriptionChannelType.RESTHOOK);
|
||||
channel.setEndpoint("channel");
|
||||
channel.setEndpoint("http://acme.corp/");
|
||||
return subscription;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
package ca.uhn.fhir.jpa.subscription.submit.interceptor.validator;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
|
||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.r4.model.Subscription;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.REQUESTED;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class RestHookChannelValidatorTest {
|
||||
private final FhirContext myCtx = FhirContext.forR4();
|
||||
private final SubscriptionSettings mySubscriptionSettings = new SubscriptionSettings();
|
||||
private final SubscriptionCanonicalizer mySubscriptionCanonicalizer= new SubscriptionCanonicalizer(myCtx, mySubscriptionSettings);
|
||||
|
||||
private final String NO_PAYLOAD = StringUtils.EMPTY;
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("urlAndExpectedEvaluationResultProvider")
|
||||
public void testRestHookChannelValidation_withUrl(String theUrl, boolean theExpectedValidationResult){
|
||||
RegexEndpointUrlValidationStrategy regexEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(SubscriptionSettings.DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX);
|
||||
RestHookChannelValidator restHookChannelValidator = new RestHookChannelValidator(regexEndpointUrlValidationStrategy);
|
||||
|
||||
CanonicalSubscription subscription = createSubscription(theUrl, NO_PAYLOAD);
|
||||
doValidateUrlAndAssert(restHookChannelValidator, subscription, theExpectedValidationResult);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("urlAndExpectedEvaluationResultProviderForNoUrlValidation")
|
||||
public void testRestHookChannelValidation_withNoUrlValidation(String theUrl, boolean theExpectedValidationResult){
|
||||
RestHookChannelValidator restHookChannelValidator = new RestHookChannelValidator();
|
||||
|
||||
CanonicalSubscription subscription = createSubscription(theUrl, NO_PAYLOAD);
|
||||
doValidateUrlAndAssert(restHookChannelValidator, subscription, theExpectedValidationResult);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("payloadAndExpectedEvaluationResultProvider")
|
||||
public void testRestHookChannelValidation_withPayload(String thePayload, boolean theExpectedValidationResult){
|
||||
RestHookChannelValidator restHookChannelValidator = new RestHookChannelValidator();
|
||||
|
||||
CanonicalSubscription subscription = createSubscription("https://acme.org", thePayload);
|
||||
doValidatePayloadAndAssert(restHookChannelValidator, subscription, theExpectedValidationResult);
|
||||
}
|
||||
|
||||
private void doValidatePayloadAndAssert(RestHookChannelValidator theRestHookChannelValidator, CanonicalSubscription theSubscription, boolean theExpectedValidationResult) {
|
||||
boolean validationResult = true;
|
||||
|
||||
try {
|
||||
theRestHookChannelValidator.validateChannelPayload(theSubscription);
|
||||
} catch (Exception e){
|
||||
validationResult = false;
|
||||
}
|
||||
|
||||
if( validationResult != theExpectedValidationResult){
|
||||
String message = String.format("Validation result for payload %s was expected to be %b but was %b", theSubscription.getEndpointUrl(), theExpectedValidationResult, validationResult);
|
||||
fail(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void doValidateUrlAndAssert(RestHookChannelValidator theRestHookChannelValidator, CanonicalSubscription theSubscription, boolean theExpectedValidationResult) {
|
||||
boolean validationResult = true;
|
||||
|
||||
try {
|
||||
theRestHookChannelValidator.validateChannelEndpoint(theSubscription);
|
||||
} catch (Exception e){
|
||||
validationResult = false;
|
||||
}
|
||||
|
||||
if( validationResult != theExpectedValidationResult){
|
||||
String message = String.format("Validation result for URL %s was expected to be %b but was %b", theSubscription.getEndpointUrl(), theExpectedValidationResult, validationResult);
|
||||
fail(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private CanonicalSubscription createSubscription(String theUrl, String thePayload) {
|
||||
final Subscription subscription = new Subscription();
|
||||
subscription.setStatus(REQUESTED);
|
||||
subscription.setCriteria("Patient?");
|
||||
final Subscription.SubscriptionChannelComponent channel = subscription.getChannel();
|
||||
channel.setType(Subscription.SubscriptionChannelType.RESTHOOK);
|
||||
channel.setEndpoint(theUrl);
|
||||
channel.setPayload(thePayload);
|
||||
return mySubscriptionCanonicalizer.canonicalize(subscription);
|
||||
}
|
||||
|
||||
static Stream<Arguments> urlAndExpectedEvaluationResultProvider() {
|
||||
return Stream.of(
|
||||
Arguments.of("http://www.acme.corp/fhir", true),
|
||||
Arguments.of("http://acme.corp/fhir", true),
|
||||
Arguments.of("http://acme.corp:8000/fhir", true),
|
||||
Arguments.of("http://acme.corp:8000/fhir/", true),
|
||||
Arguments.of("http://acme.corp/fhir/", true),
|
||||
Arguments.of("https://foo.bar.com", true),
|
||||
Arguments.of("http://localhost:8000", true),
|
||||
Arguments.of("http://localhost:8000/", true),
|
||||
Arguments.of("http://localhost:8000/fhir", true),
|
||||
Arguments.of("http://localhost:8000/fhir/", true),
|
||||
Arguments.of("acme.corp", false),
|
||||
Arguments.of("https://acme.corp/badstuff-%%$^&& iuyi", false),
|
||||
Arguments.of("ftp://acme.corp", false));
|
||||
}
|
||||
|
||||
static Stream<Arguments> urlAndExpectedEvaluationResultProviderForNoUrlValidation() {
|
||||
return Stream.of(
|
||||
Arguments.of(null, false),
|
||||
Arguments.of("", false),
|
||||
Arguments.of(" ", false),
|
||||
Arguments.of("something", true));
|
||||
}
|
||||
|
||||
static Stream<Arguments> payloadAndExpectedEvaluationResultProvider() {
|
||||
return Stream.of(
|
||||
Arguments.of(null, true),
|
||||
Arguments.of("", true),
|
||||
Arguments.of(" ", true),
|
||||
Arguments.of("application/json", true),
|
||||
Arguments.of("garbage/fhir", false));
|
||||
}
|
||||
|
||||
}
|
|
@ -761,7 +761,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest {
|
|||
mySystemDao.transaction(mySrd, request);
|
||||
fail("");
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertThat(e.getMessage()).contains("resource with match URL \"Patient?");
|
||||
assertThat(e.getMessage()).contains("Patient with match URL \"Patient?");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -641,7 +641,7 @@ public class ResourceProviderDstu2Test extends BaseResourceProviderDstu2Test {
|
|||
//@formatter:on
|
||||
fail("");
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("HTTP 412 Precondition Failed: " + Msg.code(962) + "Failed to DELETE resource with match URL \"Patient?identifier=testDeleteConditionalMultiple\" because this search matched 2 resources", e.getMessage());
|
||||
assertEquals("HTTP 412 Precondition Failed: " + Msg.code(962) + "Failed to DELETE Patient with match URL \"Patient?identifier=testDeleteConditionalMultiple\" because this search matched 2 resources", e.getMessage());
|
||||
}
|
||||
|
||||
// Not deleted yet..
|
||||
|
|
|
@ -1357,7 +1357,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
|
|||
mySystemDao.transaction(mySrd, request);
|
||||
fail("");
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertThat(e.getMessage()).contains("resource with match URL \"Patient?");
|
||||
assertThat(e.getMessage()).contains("Patient with match URL \"Patient?");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1083,7 +1083,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
|
|||
//@formatter:on
|
||||
fail("");
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("HTTP 412 Precondition Failed: " + Msg.code(962) + "Failed to DELETE resource with match URL \"Patient?identifier=testDeleteConditionalMultiple&_format=json\" because this search matched 2 resources", e.getMessage());
|
||||
assertEquals("HTTP 412 Precondition Failed: " + Msg.code(962) + "Failed to DELETE Patient with match URL \"Patient?identifier=testDeleteConditionalMultiple&_format=json\" because this search matched 2 resources", e.getMessage());
|
||||
}
|
||||
|
||||
// Not deleted yet..
|
||||
|
|
|
@ -2548,7 +2548,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
|
|||
mySystemDao.transaction(mySrd, request);
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertThat(e.getMessage()).contains("resource with match URL \"Patient?");
|
||||
assertThat(e.getMessage()).contains("Patient with match URL \"Patient?");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -529,7 +529,7 @@ public class PatchProviderR4Test extends BaseResourceProviderR4Test {
|
|||
assertEquals(412, response.getStatusLine().getStatusCode());
|
||||
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
assertThat(responseString).contains("<OperationOutcome");
|
||||
assertThat(responseString).contains("Failed to PATCH resource with match URL "Patient?active=true" because this search matched 2 resources");
|
||||
assertThat(responseString).contains("Failed to PATCH Patient with match URL "Patient?active=true" because this search matched 2 resources");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1724,7 +1724,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
|||
//@formatter:on
|
||||
fail();
|
||||
} catch (PreconditionFailedException e) {
|
||||
assertEquals("HTTP 412 Precondition Failed: " + Msg.code(962) + "Failed to DELETE resource with match URL \"Patient?identifier=testDeleteConditionalMultiple\" because this search matched 2 resources", e.getMessage());
|
||||
assertEquals("HTTP 412 Precondition Failed: " + Msg.code(962) + "Failed to DELETE Patient with match URL \"Patient?identifier=testDeleteConditionalMultiple\" because this search matched 2 resources", e.getMessage());
|
||||
}
|
||||
|
||||
// Not deleted yet..
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package ca.uhn.fhir.jpa.subscription;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
|
||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||
import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RegexEndpointUrlValidationStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator;
|
||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.SubscriptionChannelTypeValidatorFactory;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
|
@ -29,9 +31,13 @@ import org.mockito.Mock;
|
|||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static ca.uhn.fhir.jpa.subscription.submit.interceptor.validator.RestHookChannelValidator.IEndpointUrlValidationStrategy;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
|
@ -57,6 +63,9 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
|
||||
@Mock
|
||||
private SubscriptionSettings mySubscriptionSettings;
|
||||
|
||||
private SubscriptionChannelTypeValidatorFactory mySubscriptionChannelTypeValidatorFactory;
|
||||
|
||||
private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
|
||||
|
||||
@BeforeEach
|
||||
|
@ -69,6 +78,11 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
mySvc.setFhirContext(myCtx);
|
||||
mySvc.setSubscriptionSettingsForUnitTest(mySubscriptionSettings);
|
||||
mySvc.setRequestPartitionHelperSvcForUnitTest(myRequestPartitionHelperSvc);
|
||||
|
||||
IEndpointUrlValidationStrategy iEndpointUrlValidationStrategy = new RegexEndpointUrlValidationStrategy(SubscriptionSettings.DEFAULT_RESTHOOK_ENDPOINTURL_VALIDATION_REGEX);
|
||||
mySubscriptionChannelTypeValidatorFactory = new SubscriptionChannelTypeValidatorFactory(List.of(new RestHookChannelValidator(iEndpointUrlValidationStrategy)));
|
||||
|
||||
mySvc.setSubscriptionChannelTypeValidatorFactoryForUnitTest(mySubscriptionChannelTypeValidatorFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -85,7 +99,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_RestHook_Populated() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -99,7 +113,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_RestHook_ResourceTypeNotSupported() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(false);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(false);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -118,7 +132,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_RestHook_MultitypeResourceTypeNotSupported() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(false);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(false);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -137,7 +151,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_RestHook_NoEndpoint() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -156,7 +170,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_RestHook_NoType() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -174,7 +188,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_RestHook_NoPayload() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -203,7 +217,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_Cross_Partition_Subscription() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
when(mySubscriptionSettings.isCrossPartitionSubscriptionEnabled()).thenReturn(true);
|
||||
when(myRequestPartitionHelperSvc.determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class), eq("Subscription"))).thenReturn(RequestPartitionId.defaultPartition());
|
||||
|
||||
|
@ -222,8 +236,8 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
// is invalid
|
||||
assertDoesNotThrow(() -> mySvc.resourcePreCreate(subscription, requestDetails, null));
|
||||
Mockito.verify(mySubscriptionSettings, times(1)).isCrossPartitionSubscriptionEnabled();
|
||||
Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported(eq("Patient"));
|
||||
Mockito.verify(myRequestPartitionHelperSvc, times(1)).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class), eq("Subscription"));
|
||||
Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported("Patient");
|
||||
Mockito.verify(myRequestPartitionHelperSvc, times(1)).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Subscription.class),eq("Subscription"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -275,7 +289,7 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
|
||||
@Test
|
||||
public void testValidate_Cross_Partition_System_Subscription_Without_Setting() {
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||
|
@ -292,14 +306,14 @@ public class SubscriptionValidatingInterceptorTest {
|
|||
// is invalid
|
||||
mySvc.resourcePreCreate(subscription, requestDetails, null);
|
||||
Mockito.verify(mySubscriptionSettings, never()).isCrossPartitionSubscriptionEnabled();
|
||||
Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported(eq("Patient"));
|
||||
Mockito.verify(myRequestPartitionHelperSvc, never()).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Patient.class), eq("Patient"));
|
||||
Mockito.verify(myDaoRegistry, times(1)).isResourceTypeSupported("Patient");
|
||||
Mockito.verify(myRequestPartitionHelperSvc, never()).determineCreatePartitionForRequest(isA(RequestDetails.class), isA(Patient.class),eq("Patient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubscriptionUpdate() {
|
||||
// setup
|
||||
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||
when(myDaoRegistry.isResourceTypeSupported("Patient")).thenReturn(true);
|
||||
when(mySubscriptionSettings.isCrossPartitionSubscriptionEnabled()).thenReturn(true);
|
||||
lenient()
|
||||
.when(myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead(isA(RequestDetails.class), isA(String.class), isA(IIdType.class)))
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.QuestionnaireResponse;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ResourceCompartmentUtilTest extends BaseJpaR4Test {
|
||||
|
||||
public static final String FAMILY = "TestFamily";
|
||||
private static final String ORG_NAME = "Test Organization";
|
||||
@Autowired
|
||||
ISearchParamExtractor mySearchParamExtractor;
|
||||
|
||||
@Test
|
||||
public void testMultiCompartment() {
|
||||
IIdType pid = createPatient(withFamily(FAMILY));
|
||||
QuestionnaireResponse qr = new QuestionnaireResponse();
|
||||
qr.setSubject(new Reference(pid));
|
||||
IIdType oid = createOrganization(withName(ORG_NAME));
|
||||
qr.setAuthor(new Reference(oid));
|
||||
|
||||
Optional<String> result = ResourceCompartmentUtil.getPatientCompartmentIdentity(qr, myFhirContext, mySearchParamExtractor);
|
||||
// red-green: before the bug fix, this returned the org id because "author" is alphabetically before "patient"
|
||||
assertThat(result)
|
||||
.isPresent()
|
||||
.hasValue(pid.getIdPart());
|
||||
}
|
||||
}
|
|
@ -189,15 +189,7 @@
|
|||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipRemoteStaging>true</skipRemoteStaging>
|
||||
<skipLocalStaging>true</skipLocalStaging>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
*/
|
||||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum ConsentOperationStatusEnum {
|
||||
|
||||
/**
|
||||
|
@ -39,4 +41,71 @@ public enum ConsentOperationStatusEnum {
|
|||
* counting/caching methods)
|
||||
*/
|
||||
AUTHORIZED,
|
||||
;
|
||||
|
||||
/**
|
||||
* Assigns ordinals to the verdicts by strength:
|
||||
* REJECT > AUTHORIZED > PROCEED.
|
||||
* @return 2/1/0 for REJECT/AUTHORIZED/PROCEED
|
||||
*/
|
||||
int getPrecedence() {
|
||||
switch (this) {
|
||||
case REJECT:
|
||||
return 2;
|
||||
case AUTHORIZED:
|
||||
return 1;
|
||||
case PROCEED:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @return the first decisive verdict, or PROCEED when empty or all PROCEED.
|
||||
*/
|
||||
public static ConsentOperationStatusEnum serialEvaluate(Stream<ConsentOperationStatusEnum> theVoteStream) {
|
||||
return theVoteStream.filter(verdict -> PROCEED != verdict).findFirst().orElse(PROCEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict.
|
||||
*
|
||||
* @param theNextVerdict the next verdict to consider
|
||||
* @return the combined verdict
|
||||
*/
|
||||
public ConsentOperationStatusEnum serialReduce(ConsentOperationStatusEnum theNextVerdict) {
|
||||
if (this != PROCEED) {
|
||||
return this;
|
||||
} else {
|
||||
return theNextVerdict;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate all verdicts together, allowing any to veto (i.e. REJECT) the operation.
|
||||
* <ul>
|
||||
* <li>If any vote is REJECT, then the result is REJECT.
|
||||
* <li>If no vote is REJECT, and any vote is AUTHORIZED, then the result is AUTHORIZED.
|
||||
* <li>If no vote is REJECT or AUTHORIZED, the result is PROCEED.
|
||||
* </ul>
|
||||
*
|
||||
* @return REJECT if any reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED if empty or all PROCEED
|
||||
*/
|
||||
public static ConsentOperationStatusEnum parallelEvaluate(Stream<ConsentOperationStatusEnum> theVoteStream) {
|
||||
return theVoteStream.reduce(PROCEED, ConsentOperationStatusEnum::parallelReduce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate two verdicts together, allowing either to veto (i.e. REJECT) the operation.
|
||||
*
|
||||
* @return REJECT if either reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED otherwise
|
||||
*/
|
||||
public ConsentOperationStatusEnum parallelReduce(ConsentOperationStatusEnum theNextVerdict) {
|
||||
if (theNextVerdict.getPrecedence() > this.getPrecedence()) {
|
||||
return theNextVerdict;
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor.consent;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.AUTHORIZED;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.PROCEED;
|
||||
import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.REJECT;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ConsentOperationStatusEnumTest {
|
||||
|
||||
/**
|
||||
* With "serial" evaluation, the first non-PROCEED verdict wins.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT REJECT REJECT , REJECT
|
||||
REJECT REJECT PROCEED , REJECT
|
||||
REJECT REJECT AUTHORIZED, REJECT
|
||||
REJECT PROCEED REJECT , REJECT
|
||||
REJECT PROCEED PROCEED , REJECT
|
||||
REJECT PROCEED AUTHORIZED, REJECT
|
||||
REJECT AUTHORIZED REJECT , REJECT
|
||||
REJECT AUTHORIZED PROCEED , REJECT
|
||||
REJECT AUTHORIZED AUTHORIZED, REJECT
|
||||
PROCEED REJECT REJECT , REJECT
|
||||
PROCEED REJECT PROCEED , REJECT
|
||||
PROCEED REJECT AUTHORIZED, REJECT
|
||||
PROCEED PROCEED REJECT , REJECT
|
||||
PROCEED PROCEED PROCEED , PROCEED
|
||||
PROCEED PROCEED AUTHORIZED, AUTHORIZED
|
||||
PROCEED AUTHORIZED REJECT , AUTHORIZED
|
||||
PROCEED AUTHORIZED PROCEED , AUTHORIZED
|
||||
PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED REJECT REJECT , AUTHORIZED
|
||||
AUTHORIZED REJECT PROCEED , AUTHORIZED
|
||||
AUTHORIZED REJECT AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED PROCEED REJECT , AUTHORIZED
|
||||
AUTHORIZED PROCEED PROCEED , AUTHORIZED
|
||||
AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED REJECT , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testSerialEvaluation_choosesFirstVerdict(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
// given
|
||||
Stream<ConsentOperationStatusEnum> consentOperationStatusEnumStream = Arrays.stream(theInput.split(" +"))
|
||||
.map(String::trim)
|
||||
.map(ConsentOperationStatusEnum::valueOf);
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = ConsentOperationStatusEnum.serialEvaluate(consentOperationStatusEnumStream);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT , REJECT , REJECT
|
||||
REJECT , PROCEED , REJECT
|
||||
REJECT , AUTHORIZED, REJECT
|
||||
AUTHORIZED, REJECT , AUTHORIZED
|
||||
AUTHORIZED, PROCEED , AUTHORIZED
|
||||
AUTHORIZED, AUTHORIZED, AUTHORIZED
|
||||
PROCEED , REJECT , REJECT
|
||||
PROCEED , PROCEED , PROCEED
|
||||
PROCEED , AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testSerialReduction_choosesFirstVerdict(ConsentOperationStatusEnum theFirst, ConsentOperationStatusEnum theSecond, ConsentOperationStatusEnum theExpectedResult) {
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = theFirst.serialReduce(theSecond);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* With "parallel" evaluation, the "strongest" verdict wins.
|
||||
* REJECT > AUTHORIZED > PROCEED.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
REJECT REJECT REJECT , REJECT
|
||||
REJECT REJECT PROCEED , REJECT
|
||||
REJECT REJECT AUTHORIZED, REJECT
|
||||
REJECT PROCEED REJECT , REJECT
|
||||
REJECT PROCEED PROCEED , REJECT
|
||||
REJECT PROCEED AUTHORIZED, REJECT
|
||||
REJECT AUTHORIZED REJECT , REJECT
|
||||
REJECT AUTHORIZED PROCEED , REJECT
|
||||
REJECT AUTHORIZED AUTHORIZED, REJECT
|
||||
PROCEED REJECT REJECT , REJECT
|
||||
PROCEED REJECT PROCEED , REJECT
|
||||
PROCEED REJECT AUTHORIZED, REJECT
|
||||
PROCEED PROCEED REJECT , REJECT
|
||||
PROCEED PROCEED PROCEED , PROCEED
|
||||
PROCEED PROCEED AUTHORIZED, AUTHORIZED
|
||||
PROCEED AUTHORIZED REJECT , REJECT
|
||||
PROCEED AUTHORIZED PROCEED , AUTHORIZED
|
||||
PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED REJECT REJECT , REJECT
|
||||
AUTHORIZED REJECT PROCEED , REJECT
|
||||
AUTHORIZED REJECT AUTHORIZED, REJECT
|
||||
AUTHORIZED PROCEED REJECT , REJECT
|
||||
AUTHORIZED PROCEED PROCEED , AUTHORIZED
|
||||
AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED REJECT , REJECT
|
||||
AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED
|
||||
AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED
|
||||
""")
|
||||
void testParallelReduction_strongestVerdictWins(String theInput, ConsentOperationStatusEnum theExpectedResult) {
|
||||
// given
|
||||
Stream<ConsentOperationStatusEnum> consentOperationStatusEnumStream = Arrays.stream(theInput.split(" +"))
|
||||
.map(String::trim)
|
||||
.map(ConsentOperationStatusEnum::valueOf);
|
||||
|
||||
// when
|
||||
ConsentOperationStatusEnum result = ConsentOperationStatusEnum.parallelEvaluate(consentOperationStatusEnumStream);
|
||||
|
||||
assertEquals(theExpectedResult, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStrengthOrder() {
|
||||
assertTrue(REJECT.getPrecedence() > AUTHORIZED.getPrecedence());
|
||||
assertTrue(AUTHORIZED.getPrecedence() > PROCEED.getPrecedence());
|
||||
}
|
||||
|
||||
}
|
|
@ -34,10 +34,10 @@
|
|||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
|
@ -85,19 +85,16 @@ public class JdbcUtils {
|
|||
try {
|
||||
metadata = connection.getMetaData();
|
||||
|
||||
ResultSet indexes = getIndexInfo(theTableName, connection, metadata, false);
|
||||
Set<String> indexNames = new HashSet<>();
|
||||
while (indexes.next()) {
|
||||
ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0));
|
||||
String indexName = indexes.getString("INDEX_NAME");
|
||||
indexNames.add(indexName);
|
||||
}
|
||||
|
||||
indexes = getIndexInfo(theTableName, connection, metadata, true);
|
||||
while (indexes.next()) {
|
||||
ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0));
|
||||
String indexName = indexes.getString("INDEX_NAME");
|
||||
indexNames.add(indexName);
|
||||
for (boolean unique : Set.of(false, true)) {
|
||||
try (ResultSet indexes = getIndexInfo(theTableName, connection, metadata, unique)) {
|
||||
while (indexes.next()) {
|
||||
ourLog.debug("*** Next index: {}", new ColumnMapRowMapper().mapRow(indexes, 0));
|
||||
String indexName = indexes.getString("INDEX_NAME");
|
||||
indexNames.add(indexName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indexNames = indexNames.stream()
|
||||
|
@ -124,13 +121,14 @@ public class JdbcUtils {
|
|||
DatabaseMetaData metadata;
|
||||
try {
|
||||
metadata = connection.getMetaData();
|
||||
ResultSet indexes = getIndexInfo(theTableName, connection, metadata, false);
|
||||
try (ResultSet indexes = getIndexInfo(theTableName, connection, metadata, false)) {
|
||||
|
||||
while (indexes.next()) {
|
||||
String indexName = indexes.getString("INDEX_NAME");
|
||||
if (theIndexName.equalsIgnoreCase(indexName)) {
|
||||
boolean nonUnique = indexes.getBoolean("NON_UNIQUE");
|
||||
return !nonUnique;
|
||||
while (indexes.next()) {
|
||||
String indexName = indexes.getString("INDEX_NAME");
|
||||
if (theIndexName.equalsIgnoreCase(indexName)) {
|
||||
boolean nonUnique = indexes.getBoolean("NON_UNIQUE");
|
||||
return !nonUnique;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,65 +169,69 @@ public class JdbcUtils {
|
|||
metadata = connection.getMetaData();
|
||||
String catalog = connection.getCatalog();
|
||||
String schema = connection.getSchema();
|
||||
ResultSet indexes =
|
||||
metadata.getColumns(catalog, schema, massageIdentifier(metadata, theTableName), null);
|
||||
try (ResultSet indexes =
|
||||
metadata.getColumns(catalog, schema, massageIdentifier(metadata, theTableName), null)) {
|
||||
|
||||
while (indexes.next()) {
|
||||
while (indexes.next()) {
|
||||
|
||||
String tableName = indexes.getString("TABLE_NAME").toUpperCase(Locale.US);
|
||||
if (!theTableName.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
String columnName = indexes.getString("COLUMN_NAME").toUpperCase(Locale.US);
|
||||
if (!theColumnName.equalsIgnoreCase(columnName)) {
|
||||
continue;
|
||||
}
|
||||
String tableName = indexes.getString("TABLE_NAME").toUpperCase(Locale.US);
|
||||
if (!theTableName.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
String columnName = indexes.getString("COLUMN_NAME").toUpperCase(Locale.US);
|
||||
if (!theColumnName.equalsIgnoreCase(columnName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int dataType = indexes.getInt("DATA_TYPE");
|
||||
Long length = indexes.getLong("COLUMN_SIZE");
|
||||
switch (dataType) {
|
||||
case Types.LONGVARCHAR:
|
||||
return new ColumnType(ColumnTypeEnum.TEXT, length);
|
||||
case Types.BIT:
|
||||
case Types.BOOLEAN:
|
||||
return new ColumnType(ColumnTypeEnum.BOOLEAN, length);
|
||||
case Types.VARCHAR:
|
||||
return new ColumnType(ColumnTypeEnum.STRING, length);
|
||||
case Types.NUMERIC:
|
||||
case Types.BIGINT:
|
||||
case Types.DECIMAL:
|
||||
return new ColumnType(ColumnTypeEnum.LONG, length);
|
||||
case Types.INTEGER:
|
||||
return new ColumnType(ColumnTypeEnum.INT, length);
|
||||
case Types.TIMESTAMP:
|
||||
case Types.TIMESTAMP_WITH_TIMEZONE:
|
||||
return new ColumnType(ColumnTypeEnum.DATE_TIMESTAMP, length);
|
||||
case Types.BLOB:
|
||||
return new ColumnType(ColumnTypeEnum.BLOB, length);
|
||||
case Types.LONGVARBINARY:
|
||||
return new ColumnType(ColumnTypeEnum.BINARY, length);
|
||||
case Types.VARBINARY:
|
||||
if (DriverTypeEnum.MSSQL_2012.equals(theConnectionProperties.getDriverType())) {
|
||||
// MS SQLServer seems to be mapping BLOB to VARBINARY under the covers, so we need
|
||||
// to reverse that mapping
|
||||
int dataType = indexes.getInt("DATA_TYPE");
|
||||
Long length = indexes.getLong("COLUMN_SIZE");
|
||||
switch (dataType) {
|
||||
case Types.LONGVARCHAR:
|
||||
return new ColumnType(ColumnTypeEnum.TEXT, length);
|
||||
case Types.BIT:
|
||||
case Types.BOOLEAN:
|
||||
return new ColumnType(ColumnTypeEnum.BOOLEAN, length);
|
||||
case Types.VARCHAR:
|
||||
return new ColumnType(ColumnTypeEnum.STRING, length);
|
||||
case Types.NUMERIC:
|
||||
case Types.BIGINT:
|
||||
case Types.DECIMAL:
|
||||
return new ColumnType(ColumnTypeEnum.LONG, length);
|
||||
case Types.INTEGER:
|
||||
return new ColumnType(ColumnTypeEnum.INT, length);
|
||||
case Types.TIMESTAMP:
|
||||
case Types.TIMESTAMP_WITH_TIMEZONE:
|
||||
return new ColumnType(ColumnTypeEnum.DATE_TIMESTAMP, length);
|
||||
case Types.BLOB:
|
||||
return new ColumnType(ColumnTypeEnum.BLOB, length);
|
||||
case Types.LONGVARBINARY:
|
||||
return new ColumnType(ColumnTypeEnum.BINARY, length);
|
||||
case Types.VARBINARY:
|
||||
if (DriverTypeEnum.MSSQL_2012.equals(theConnectionProperties.getDriverType())) {
|
||||
// MS SQLServer seems to be mapping BLOB to VARBINARY under the covers,
|
||||
// so we need to reverse that mapping
|
||||
return new ColumnType(ColumnTypeEnum.BLOB, length);
|
||||
|
||||
} else {
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
Msg.code(33) + "Don't know how to handle datatype " + dataType
|
||||
+ " for column " + theColumnName
|
||||
+ " on table " + theTableName);
|
||||
}
|
||||
case Types.CLOB:
|
||||
return new ColumnType(ColumnTypeEnum.CLOB, length);
|
||||
case Types.DOUBLE:
|
||||
return new ColumnType(ColumnTypeEnum.DOUBLE, length);
|
||||
case Types.FLOAT:
|
||||
return new ColumnType(ColumnTypeEnum.FLOAT, length);
|
||||
case Types.TINYINT:
|
||||
return new ColumnType(ColumnTypeEnum.TINYINT, length);
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
Msg.code(33) + "Don't know how to handle datatype " + dataType
|
||||
+ " for column " + theColumnName + " on table " + theTableName);
|
||||
}
|
||||
case Types.CLOB:
|
||||
return new ColumnType(ColumnTypeEnum.CLOB, length);
|
||||
case Types.DOUBLE:
|
||||
return new ColumnType(ColumnTypeEnum.DOUBLE, length);
|
||||
case Types.FLOAT:
|
||||
return new ColumnType(ColumnTypeEnum.FLOAT, length);
|
||||
case Types.TINYINT:
|
||||
return new ColumnType(ColumnTypeEnum.TINYINT, length);
|
||||
default:
|
||||
throw new IllegalArgumentException(Msg.code(34) + "Don't know how to handle datatype "
|
||||
+ dataType + " for column " + theColumnName + " on table " + theTableName);
|
||||
Msg.code(34) + "Don't know how to handle datatype " + dataType
|
||||
+ " for column " + theColumnName
|
||||
+ " on table " + theTableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,13 +276,13 @@ public class JdbcUtils {
|
|||
|
||||
Set<String> fkNames = new HashSet<>();
|
||||
for (String nextParentTable : parentTables) {
|
||||
ResultSet indexes = metadata.getCrossReference(
|
||||
catalog, schema, nextParentTable, catalog, schema, foreignTable);
|
||||
|
||||
while (indexes.next()) {
|
||||
String fkName = indexes.getString("FK_NAME");
|
||||
fkName = fkName.toUpperCase(Locale.US);
|
||||
fkNames.add(fkName);
|
||||
try (ResultSet indexes = metadata.getCrossReference(
|
||||
catalog, schema, nextParentTable, catalog, schema, foreignTable)) {
|
||||
while (indexes.next()) {
|
||||
String fkName = indexes.getString("FK_NAME");
|
||||
fkName = fkName.toUpperCase(Locale.US);
|
||||
fkNames.add(fkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,14 +319,14 @@ public class JdbcUtils {
|
|||
|
||||
Set<String> fkNames = new HashSet<>();
|
||||
for (String nextParentTable : parentTables) {
|
||||
ResultSet indexes = metadata.getCrossReference(
|
||||
catalog, schema, nextParentTable, catalog, schema, foreignTable);
|
||||
|
||||
while (indexes.next()) {
|
||||
if (theForeignKeyColumn.equals(indexes.getString("FKCOLUMN_NAME"))) {
|
||||
String fkName = indexes.getString("FK_NAME");
|
||||
fkName = fkName.toUpperCase(Locale.US);
|
||||
fkNames.add(fkName);
|
||||
try (ResultSet indexes = metadata.getCrossReference(
|
||||
catalog, schema, nextParentTable, catalog, schema, foreignTable)) {
|
||||
while (indexes.next()) {
|
||||
if (theForeignKeyColumn.equals(indexes.getString("FKCOLUMN_NAME"))) {
|
||||
String fkName = indexes.getString("FK_NAME");
|
||||
fkName = fkName.toUpperCase(Locale.US);
|
||||
fkNames.add(fkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,22 +350,24 @@ public class JdbcUtils {
|
|||
DatabaseMetaData metadata;
|
||||
try {
|
||||
metadata = connection.getMetaData();
|
||||
ResultSet indexes = metadata.getColumns(
|
||||
LinkedCaseInsensitiveMap<String> columnNames = new LinkedCaseInsensitiveMap<>();
|
||||
|
||||
try (ResultSet indexes = metadata.getColumns(
|
||||
connection.getCatalog(),
|
||||
connection.getSchema(),
|
||||
massageIdentifier(metadata, theTableName),
|
||||
null);
|
||||
null)) {
|
||||
|
||||
LinkedCaseInsensitiveMap<String> columnNames = new LinkedCaseInsensitiveMap<>();
|
||||
while (indexes.next()) {
|
||||
String tableName = indexes.getString("TABLE_NAME").toUpperCase(Locale.US);
|
||||
if (!theTableName.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
while (indexes.next()) {
|
||||
String tableName = indexes.getString("TABLE_NAME").toUpperCase(Locale.US);
|
||||
if (!theTableName.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String columnName = indexes.getString("COLUMN_NAME");
|
||||
columnName = columnName.toUpperCase(Locale.US);
|
||||
columnNames.put(columnName, columnName);
|
||||
}
|
||||
|
||||
String columnName = indexes.getString("COLUMN_NAME");
|
||||
columnName = columnName.toUpperCase(Locale.US);
|
||||
columnNames.put(columnName, columnName);
|
||||
}
|
||||
|
||||
return columnNames.keySet();
|
||||
|
@ -391,6 +395,7 @@ public class JdbcUtils {
|
|||
SequenceInformationExtractor sequenceInformationExtractor =
|
||||
dialect.getSequenceInformationExtractor();
|
||||
ExtractionContext extractionContext = new ExtractionContext.EmptyExtractionContext() {
|
||||
|
||||
@Override
|
||||
public Connection getJdbcConnection() {
|
||||
return connection;
|
||||
|
@ -404,6 +409,7 @@ public class JdbcUtils {
|
|||
@Override
|
||||
public JdbcEnvironment getJdbcEnvironment() {
|
||||
return new JdbcEnvironment() {
|
||||
|
||||
@Override
|
||||
public Dialect getDialect() {
|
||||
return dialect;
|
||||
|
@ -480,22 +486,25 @@ public class JdbcUtils {
|
|||
DatabaseMetaData metadata;
|
||||
try {
|
||||
metadata = connection.getMetaData();
|
||||
ResultSet tables = metadata.getTables(connection.getCatalog(), connection.getSchema(), null, null);
|
||||
|
||||
Set<String> columnNames = new HashSet<>();
|
||||
while (tables.next()) {
|
||||
String tableName = tables.getString("TABLE_NAME");
|
||||
tableName = tableName.toUpperCase(Locale.US);
|
||||
|
||||
String tableType = tables.getString("TABLE_TYPE");
|
||||
if ("SYSTEM TABLE".equalsIgnoreCase(tableType)) {
|
||||
continue;
|
||||
}
|
||||
if (SchemaMigrator.HAPI_FHIR_MIGRATION_TABLENAME.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
try (ResultSet tables =
|
||||
metadata.getTables(connection.getCatalog(), connection.getSchema(), null, null)) {
|
||||
|
||||
columnNames.add(tableName);
|
||||
while (tables.next()) {
|
||||
String tableName = tables.getString("TABLE_NAME");
|
||||
tableName = tableName.toUpperCase(Locale.US);
|
||||
|
||||
String tableType = tables.getString("TABLE_TYPE");
|
||||
if ("SYSTEM TABLE".equalsIgnoreCase(tableType)) {
|
||||
continue;
|
||||
}
|
||||
if (SchemaMigrator.HAPI_FHIR_MIGRATION_TABLENAME.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
columnNames.add(tableName);
|
||||
}
|
||||
}
|
||||
|
||||
return columnNames;
|
||||
|
@ -516,26 +525,27 @@ public class JdbcUtils {
|
|||
DatabaseMetaData metadata;
|
||||
try {
|
||||
metadata = connection.getMetaData();
|
||||
ResultSet tables = metadata.getColumns(
|
||||
try (ResultSet tables = metadata.getColumns(
|
||||
connection.getCatalog(),
|
||||
connection.getSchema(),
|
||||
massageIdentifier(metadata, theTableName),
|
||||
null);
|
||||
null)) {
|
||||
|
||||
while (tables.next()) {
|
||||
String tableName = tables.getString("TABLE_NAME").toUpperCase(Locale.US);
|
||||
if (!theTableName.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
while (tables.next()) {
|
||||
String tableName = tables.getString("TABLE_NAME").toUpperCase(Locale.US);
|
||||
if (!theTableName.equalsIgnoreCase(tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (theColumnName.equalsIgnoreCase(tables.getString("COLUMN_NAME"))) {
|
||||
String nullable = tables.getString("IS_NULLABLE");
|
||||
if ("YES".equalsIgnoreCase(nullable)) {
|
||||
return true;
|
||||
} else if ("NO".equalsIgnoreCase(nullable)) {
|
||||
return false;
|
||||
} else {
|
||||
throw new IllegalStateException(Msg.code(41) + "Unknown nullable: " + nullable);
|
||||
if (theColumnName.equalsIgnoreCase(tables.getString("COLUMN_NAME"))) {
|
||||
String nullable = tables.getString("IS_NULLABLE");
|
||||
if ("YES".equalsIgnoreCase(nullable)) {
|
||||
return true;
|
||||
} else if ("NO".equalsIgnoreCase(nullable)) {
|
||||
return false;
|
||||
} else {
|
||||
throw new IllegalStateException(Msg.code(41) + "Unknown nullable: " + nullable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,6 +122,7 @@ public abstract class BaseStorageResourceDao<T extends IBaseResource> extends Ba
|
|||
BaseStorageDao.class,
|
||||
"transactionOperationWithMultipleMatchFailure",
|
||||
"PATCH",
|
||||
getResourceName(),
|
||||
theConditionalUrl,
|
||||
match.size());
|
||||
throw new PreconditionFailedException(Msg.code(972) + msg);
|
||||
|
|
|
@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
|
|||
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.IdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
@ -92,15 +93,15 @@ public class PatientIdPartitionInterceptor {
|
|||
|
||||
Optional<String> oCompartmentIdentity;
|
||||
if (resourceDef.getName().equals("Patient")) {
|
||||
oCompartmentIdentity =
|
||||
Optional.ofNullable(theResource.getIdElement().getIdPart());
|
||||
if (oCompartmentIdentity.isEmpty()) {
|
||||
IIdType idElement = theResource.getIdElement();
|
||||
oCompartmentIdentity = Optional.ofNullable(idElement.getIdPart());
|
||||
if (idElement.isUuid() || oCompartmentIdentity.isEmpty()) {
|
||||
throw new MethodNotAllowedException(
|
||||
Msg.code(1321) + "Patient resource IDs must be client-assigned in patient compartment mode");
|
||||
}
|
||||
} else {
|
||||
oCompartmentIdentity =
|
||||
ResourceCompartmentUtil.getResourceCompartment(theResource, compartmentSps, mySearchParamExtractor);
|
||||
oCompartmentIdentity = ResourceCompartmentUtil.getResourceCompartment(
|
||||
"Patient", theResource, compartmentSps, mySearchParamExtractor);
|
||||
}
|
||||
|
||||
return oCompartmentIdentity
|
||||
|
|
|
@ -45,8 +45,8 @@ public class ResourceCompartmentUtil {
|
|||
/**
|
||||
* Extract, if exists, the patient compartment identity of the received resource.
|
||||
* It must be invoked in patient compartment mode.
|
||||
* @param theResource the resource to which extract the patient compartment identity
|
||||
* @param theFhirContext the active FhirContext
|
||||
* @param theResource the resource to which extract the patient compartment identity
|
||||
* @param theFhirContext the active FhirContext
|
||||
* @param theSearchParamExtractor the configured search parameter extractor
|
||||
* @return the optional patient compartment identifier
|
||||
* @throws MethodNotAllowedException if received resource is of type "Patient" and ID is not assigned.
|
||||
|
@ -69,20 +69,23 @@ public class ResourceCompartmentUtil {
|
|||
return Optional.of(compartmentIdentity);
|
||||
}
|
||||
|
||||
return getResourceCompartment(theResource, patientCompartmentSps, theSearchParamExtractor);
|
||||
return getResourceCompartment("Patient", theResource, patientCompartmentSps, theSearchParamExtractor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and returns an optional compartment of the received resource
|
||||
* @param theResource source resource which compartment is extracted
|
||||
* @param theCompartmentSps the RuntimeSearchParam list involving the searched compartment
|
||||
* @param theCompartmentName the name of the compartment
|
||||
* @param theResource source resource which compartment is extracted
|
||||
* @param theCompartmentSps the RuntimeSearchParam list involving the searched compartment
|
||||
* @param mySearchParamExtractor the ISearchParamExtractor to be used to extract the parameter values
|
||||
* @return optional compartment of the received resource
|
||||
*/
|
||||
public static Optional<String> getResourceCompartment(
|
||||
String theCompartmentName,
|
||||
IBaseResource theResource,
|
||||
List<RuntimeSearchParam> theCompartmentSps,
|
||||
ISearchParamExtractor mySearchParamExtractor) {
|
||||
// TODO KHS consolidate with FhirTerser.getCompartmentOwnersForResource()
|
||||
return theCompartmentSps.stream()
|
||||
.flatMap(param -> Arrays.stream(BaseSearchParamExtractor.splitPathsR4(param.getPath())))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
|
@ -94,7 +97,10 @@ public class ResourceCompartmentUtil {
|
|||
.filter(t -> t instanceof IBaseReference)
|
||||
.map(t -> (IBaseReference) t)
|
||||
.map(t -> t.getReferenceElement().getValue())
|
||||
.map(t -> new IdType(t).getIdPart())
|
||||
.map(IdType::new)
|
||||
.filter(t -> theCompartmentName.equals(
|
||||
t.getResourceType())) // assume the compartment name matches the resource type
|
||||
.map(IdType::getIdPart)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst();
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ class ResourceCompartmentUtilTest {
|
|||
when(mySearchParamExtractor.getPathValueExtractor(myResource, "Observation.subject"))
|
||||
.thenReturn(() -> List.of(new Reference("Patient/P01")));
|
||||
|
||||
Optional<String> oCompartment = ResourceCompartmentUtil.getResourceCompartment(
|
||||
Optional<String> oCompartment = ResourceCompartmentUtil.getResourceCompartment("Patient",
|
||||
myResource, myCompartmentSearchParams, mySearchParamExtractor);
|
||||
|
||||
assertThat(oCompartment).isPresent();
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package ca.uhn.test.junit;
|
||||
|
||||
import org.junit.jupiter.api.extension.BeforeAllCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.api.extension.ParameterContext;
|
||||
import org.junit.jupiter.api.extension.ParameterResolutionException;
|
||||
import org.junit.jupiter.api.extension.ParameterResolver;
|
||||
import org.junit.platform.commons.util.ExceptionUtils;
|
||||
import org.junit.platform.commons.util.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields;
|
||||
import static org.junit.platform.commons.util.ReflectionUtils.makeAccessible;
|
||||
|
||||
/**
|
||||
* Register any field annotated with @{@link JunitFieldProvider} as a parameter provider, matching parameters by type.
|
||||
* Can also be used directly via @RegisterExtension, using values passed to the constructor.
|
||||
*/
|
||||
public class JunitFieldParameterProviderExtension implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
|
||||
|
||||
Set<Object> myValues = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Junit constructor for @{@link org.junit.jupiter.api.extension.ExtendWith}
|
||||
*/
|
||||
public JunitFieldParameterProviderExtension() {}
|
||||
|
||||
/**
|
||||
* Used for explicit registration.
|
||||
* @param theValues the values to register as provided junit parameters.
|
||||
*/
|
||||
public JunitFieldParameterProviderExtension(Object ...theValues) {
|
||||
Collections.addAll(myValues, theValues);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Do we have a value matching the parameter type?
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
|
||||
Class<?> paramType = parameterContext.getParameter().getType();
|
||||
return myValues.stream().anyMatch(v->paramType.isAssignableFrom(v.getClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first value matching the parameter type.
|
||||
*/
|
||||
@Override
|
||||
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
|
||||
Class<?> paramType = parameterContext.getParameter().getType();
|
||||
return myValues.stream().filter(v->paramType.isAssignableFrom(v.getClass())).findAny().orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all statics annotated with @JunitFieldProvider.
|
||||
*/
|
||||
@Override
|
||||
public void beforeAll(ExtensionContext context) throws Exception {
|
||||
collectFields(null, context.getRequiredTestClass(), ReflectionUtils::isStatic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect any instance fields annotated with @JunitFieldProvider.
|
||||
*/
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) {
|
||||
context.getRequiredTestInstances().getAllInstances() //
|
||||
.forEach(instance -> collectFields(instance, instance.getClass(), ReflectionUtils::isStatic));
|
||||
}
|
||||
|
||||
private void collectFields(Object testInstance, Class<?> testClass,
|
||||
Predicate<Field> predicate) {
|
||||
|
||||
findAnnotatedFields(testClass, JunitFieldProvider.class, predicate).forEach(field -> {
|
||||
try {
|
||||
makeAccessible(field);
|
||||
myValues.add(field.get(testInstance));
|
||||
}
|
||||
catch (Exception t) {
|
||||
ExceptionUtils.throwAsUncheckedException(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package ca.uhn.test.junit;
|
||||
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Field annotation to register a value for use in JUnit 5 parameter resolution.
|
||||
*/
|
||||
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ExtendWith(JunitFieldParameterProviderExtension.class)
|
||||
public @interface JunitFieldProvider {
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Junit helpers
|
||||
*/
|
||||
package ca.uhn.test.junit;
|
|
@ -0,0 +1,46 @@
|
|||
package ca.uhn.test.junit;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class JunitFieldParameterProviderExtensionTest {
|
||||
/** Verify statics annotated with @{@link JunitFieldProvider} are available to static junit methods. */
|
||||
@Nested
|
||||
class AnnotationRegistration implements IParameterizedTestTemplate {
|
||||
@JunitFieldProvider
|
||||
static final CaseSupplyingFixture ourFixture = new CaseSupplyingFixture();
|
||||
}
|
||||
/** Verify explicit registration of the extension.*/
|
||||
@Nested
|
||||
class ExplicitRegistration implements IParameterizedTestTemplate {
|
||||
static final CaseSupplyingFixture ourFixture = new CaseSupplyingFixture();
|
||||
@RegisterExtension
|
||||
static final JunitFieldParameterProviderExtension ourExtension = new JunitFieldParameterProviderExtension(ourFixture);
|
||||
}
|
||||
|
||||
static class CaseSupplyingFixture {
|
||||
public List<Integer> getCases() {
|
||||
return List.of(1,2,3);
|
||||
}
|
||||
}
|
||||
interface IParameterizedTestTemplate {
|
||||
@ParameterizedTest
|
||||
// intellij complains when a static source requires params
|
||||
@SuppressWarnings("JUnitMalformedDeclaration")
|
||||
@MethodSource("testCaseSource")
|
||||
default void testStaticFactoryBound(int theTestCase) {
|
||||
// given
|
||||
assertTrue(theTestCase > 0);
|
||||
}
|
||||
|
||||
static List<Integer> testCaseSource(CaseSupplyingFixture theFixture) {
|
||||
return theFixture.getCases();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -244,10 +244,10 @@
|
|||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
<skipNexusStagingDeployMojo>false</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
|
|
@ -476,10 +476,10 @@
|
|||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
|
41
pom.xml
41
pom.xml
|
@ -919,15 +919,25 @@
|
|||
<id>adriennesox</id>
|
||||
<name>Adrienne Sox</name>
|
||||
<organization>Galileo, Inc.</organization>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>melihaydogd</id>
|
||||
<name>Ahmet Melih Aydoğdu</name>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>alexrkopp</id>
|
||||
<name>Alex Kopp</name>
|
||||
</developer>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>melihaydogd</id>
|
||||
<name>Ahmet Melih Aydoğdu</name>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>alexrkopp</id>
|
||||
<name>Alex Kopp</name>
|
||||
<organization>athenahealth</organization>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>acoteathn</id>
|
||||
<name>Alex Cote</name>
|
||||
<organization>athenahealth</organization>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>plchldr</id>
|
||||
<name>Jonas Beyer</name>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<licenses>
|
||||
|
@ -1031,7 +1041,7 @@
|
|||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<ebay_cors_filter_version>1.0.1</ebay_cors_filter_version>
|
||||
<elastic_apm_version>1.44.0</elastic_apm_version>
|
||||
<elasticsearch_version>8.11.1</elasticsearch_version>
|
||||
<elasticsearch_version>8.14.3</elasticsearch_version>
|
||||
<ucum_version>1.0.8</ucum_version>
|
||||
|
||||
<!-- Clinical Reasoning & CQL Support -->
|
||||
|
@ -1833,7 +1843,7 @@
|
|||
<dependency>
|
||||
<groupId>org.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>3.9.0</version>
|
||||
<version>3.11.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<!-- Don't let HTMLUnit bring in Jetty 9 -->
|
||||
|
@ -2180,7 +2190,7 @@
|
|||
<dependency>
|
||||
<groupId>org.webjars.bower</groupId>
|
||||
<artifactId>moment</artifactId>
|
||||
<version>2.27.0</version>
|
||||
<version>2.29.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webjars.npm</groupId>
|
||||
|
@ -2430,9 +2440,9 @@
|
|||
<version>3.1.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>2.8.2</version>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.7.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -3243,7 +3253,6 @@
|
|||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.6.13</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
|
|
|
@ -102,8 +102,7 @@
|
|||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipRemoteStaging>true</skipRemoteStaging>
|
||||
<skipLocalStaging>true</skipLocalStaging>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
@ -118,13 +117,6 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
|
|
|
@ -73,10 +73,10 @@
|
|||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
|
@ -125,13 +125,13 @@
|
|||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
Loading…
Reference in New Issue