mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-03-09 14:33:32 +00:00
Ongoing cleanup
This commit is contained in:
parent
40d0c27ae3
commit
b78205c218
@ -56,7 +56,7 @@ web address.
|
|||||||
|
|
||||||
A common use for logical references is in references to conformance resources, such as ValueSets, StructureDefinitions, etc. For example, you might refer to the ValueSet `http://hl7.org/fhir/ValueSet/quantity-comparator` from your own resources. In this case, you are not necessarily telling the server that this is a real address that it should resolve, but rather that this is an identifier for a ValueSet where `ValueSet.url` has the given URI/URL.
|
A common use for logical references is in references to conformance resources, such as ValueSets, StructureDefinitions, etc. For example, you might refer to the ValueSet `http://hl7.org/fhir/ValueSet/quantity-comparator` from your own resources. In this case, you are not necessarily telling the server that this is a real address that it should resolve, but rather that this is an identifier for a ValueSet where `ValueSet.url` has the given URI/URL.
|
||||||
|
|
||||||
HAPI can be configured to treat certain URI/URL patterns as logical by using the DaoConfig#setTreatReferencesAsLogical property (see [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/dao/DaoConfig.html#setTreatReferencesAsLogical(java.util.Set))).
|
HAPI can be configured to treat certain URI/URL patterns as logical by using the DaoConfig#setTreatReferencesAsLogical property (see [JavaDoc](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setTreatReferencesAsLogical(java.util.Set))).
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package ca.uhn.fhir.jpa.provider;
|
package ca.uhn.fhir.jpa.provider;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.WebsocketDispatcherConfig;
|
||||||
import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test;
|
import ca.uhn.fhir.jpa.dao.dstu2.BaseJpaDstu2Test;
|
||||||
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
||||||
import ca.uhn.fhir.model.dstu2.resource.Bundle;
|
import ca.uhn.fhir.model.dstu2.resource.Bundle;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package ca.uhn.fhir.jpa.provider.dstu3;
|
package ca.uhn.fhir.jpa.provider.dstu3;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.WebsocketDispatcherConfig;
|
||||||
import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
|
import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
|
||||||
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
|
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
|
||||||
|
@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.provider.r4;
|
|||||||
|
|
||||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.WebsocketDispatcherConfig;
|
||||||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||||
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
||||||
|
@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.provider.r5;
|
|||||||
|
|
||||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.WebsocketDispatcherConfig;
|
||||||
import ca.uhn.fhir.jpa.dao.r5.BaseJpaR5Test;
|
import ca.uhn.fhir.jpa.dao.r5.BaseJpaR5Test;
|
||||||
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
||||||
|
@ -20,10 +20,6 @@ package ca.uhn.fhir.jpa.subscription.channel.queue;
|
|||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import org.springframework.messaging.MessageChannel;
|
|
||||||
import org.springframework.messaging.SubscribableChannel;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface is the factory for Queue Channels, which are the low level abstraction over a
|
* This interface is the factory for Queue Channels, which are the low level abstraction over a
|
||||||
* queue (e.g. memory queue, JMS queue, Kafka stream, etc.) for any purpose.
|
* queue (e.g. memory queue, JMS queue, Kafka stream, etc.) for any purpose.
|
||||||
@ -34,20 +30,38 @@ public interface IQueueChannelFactory {
|
|||||||
* Create a channel that is used to receive messages from the queue.
|
* Create a channel that is used to receive messages from the queue.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Implementations can choose to return the same object for multiple invocations of this method (and {@link #getOrCreateReceiver(String, Class, int)}
|
* Implementations can choose to return the same object for multiple invocations of this method (and {@link #getOrCreateReceiver(String, Class, QueueChannelConsumerConfig)}
|
||||||
* when invoked with the same {@literal theChannelName} if they need to, or they can create a new instance.
|
* when invoked with the same {@literal theChannelName} if they need to, or they can create a new instance.
|
||||||
* </p>
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theChannelName The actual underlying queue name
|
||||||
|
* @param theMessageType The object type that will be placed on this queue. Objects will be Jackson-annotated structures.
|
||||||
|
* @param theConfig Contains the configuration for subscribers. Note that this parameter is provided for
|
||||||
|
* both {@link #getOrCreateReceiver} and
|
||||||
|
* {@link #getOrCreateSender(String, Class, QueueChannelConsumerConfig)}
|
||||||
|
* even though this object is used to configure the sender only. We do this because the factory
|
||||||
|
* may want to create a single object to be used for both the sender and receiver, so this allows
|
||||||
|
* the config details to be known regardless of which method is returned first.
|
||||||
*/
|
*/
|
||||||
SubscribableChannel getOrCreateReceiver(String theChannelName, Class<?> theMessageType, int theConcurrentConsumers);
|
IQueueChannelReceiver getOrCreateReceiver(String theChannelName, Class<?> theMessageType, QueueChannelConsumerConfig theConfig);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a channel that is used to send messages to the queue.
|
* Create a channel that is used to send messages to the queue.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Implementations can choose to return the same object for multiple invocations of this method (and {@link #getOrCreateReceiver(String, Class, int)}
|
* Implementations can choose to return the same object for multiple invocations of this method (and {@link #getOrCreateReceiver(String, Class, QueueChannelConsumerConfig)}
|
||||||
* when invoked with the same {@literal theChannelName} if they need to, or they can create a new instance.
|
* when invoked with the same {@literal theChannelName} if they need to, or they can create a new instance.
|
||||||
* </p>
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theChannelName The actual underlying queue name
|
||||||
|
* @param theMessageType The object type that will be placed on this queue. Objects will be Jackson-annotated structures.
|
||||||
|
* @param theConfig Contains the configuration for subscribers. Note that this parameter is provided for
|
||||||
|
* both {@link #getOrCreateReceiver} and
|
||||||
|
* {@link #getOrCreateSender(String, Class, QueueChannelConsumerConfig)}
|
||||||
|
* even though this object is used to configure the sender only. We do this because the factory
|
||||||
|
* may want to create a single object to be used for both the sender and receiver, so this allows
|
||||||
|
* the config details to be known regardless of which method is returned first.
|
||||||
*/
|
*/
|
||||||
MessageChannel getOrCreateSender(String theChannelName, Class<?> theMessageType, int theConcurrentConsumers);
|
IQueueChannelSender getOrCreateSender(String theChannelName, Class<?> theMessageType, QueueChannelConsumerConfig theConfig);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package ca.uhn.fhir.jpa.subscription.channel.queue;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2020 University Health Network
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.springframework.messaging.SubscribableChannel;
|
||||||
|
import org.springframework.messaging.support.InterceptableChannel;
|
||||||
|
|
||||||
|
public interface IQueueChannelReceiver extends SubscribableChannel, InterceptableChannel {
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package ca.uhn.fhir.jpa.subscription.channel.queue;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2020 University Health Network
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.springframework.messaging.MessageChannel;
|
||||||
|
import org.springframework.messaging.SubscribableChannel;
|
||||||
|
import org.springframework.messaging.support.InterceptableChannel;
|
||||||
|
|
||||||
|
public interface IQueueChannelSender extends MessageChannel, InterceptableChannel {
|
||||||
|
}
|
@ -20,84 +20,14 @@ package ca.uhn.fhir.jpa.subscription.channel.queue;
|
|||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.util.StopWatch;
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.messaging.Message;
|
|
||||||
import org.springframework.messaging.MessageHandler;
|
|
||||||
import org.springframework.messaging.SubscribableChannel;
|
|
||||||
import org.springframework.messaging.support.ChannelInterceptor;
|
|
||||||
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
public class LinkedBlockingQueueChannel implements SubscribableChannel {
|
public class LinkedBlockingQueueChannel extends ExecutorSubscribableChannel implements IQueueChannelSender, IQueueChannelReceiver {
|
||||||
private Logger ourLog = LoggerFactory.getLogger(LinkedBlockingQueueChannel.class);
|
|
||||||
|
|
||||||
private final ExecutorSubscribableChannel mySubscribableChannel;
|
public LinkedBlockingQueueChannel(ThreadPoolExecutor theExecutor) {
|
||||||
private final BlockingQueue<Runnable> myQueue;
|
super(theExecutor);
|
||||||
|
|
||||||
public LinkedBlockingQueueChannel(BlockingQueue<Runnable> theQueue, String theThreadNamingPattern, int theConcurrentConsumers) {
|
|
||||||
|
|
||||||
ThreadFactory threadFactory = new BasicThreadFactory.Builder()
|
|
||||||
.namingPattern(theThreadNamingPattern)
|
|
||||||
.daemon(false)
|
|
||||||
.priority(Thread.NORM_PRIORITY)
|
|
||||||
.build();
|
|
||||||
RejectedExecutionHandler rejectedExecutionHandler = (theRunnable, theExecutor) -> {
|
|
||||||
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", theQueue.size());
|
|
||||||
StopWatch sw = new StopWatch();
|
|
||||||
try {
|
|
||||||
theQueue.put(theRunnable);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RejectedExecutionException("Task " + theRunnable.toString() +
|
|
||||||
" rejected from " + e.toString());
|
|
||||||
}
|
|
||||||
ourLog.info("Slot become available after {}ms", sw.getMillis());
|
|
||||||
};
|
|
||||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
|
||||||
1,
|
|
||||||
theConcurrentConsumers,
|
|
||||||
0L,
|
|
||||||
TimeUnit.MILLISECONDS,
|
|
||||||
theQueue,
|
|
||||||
threadFactory,
|
|
||||||
rejectedExecutionHandler);
|
|
||||||
myQueue = theQueue;
|
|
||||||
mySubscribableChannel = new ExecutorSubscribableChannel(executor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean subscribe(MessageHandler handler) {
|
|
||||||
return mySubscribableChannel.subscribe(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean unsubscribe(MessageHandler handler) {
|
|
||||||
return mySubscribableChannel.unsubscribe(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean send(Message<?> message, long timeout) {
|
|
||||||
return mySubscribableChannel.send(message, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public void clearInterceptorsForUnitTest() {
|
|
||||||
mySubscribableChannel.setInterceptors(new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public void addInterceptorForUnitTest(ChannelInterceptor theInterceptor) {
|
|
||||||
mySubscribableChannel.addInterceptor(theInterceptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public int getQueueSizeForUnitTest() {
|
|
||||||
return myQueue.size();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,17 +21,27 @@ package ca.uhn.fhir.jpa.subscription.channel.queue;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.subscription.process.registry.SubscriptionConstants;
|
import ca.uhn.fhir.jpa.subscription.process.registry.SubscriptionConstants;
|
||||||
import org.springframework.messaging.MessageChannel;
|
import ca.uhn.fhir.util.StopWatch;
|
||||||
|
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.messaging.SubscribableChannel;
|
import org.springframework.messaging.SubscribableChannel;
|
||||||
|
import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.RejectedExecutionHandler;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class LinkedBlockingQueueChannelFactory implements IQueueChannelFactory {
|
public class LinkedBlockingQueueChannelFactory implements IQueueChannelFactory {
|
||||||
|
|
||||||
private Map<String, SubscribableChannel> myChannels = Collections.synchronizedMap(new HashMap<>());
|
private Map<String, LinkedBlockingQueueChannel> myChannels = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(LinkedBlockingQueueChannelFactory.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -41,20 +51,49 @@ public class LinkedBlockingQueueChannelFactory implements IQueueChannelFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SubscribableChannel getOrCreateReceiver(String theChannelName, Class<?> theMessageType, int theConcurrentConsumers) {
|
public IQueueChannelReceiver getOrCreateReceiver(String theChannelName, Class<?> theMessageType, QueueChannelConsumerConfig theConfig) {
|
||||||
return getOrCreateChannel(theChannelName, theConcurrentConsumers);
|
return getOrCreateChannel(theChannelName, theConfig.getConcurrentConsumers());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageChannel getOrCreateSender(String theChannelName, Class<?> theMessageType, int theConcurrentConsumers) {
|
public IQueueChannelSender getOrCreateSender(String theChannelName, Class<?> theMessageType, QueueChannelConsumerConfig theConfig) {
|
||||||
return getOrCreateChannel(theChannelName, theConcurrentConsumers);
|
return getOrCreateChannel(theChannelName, theConfig.getConcurrentConsumers());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SubscribableChannel getOrCreateChannel(String theChannelName, int theConcurrentConsumers) {
|
private LinkedBlockingQueueChannel getOrCreateChannel(String theChannelName, int theConcurrentConsumers) {
|
||||||
return myChannels.computeIfAbsent(theChannelName, t -> {
|
return myChannels.computeIfAbsent(theChannelName, t -> {
|
||||||
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(SubscriptionConstants.DELIVERY_EXECUTOR_QUEUE_SIZE);
|
|
||||||
String threadNamingPattern = theChannelName + "-%d";
|
String threadNamingPattern = theChannelName + "-%d";
|
||||||
return new LinkedBlockingQueueChannel(queue, threadNamingPattern, theConcurrentConsumers);
|
|
||||||
|
ThreadFactory threadFactory = new BasicThreadFactory.Builder()
|
||||||
|
.namingPattern(threadNamingPattern)
|
||||||
|
.daemon(false)
|
||||||
|
.priority(Thread.NORM_PRIORITY)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(SubscriptionConstants.DELIVERY_EXECUTOR_QUEUE_SIZE);
|
||||||
|
RejectedExecutionHandler rejectedExecutionHandler = (theRunnable, theExecutor) -> {
|
||||||
|
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", queue.size());
|
||||||
|
StopWatch sw = new StopWatch();
|
||||||
|
try {
|
||||||
|
queue.put(theRunnable);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RejectedExecutionException("Task " + theRunnable.toString() +
|
||||||
|
" rejected from " + e.toString());
|
||||||
|
}
|
||||||
|
ourLog.info("Slot become available after {}ms", sw.getMillis());
|
||||||
|
};
|
||||||
|
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||||
|
1,
|
||||||
|
theConcurrentConsumers,
|
||||||
|
0L,
|
||||||
|
TimeUnit.MILLISECONDS,
|
||||||
|
queue,
|
||||||
|
threadFactory,
|
||||||
|
rejectedExecutionHandler);
|
||||||
|
return new LinkedBlockingQueueChannel(executor);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package ca.uhn.fhir.jpa.subscription.channel.queue;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR Subscription Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2020 University Health Network
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class QueueChannelConsumerConfig {
|
||||||
|
|
||||||
|
private int myConcurrentConsumers;
|
||||||
|
|
||||||
|
public int getConcurrentConsumers() {
|
||||||
|
return myConcurrentConsumers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConcurrentConsumers(int theConcurrentConsumers) {
|
||||||
|
myConcurrentConsumers = theConcurrentConsumers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -21,23 +21,22 @@ package ca.uhn.fhir.jpa.subscription.channel.subscription;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.queue.IQueueChannelFactory;
|
import ca.uhn.fhir.jpa.subscription.channel.queue.IQueueChannelFactory;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.channel.queue.IQueueChannelReceiver;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.channel.queue.IQueueChannelSender;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.channel.queue.QueueChannelConsumerConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryJsonMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryJsonMessage;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
|
||||||
import ca.uhn.fhir.jpa.subscription.process.registry.SubscriptionConstants;
|
import ca.uhn.fhir.jpa.subscription.process.registry.SubscriptionConstants;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.DisposableBean;
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.MessageChannel;
|
|
||||||
import org.springframework.messaging.MessageHandler;
|
import org.springframework.messaging.MessageHandler;
|
||||||
import org.springframework.messaging.MessagingException;
|
|
||||||
import org.springframework.messaging.SubscribableChannel;
|
import org.springframework.messaging.SubscribableChannel;
|
||||||
import org.springframework.messaging.support.AbstractSubscribableChannel;
|
import org.springframework.messaging.support.AbstractSubscribableChannel;
|
||||||
|
import org.springframework.messaging.support.ChannelInterceptor;
|
||||||
|
|
||||||
public class SubscriptionChannelFactory {
|
public class SubscriptionChannelFactory {
|
||||||
|
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionChannelFactory.class);
|
|
||||||
private final IQueueChannelFactory myQueueChannelFactory;
|
private final IQueueChannelFactory myQueueChannelFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,24 +47,40 @@ public class SubscriptionChannelFactory {
|
|||||||
myQueueChannelFactory = theQueueChannelFactory;
|
myQueueChannelFactory = theQueueChannelFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageChannel newDeliverySendingChannel(String theChannelName) {
|
public IQueueChannelSender newDeliverySendingChannel(String theChannelName) {
|
||||||
return myQueueChannelFactory.getOrCreateSender(theChannelName, ResourceDeliveryJsonMessage.class, getDeliveryChannelConcurrentConsumers());
|
QueueChannelConsumerConfig config = newConfigForDeliveryChannel();
|
||||||
|
return myQueueChannelFactory.getOrCreateSender(theChannelName, ResourceDeliveryJsonMessage.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubscribableChannel newDeliveryChannel(String theChannelName) {
|
public IQueueChannelReceiver newDeliveryReceivingChannel(String theChannelName) {
|
||||||
SubscribableChannel channel = myQueueChannelFactory.getOrCreateReceiver(theChannelName, ResourceDeliveryJsonMessage.class, getDeliveryChannelConcurrentConsumers());
|
QueueChannelConsumerConfig config = newConfigForDeliveryChannel();
|
||||||
|
IQueueChannelReceiver channel = myQueueChannelFactory.getOrCreateReceiver(theChannelName, ResourceDeliveryJsonMessage.class, config);
|
||||||
return new BroadcastingSubscribableChannelWrapper(channel);
|
return new BroadcastingSubscribableChannelWrapper(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageChannel newMatchingSendingChannel(String theChannelName) {
|
public IQueueChannelSender newMatchingSendingChannel(String theChannelName) {
|
||||||
return myQueueChannelFactory.getOrCreateSender(theChannelName, ResourceModifiedJsonMessage.class, getMatchingChannelConcurrentConsumers());
|
QueueChannelConsumerConfig config = newConfigForMatchingChannel();
|
||||||
|
return myQueueChannelFactory.getOrCreateSender(theChannelName, ResourceModifiedJsonMessage.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubscribableChannel newMatchingReceivingChannel(String theChannelName) {
|
public IQueueChannelReceiver newMatchingReceivingChannel(String theChannelName) {
|
||||||
SubscribableChannel channel = myQueueChannelFactory.getOrCreateReceiver(theChannelName, ResourceModifiedJsonMessage.class, getMatchingChannelConcurrentConsumers());
|
QueueChannelConsumerConfig config = newConfigForMatchingChannel();
|
||||||
|
IQueueChannelReceiver channel = myQueueChannelFactory.getOrCreateReceiver(theChannelName, ResourceModifiedJsonMessage.class, config);
|
||||||
return new BroadcastingSubscribableChannelWrapper(channel);
|
return new BroadcastingSubscribableChannelWrapper(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected QueueChannelConsumerConfig newConfigForDeliveryChannel() {
|
||||||
|
QueueChannelConsumerConfig config = new QueueChannelConsumerConfig();
|
||||||
|
config.setConcurrentConsumers(getDeliveryChannelConcurrentConsumers());
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected QueueChannelConsumerConfig newConfigForMatchingChannel() {
|
||||||
|
QueueChannelConsumerConfig config = new QueueChannelConsumerConfig();
|
||||||
|
config.setConcurrentConsumers(getMatchingChannelConcurrentConsumers());
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
public int getDeliveryChannelConcurrentConsumers() {
|
public int getDeliveryChannelConcurrentConsumers() {
|
||||||
return SubscriptionConstants.DELIVERY_CHANNEL_CONCURRENT_CONSUMERS;
|
return SubscriptionConstants.DELIVERY_CHANNEL_CONCURRENT_CONSUMERS;
|
||||||
}
|
}
|
||||||
@ -74,12 +89,12 @@ public class SubscriptionChannelFactory {
|
|||||||
return SubscriptionConstants.MATCHING_CHANNEL_CONCURRENT_CONSUMERS;
|
return SubscriptionConstants.MATCHING_CHANNEL_CONCURRENT_CONSUMERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BroadcastingSubscribableChannelWrapper extends AbstractSubscribableChannel implements MessageHandler, DisposableBean {
|
public static class BroadcastingSubscribableChannelWrapper extends AbstractSubscribableChannel implements IQueueChannelReceiver, DisposableBean {
|
||||||
|
|
||||||
private final SubscribableChannel myWrappedChannel;
|
private final IQueueChannelReceiver myWrappedChannel;
|
||||||
|
|
||||||
public BroadcastingSubscribableChannelWrapper(SubscribableChannel theChannel) {
|
public BroadcastingSubscribableChannelWrapper(IQueueChannelReceiver theChannel) {
|
||||||
theChannel.subscribe(this);
|
theChannel.subscribe(message -> send(message));
|
||||||
myWrappedChannel = theChannel;
|
myWrappedChannel = theChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,20 +104,10 @@ public class SubscriptionChannelFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean sendInternal(Message<?> theMessage, long timeout) {
|
protected boolean sendInternal(Message<?> theMessage, long timeout) {
|
||||||
// try {
|
for (MessageHandler next : getSubscribers()) {
|
||||||
for (MessageHandler next : getSubscribers()) {
|
next.handleMessage(theMessage);
|
||||||
next.handleMessage(theMessage);
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// ourLog.error("Failiure handling message", e);
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message<?> message) throws MessagingException {
|
|
||||||
send(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -112,5 +117,12 @@ public class SubscriptionChannelFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptor(ChannelInterceptor interceptor) {
|
||||||
|
super.addInterceptor(interceptor);
|
||||||
|
myWrappedChannel.addInterceptor(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.subscription.channel.subscription;
|
|||||||
|
|
||||||
import ca.uhn.fhir.jpa.subscription.process.registry.ActiveSubscription;
|
import ca.uhn.fhir.jpa.subscription.process.registry.ActiveSubscription;
|
||||||
import ca.uhn.fhir.jpa.subscription.process.registry.SubscriptionRegistry;
|
import ca.uhn.fhir.jpa.subscription.process.registry.SubscriptionRegistry;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.common.collect.MultimapBuilder;
|
import com.google.common.collect.MultimapBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -32,7 +31,6 @@ import org.springframework.messaging.MessageChannel;
|
|||||||
import org.springframework.messaging.MessageHandler;
|
import org.springframework.messaging.MessageHandler;
|
||||||
import org.springframework.messaging.SubscribableChannel;
|
import org.springframework.messaging.SubscribableChannel;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@ -61,7 +59,7 @@ public class SubscriptionChannelRegistry {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SubscribableChannel deliveryChannel = mySubscriptionDeliveryChannelFactory.newDeliveryChannel(channelName);
|
SubscribableChannel deliveryChannel = mySubscriptionDeliveryChannelFactory.newDeliveryReceivingChannel(channelName);
|
||||||
Optional<MessageHandler> deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(theActiveSubscription.getChannelType());
|
Optional<MessageHandler> deliveryHandler = mySubscriptionDeliveryHandlerFactory.createDeliveryHandler(theActiveSubscription.getChannelType());
|
||||||
|
|
||||||
SubscriptionChannelWithHandlers subscriptionChannelWithHandlers = new SubscriptionChannelWithHandlers(channelName, deliveryChannel);
|
SubscriptionChannelWithHandlers subscriptionChannelWithHandlers = new SubscriptionChannelWithHandlers(channelName, deliveryChannel);
|
||||||
|
@ -97,11 +97,6 @@ public class SubscriptionProcessorConfig {
|
|||||||
return new DaoResourceRetriever();
|
return new DaoResourceRetriever();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public WebsocketConnectionValidator websocketConnectionValidator() {
|
|
||||||
return new WebsocketConnectionValidator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SubscriptionLoader subscriptionLoader() {
|
public SubscriptionLoader subscriptionLoader() {
|
||||||
return new SubscriptionLoader();
|
return new SubscriptionLoader();
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package ca.uhn.fhir.jpa.config;
|
package ca.uhn.fhir.jpa.subscription.process.config;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* #%L
|
* #%L
|
||||||
* HAPI FHIR JPA Server
|
* HAPI FHIR Subscription Server
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2014 - 2020 University Health Network
|
* Copyright (C) 2014 - 2020 University Health Network
|
||||||
* %%
|
* %%
|
||||||
@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.config;
|
|||||||
|
|
||||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.process.deliver.websocket.SubscriptionWebsocketHandler;
|
import ca.uhn.fhir.jpa.subscription.process.deliver.websocket.SubscriptionWebsocketHandler;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.process.deliver.websocket.WebsocketConnectionValidator;
|
||||||
import org.springframework.beans.factory.annotation.Autowire;
|
import org.springframework.beans.factory.annotation.Autowire;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@ -37,9 +38,15 @@ import org.springframework.web.socket.handler.PerConnectionWebSocketHandler;
|
|||||||
@EnableWebSocket()
|
@EnableWebSocket()
|
||||||
@Controller
|
@Controller
|
||||||
public class WebsocketDispatcherConfig implements WebSocketConfigurer {
|
public class WebsocketDispatcherConfig implements WebSocketConfigurer {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ModelConfig myModelConfig;
|
ModelConfig myModelConfig;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebsocketConnectionValidator websocketConnectionValidator() {
|
||||||
|
return new WebsocketConnectionValidator();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) {
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry theRegistry) {
|
||||||
theRegistry.addHandler(subscriptionWebSocketHandler(), myModelConfig.getWebsocketContextPath()).setAllowedOrigins("*");
|
theRegistry.addHandler(subscriptionWebSocketHandler(), myModelConfig.getWebsocketContextPath()).setAllowedOrigins("*");
|
@ -50,6 +50,13 @@ public class SubscriptionWebsocketHandler extends TextWebSocketHandler implement
|
|||||||
@Autowired
|
@Autowired
|
||||||
SubscriptionChannelRegistry mySubscriptionChannelRegistry;
|
SubscriptionChannelRegistry mySubscriptionChannelRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public SubscriptionWebsocketHandler() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private FhirContext myCtx;
|
private FhirContext myCtx;
|
||||||
|
|
||||||
|
@ -36,6 +36,13 @@ public class WebsocketConnectionValidator {
|
|||||||
SubscriptionRegistry mySubscriptionRegistry;
|
SubscriptionRegistry mySubscriptionRegistry;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public WebsocketConnectionValidator() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public WebsocketValidationResponse validate(@NotNull IdType id) {
|
public WebsocketValidationResponse validate(@NotNull IdType id) {
|
||||||
if (!id.hasIdPart() || !id.isIdPartValid()) {
|
if (!id.hasIdPart() || !id.isIdPartValid()) {
|
||||||
return WebsocketValidationResponse.INVALID_RESPONSE("Invalid bind request - No ID included: " + id.getValue());
|
return WebsocketValidationResponse.INVALID_RESPONSE("Invalid bind request - No ID included: " + id.getValue());
|
||||||
|
@ -36,9 +36,19 @@ public abstract class BaseSubscriberForSubscriptionResources implements MessageH
|
|||||||
protected FhirContext myFhirContext;
|
protected FhirContext myFhirContext;
|
||||||
|
|
||||||
protected boolean isSubscription(ResourceModifiedMessage theNewResource) {
|
protected boolean isSubscription(ResourceModifiedMessage theNewResource) {
|
||||||
IBaseResource payload = theNewResource.getNewPayload(myFhirContext);
|
String payloadIdType = null;
|
||||||
String payloadIdType = myFhirContext.getResourceDefinition(payload).getName();
|
IIdType payloadId = theNewResource.getId(myFhirContext);
|
||||||
return payloadIdType.equals(ResourceTypeEnum.SUBSCRIPTION.getCode());
|
if (payloadId != null) {
|
||||||
|
payloadIdType = payloadId.getResourceType();
|
||||||
|
}
|
||||||
|
if (isBlank(payloadIdType)) {
|
||||||
|
IBaseResource payload = theNewResource.getNewPayload(myFhirContext);
|
||||||
|
if (payload != null) {
|
||||||
|
payloadIdType = myFhirContext.getResourceDefinition(payload).getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResourceTypeEnum.SUBSCRIPTION.getCode().equals(payloadIdType);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
package ca.uhn.fhir.jpa.subscription.channel.subscription;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.jpa.subscription.channel.queue.IQueueChannelReceiver;
|
||||||
|
import ca.uhn.fhir.jpa.subscription.channel.queue.LinkedBlockingQueueChannelFactory;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.MessageDeliveryException;
|
||||||
|
import org.springframework.messaging.MessageHandler;
|
||||||
|
import org.springframework.messaging.MessagingException;
|
||||||
|
import org.springframework.messaging.support.ChannelInterceptor;
|
||||||
|
import org.springframework.messaging.support.GenericMessage;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class SubscriptionChannelFactoryTest {
|
||||||
|
|
||||||
|
private SubscriptionChannelFactory mySvc;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ChannelInterceptor myInterceptor;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Exception> myExceptionCaptor;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
mySvc = new SubscriptionChannelFactory(new LinkedBlockingQueueChannelFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the channel doesn't silently swallow exceptions
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testInterceptorsOnChannelWrapperArePropagated() {
|
||||||
|
|
||||||
|
IQueueChannelReceiver channel = mySvc.newDeliveryReceivingChannel("CHANNEL_NAME");
|
||||||
|
channel.subscribe(new NpeThrowingHandler());
|
||||||
|
channel.addInterceptor(myInterceptor);
|
||||||
|
|
||||||
|
Message<?> input = new GenericMessage<>("TEST");
|
||||||
|
|
||||||
|
when(myInterceptor.preSend(any(),any())).thenAnswer(t->t.getArgument(0, Message.class));
|
||||||
|
when(myInterceptor.postReceive(any(),any())).thenAnswer(t->t.getArgument(0, Message.class));
|
||||||
|
|
||||||
|
try {
|
||||||
|
channel.send(input);
|
||||||
|
fail();
|
||||||
|
} catch (MessageDeliveryException e) {
|
||||||
|
assertTrue(e.getCause() instanceof NullPointerException);
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(myInterceptor, times(1)).afterSendCompletion(any(), any(), anyBoolean(), myExceptionCaptor.capture());
|
||||||
|
|
||||||
|
assertTrue(myExceptionCaptor.getValue() instanceof NullPointerException);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class NpeThrowingHandler implements MessageHandler {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message<?> message) throws MessagingException {
|
||||||
|
throw new NullPointerException("THIS IS THE MESSAGE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
|||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
|
||||||
import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
|
import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.WebsocketDispatcherConfig;
|
||||||
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
|
||||||
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
import ca.uhn.fhir.jpa.provider.GraphQLProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
|
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
|
||||||
@ -20,7 +20,6 @@ import ca.uhn.fhir.jpa.provider.r5.JpaConformanceProviderR5;
|
|||||||
import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5;
|
import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5;
|
||||||
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
|
||||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader;
|
|
||||||
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
|
import ca.uhn.fhir.jpa.util.ResourceProviderFactory;
|
||||||
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
|
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
|
||||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package ca.uhn.fhirtest.config;
|
package ca.uhn.fhirtest.config;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.WebsocketDispatcherConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
|
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.process.config.SubscriptionProcessorConfig;
|
import ca.uhn.fhir.jpa.subscription.process.config.SubscriptionProcessorConfig;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
||||||
|
@ -0,0 +1,278 @@
|
|||||||
|
package ca.uhn.fhir.rest.server.interceptor.auth;
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR - Server Framework
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2020 University Health Network
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
|
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||||
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.param.ParameterUtil;
|
||||||
|
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
||||||
|
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
|
||||||
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails;
|
||||||
|
import ca.uhn.fhir.rest.server.util.ServletRequestUtil;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
|
import ca.uhn.fhir.util.bundle.ModifiableBundleEntry;
|
||||||
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
|
import org.apache.commons.collections4.ListUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interceptor can be used to automatically narrow the scope of searches in order to
|
||||||
|
* automatically restrict the searches to specific compartments.
|
||||||
|
* <p>
|
||||||
|
* For example, this interceptor
|
||||||
|
* could be used to restrict a user to only viewing data belonging to Patient/123 (i.e. data
|
||||||
|
* in the <code>Patient/123</code> compartment). In this case, a user performing a search
|
||||||
|
* for<br/>
|
||||||
|
* <code>http://baseurl/Observation?category=laboratory</code><br/>
|
||||||
|
* would receive results as though they had requested<br/>
|
||||||
|
* <code>http://baseurl/Observation?subject=Patient/123&category=laboratory</code>
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Note that this interceptor should be used in combination with {@link AuthorizationInterceptor}
|
||||||
|
* if you are restricting results because of a security restriction. This interceptor is not
|
||||||
|
* intended to be a failsafe way of preventing users from seeing the wrong data (that is the
|
||||||
|
* purpose of AuthorizationInterceptor). This interceptor is simply intended as a convenience to
|
||||||
|
* help users simplify their queries while not receiving security errors for to trying to access
|
||||||
|
* data they do not have access to see.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see AuthorizationInterceptor
|
||||||
|
*/
|
||||||
|
public class SearchNarrowingInterceptor {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(SearchNarrowingInterceptor.class);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses should override this method to supply the set of compartments that
|
||||||
|
* the user making the request should actually have access to.
|
||||||
|
* <p>
|
||||||
|
* Typically this is done by examining <code>theRequestDetails</code> to find
|
||||||
|
* out who the current user is and then building a list of Strings.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theRequestDetails The individual request currently being applied
|
||||||
|
* @return The list of allowed compartments and instances that should be used
|
||||||
|
* for search narrowing. If this method returns <code>null</code>, no narrowing will
|
||||||
|
* be performed
|
||||||
|
*/
|
||||||
|
protected AuthorizedList buildAuthorizedList(@SuppressWarnings("unused") RequestDetails theRequestDetails) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
|
||||||
|
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
|
||||||
|
// We don't support this operation type yet
|
||||||
|
Validate.isTrue(theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_SYSTEM);
|
||||||
|
|
||||||
|
if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_TYPE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
|
||||||
|
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName());
|
||||||
|
HashMap<String, List<String>> parameterToOrValues = new HashMap<>();
|
||||||
|
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
|
||||||
|
if (authorizedList == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a map of search parameter values that need to be added to the
|
||||||
|
* given request
|
||||||
|
*/
|
||||||
|
Collection<String> compartments = authorizedList.getAllowedCompartments();
|
||||||
|
if (compartments != null) {
|
||||||
|
processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, compartments, true);
|
||||||
|
}
|
||||||
|
Collection<String> resources = authorizedList.getAllowedInstances();
|
||||||
|
if (resources != null) {
|
||||||
|
processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, resources, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add any param values to the actual request
|
||||||
|
*/
|
||||||
|
if (parameterToOrValues.size() > 0) {
|
||||||
|
Map<String, String[]> newParameters = new HashMap<>(theRequestDetails.getParameters());
|
||||||
|
for (Map.Entry<String, List<String>> nextEntry : parameterToOrValues.entrySet()) {
|
||||||
|
String nextParamName = nextEntry.getKey();
|
||||||
|
List<String> nextAllowedValues = nextEntry.getValue();
|
||||||
|
|
||||||
|
if (!newParameters.containsKey(nextParamName)) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If we don't already have a parameter of the given type, add one
|
||||||
|
*/
|
||||||
|
String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
|
||||||
|
String[] paramValues = {nextValuesJoined};
|
||||||
|
newParameters.put(nextParamName, paramValues);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the client explicitly requested the given parameter already, we'll
|
||||||
|
* just update the request to have the intersection of the values that the client
|
||||||
|
* requested, and the values that the user is allowed to see
|
||||||
|
*/
|
||||||
|
String[] existingValues = newParameters.get(nextParamName);
|
||||||
|
boolean restrictedExistingList = false;
|
||||||
|
for (int i = 0; i < existingValues.length; i++) {
|
||||||
|
|
||||||
|
String nextExistingValue = existingValues[i];
|
||||||
|
List<String> nextRequestedValues = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue);
|
||||||
|
List<String> nextPermittedValues = ListUtils.intersection(nextRequestedValues, nextAllowedValues);
|
||||||
|
if (nextPermittedValues.size() > 0) {
|
||||||
|
restrictedExistingList = true;
|
||||||
|
existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If none of the values that were requested by the client overlap at all
|
||||||
|
* with the values that the user is allowed to see, we'll just add the permitted
|
||||||
|
* list as a new list. Ultimately this scenario actually means that the client
|
||||||
|
* shouldn't get *any* results back, and adding a new AND parameter (that doesn't
|
||||||
|
* overlap at all with the others) is one way of ensuring that.
|
||||||
|
*/
|
||||||
|
if (!restrictedExistingList) {
|
||||||
|
String[] newValues = Arrays.copyOf(existingValues, existingValues.length + 1);
|
||||||
|
newValues[existingValues.length] = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
|
||||||
|
newParameters.put(nextParamName, newValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
theRequestDetails.setParameters(newParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
|
||||||
|
public void incomingRequestPreHandled(ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
|
||||||
|
if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.TRANSACTION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IBaseBundle bundle = (IBaseBundle) theRequestDetails.getResource();
|
||||||
|
FhirContext ctx = theRequestDetails.getFhirContext();
|
||||||
|
BundleEntryUrlProcessor processor = new BundleEntryUrlProcessor(ctx, theRequestDetails, theRequest, theResponse);
|
||||||
|
BundleUtil.processEntries(ctx, bundle, processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BundleEntryUrlProcessor implements Consumer<ModifiableBundleEntry> {
|
||||||
|
private final FhirContext myFhirContext;
|
||||||
|
private final ServletRequestDetails myRequestDetails;
|
||||||
|
private final HttpServletRequest myRequest;
|
||||||
|
private final HttpServletResponse myResponse;
|
||||||
|
|
||||||
|
public BundleEntryUrlProcessor(FhirContext theFhirContext, ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) {
|
||||||
|
myFhirContext = theFhirContext;
|
||||||
|
myRequestDetails = theRequestDetails;
|
||||||
|
myRequest = theRequest;
|
||||||
|
myResponse = theResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(ModifiableBundleEntry theModifiableBundleEntry) {
|
||||||
|
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
|
||||||
|
|
||||||
|
String url = theModifiableBundleEntry.getRequestUrl();
|
||||||
|
|
||||||
|
ServletSubRequestDetails subServletRequestDetails = ServletRequestUtil.getServletSubRequestDetails(myRequestDetails, url, paramValues);
|
||||||
|
BaseMethodBinding<?> method = subServletRequestDetails.getServer().determineResourceMethod(subServletRequestDetails, url);
|
||||||
|
RestOperationTypeEnum restOperationType = method.getRestOperationType();
|
||||||
|
subServletRequestDetails.setRestOperationType(restOperationType);
|
||||||
|
|
||||||
|
incomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse);
|
||||||
|
|
||||||
|
theModifiableBundleEntry.setRequestUrl(myFhirContext, ServletRequestUtil.extractUrl(subServletRequestDetails));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, HashMap<String, List<String>> theParameterToOrValues, Collection<String> theResourcesOrCompartments, boolean theAreCompartments) {
|
||||||
|
String lastCompartmentName = null;
|
||||||
|
String lastSearchParamName = null;
|
||||||
|
for (String nextCompartment : theResourcesOrCompartments) {
|
||||||
|
Validate.isTrue(StringUtils.countMatches(nextCompartment, '/') == 1, "Invalid compartment name (must be in form \"ResourceType/xxx\": %s", nextCompartment);
|
||||||
|
String compartmentName = nextCompartment.substring(0, nextCompartment.indexOf('/'));
|
||||||
|
|
||||||
|
String searchParamName = null;
|
||||||
|
if (compartmentName.equalsIgnoreCase(lastCompartmentName)) {
|
||||||
|
|
||||||
|
// Avoid doing a lookup for the same thing repeatedly
|
||||||
|
searchParamName = lastSearchParamName;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (compartmentName.equalsIgnoreCase(theRequestDetails.getResourceName())) {
|
||||||
|
|
||||||
|
searchParamName = "_id";
|
||||||
|
|
||||||
|
} else if (theAreCompartments) {
|
||||||
|
|
||||||
|
List<RuntimeSearchParam> searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName);
|
||||||
|
if (searchParams.size() > 0) {
|
||||||
|
|
||||||
|
// Resources like Observation have several fields that add the resource to
|
||||||
|
// the compartment. In the case of Observation, it's subject, patient and performer.
|
||||||
|
// For this kind of thing, we'll prefer the one called "patient".
|
||||||
|
RuntimeSearchParam searchParam =
|
||||||
|
searchParams
|
||||||
|
.stream()
|
||||||
|
.filter(t -> t.getName().equalsIgnoreCase(compartmentName))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(searchParams.get(0));
|
||||||
|
searchParamName = searchParam.getName();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCompartmentName = compartmentName;
|
||||||
|
lastSearchParamName = searchParamName;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchParamName != null) {
|
||||||
|
List<String> orValues = theParameterToOrValues.computeIfAbsent(searchParamName, t -> new ArrayList<>());
|
||||||
|
orValues.add(nextCompartment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,7 +27,6 @@ import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2;
|
|||||||
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
|
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
|
||||||
import ca.uhn.fhir.jpa.config.BaseJavaConfigR4;
|
import ca.uhn.fhir.jpa.config.BaseJavaConfigR4;
|
||||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.config.WebsocketDispatcherConfig;
|
|
||||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||||
import ca.uhn.fhir.jpa.provider.BaseJpaProvider;
|
import ca.uhn.fhir.jpa.provider.BaseJpaProvider;
|
||||||
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProvider;
|
import ca.uhn.fhir.jpa.provider.BaseJpaSystemProvider;
|
||||||
|
@ -98,6 +98,10 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
|
|||||||
setExpectedCount(theCount, false);
|
setExpectedCount(theCount, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSet() {
|
||||||
|
return myCountdownLatch.get() != null;
|
||||||
|
}
|
||||||
|
|
||||||
private void createLatch(int theCount) {
|
private void createLatch(int theCount) {
|
||||||
myFailures.set(Collections.synchronizedList(new ArrayList<>()));
|
myFailures.set(Collections.synchronizedList(new ArrayList<>()));
|
||||||
myCalledWith.set(Collections.synchronizedList(new ArrayList<>()));
|
myCalledWith.set(Collections.synchronizedList(new ArrayList<>()));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user