Merge branch 'master' into support-versioned-docs

This commit is contained in:
Tadgh 2024-09-04 13:14:18 -07:00
commit 5166207f58
72 changed files with 1233 additions and 274 deletions

View File

@ -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.

View File

@ -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,

View File

@ -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);

View File

@ -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

View File

@ -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());

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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();

View File

@ -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 {

View File

@ -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);

View File

@ -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.
*/

View File

@ -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>

View File

@ -0,0 +1,3 @@
---
release-date: "2024-03-20"
codename: "Zed"

View File

@ -0,0 +1,3 @@
---
release-date: "2024-08-24"
codename: "Zed"

View File

@ -0,0 +1,3 @@
---
release-date: "2024-08-25"
codename: "Borealis"

View File

@ -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."

View File

@ -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."

View File

@ -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!
"

View File

@ -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."

View File

@ -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!"

View File

@ -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

View File

@ -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."

View File

@ -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 -&gt; 2.29.4</li>
<li>htmlunit (Base): 3.9.0 -&gt; 3.11.0</li>
<li>Elasticsearch (Base): 8.11.1 -&gt; 8.14.3</li>
</ul>"

View File

@ -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);

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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 -> {};
}

View File

@ -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;
}
};
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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?");
}
}

View File

@ -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..

View File

@ -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?");
}
}

View File

@ -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..

View File

@ -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?");
}
}

View File

@ -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 &quot;Patient?active=true&quot; because this search matched 2 resources");
assertThat(responseString).contains("Failed to PATCH Patient with match URL &quot;Patient?active=true&quot; because this search matched 2 resources");
}
}

View File

@ -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..

View File

@ -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)))

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -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

View File

@ -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();
}

View File

@ -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();

View File

@ -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);
}
});
}
}

View File

@ -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 {
}

View File

@ -0,0 +1,4 @@
/**
* Junit helpers
*/
package ca.uhn.test.junit;

View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>