Merge branch 'master' into ja_interceptor_jpa_queries

This commit is contained in:
James Agnew 2019-04-01 09:59:03 -04:00
commit 3eeca4a8fd
36 changed files with 1298 additions and 693 deletions

View File

@ -232,7 +232,9 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
/**
* Constructor
*/
DateParamDateTimeHolder() {
// LEAVE THIS AS PUBLIC!!
@SuppressWarnings("WeakerAccess")
public DateParamDateTimeHolder() {
super();
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.util;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,14 +20,16 @@ package ca.uhn.fhir.util;
* #L%
*/
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@ -38,38 +40,38 @@ import java.util.List;
* for a long time (potentially lots of them!) and will leave your system low on
* ports if you put it into production.
* </b></p>
*
* <p>
* How it works:
*
* <p>
* We have lots of tests that need a free port because they want to open up
* a server, and need the port to be unique and unused so that the tests can
* run multithreaded. This turns out to just be an awful problem to solve for
* lots of reasons:
*
* <p>
* 1. You can request a free port from the OS by calling <code>new ServerSocket(0);</code>
* and this seems to work 99% of the time, but occasionally on a heavily loaded
* server if two processes ask at the exact same time they will receive the
* same port assignment, and one will fail.
* and this seems to work 99% of the time, but occasionally on a heavily loaded
* server if two processes ask at the exact same time they will receive the
* same port assignment, and one will fail.
* 2. Tests run in separate processes, so we can't just rely on keeping a collection
* of assigned ports or anything like that.
*
* of assigned ports or anything like that.
* <p>
* So we solve this like this:
*
* <p>
* At random, this class will pick a "control port" and bind it. A control port
* is just a randomly chosen port that is a multiple of 100. If we can bind
* successfully to that port, we now own the range of "n+1 to n+99". If we can't
* bind that port, it means some other process has probably taken it so
* we'll just try again until we find an available control port.
*
* <p>
* Assuming we successfully bind a control port, we'll give out any available
* ports in the range "n+1 to n+99" until we've exhausted the whole set, and
* then we'll pick another control port (if we actually get asked for over
* 100 ports.. this should be a rare event).
*
* <p>
* This mechanism has the benefit of (fingers crossed) being bulletproof
* in terms of its ability to give out ports that are actually free, thereby
* preventing random test failures.
*
* <p>
* This mechanism has the drawback of never giving up a control port once
* it has assigned one. To be clear, this class is deliberately leaking
* resources. Again, no production use!
@ -78,11 +80,10 @@ public class PortUtil {
private static final int SPACE_SIZE = 100;
private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class);
private static final PortUtil INSTANCE = new PortUtil();
private static int ourPortDelay = 500;
private List<ServerSocket> myControlSockets = new ArrayList<>();
private Integer myCurrentControlSocketPort = null;
private int myCurrentOffset = 0;
/**
* Constructor -
*/
@ -149,45 +150,51 @@ public class PortUtil {
int nextCandidatePort = myCurrentControlSocketPort + myCurrentOffset;
// Try to open a port on this socket and use it
try (ServerSocket server = new ServerSocket()) {
server.setReuseAddress(true);
server.bind(new InetSocketAddress("localhost", nextCandidatePort));
try (Socket client = new Socket()) {
client.setReuseAddress(true);
client.connect(new InetSocketAddress("localhost", nextCandidatePort));
}
} catch (IOException e) {
// try (ServerSocket server = new ServerSocket()) {
// server.setReuseAddress(true);
// server.bind(new InetSocketAddress("localhost", nextCandidatePort));
// try (Socket client = new Socket()) {
// client.setReuseAddress(true);
// client.connect(new InetSocketAddress("localhost", nextCandidatePort));
// }
// } catch (IOException e) {
// continue;
// }
if (!isAvailable(nextCandidatePort)) {
continue;
}
// Log who asked for the port, just in case that's useful
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement previousElement = stackTraceElements[2];
StackTraceElement previousElement = Arrays.stream(stackTraceElements)
.filter(t -> !t.toString().contains("PortUtil.") && !t.toString().contains("getStackTrace"))
.findFirst()
.orElse(stackTraceElements[2]);
ourLog.info("Returned available port {} for: {}", nextCandidatePort, previousElement.toString());
/*
* This is an attempt to make sure the port is actually
* free before releasing it. For whatever reason on Linux
* it seems like even after we close the ServerSocket there
* is a short while where it is not possible to bind the
* port, even though it should be released by then.
*
* I don't have any solid evidence that this is a good
* way to do this, but it seems to help...
*/
for (int i = 0; i < 10; i++) {
try (Socket client = new Socket()) {
client.setReuseAddress(true);
client.connect(new InetSocketAddress(nextCandidatePort), 1000);
ourLog.info("Socket still seems open");
Thread.sleep(250);
} catch (Exception e) {
break;
}
}
// /*
// * This is an attempt to make sure the port is actually
// * free before releasing it. For whatever reason on Linux
// * it seems like even after we close the ServerSocket there
// * is a short while where it is not possible to bind the
// * port, even though it should be released by then.
// *
// * I don't have any solid evidence that this is a good
// * way to do this, but it seems to help...
// */
// for (int i = 0; i < 10; i++) {
// try (Socket client = new Socket()) {
// client.setReuseAddress(true);
// client.connect(new InetSocketAddress(nextCandidatePort), 1000);
// ourLog.info("Socket still seems open");
// Thread.sleep(250);
// } catch (Exception e) {
// break;
// }
// }
//
try {
Thread.sleep(250);
Thread.sleep(ourPortDelay);
} catch (InterruptedException theE) {
// ignore
}
@ -200,6 +207,41 @@ public class PortUtil {
}
}
@VisibleForTesting
public static void setPortDelay(Integer thePortDelay) {
if (thePortDelay == null) {
thePortDelay = 500;
} else {
ourPortDelay = thePortDelay;
}
}
private static boolean isAvailable(int port) {
ServerSocket ss = null;
DatagramSocket ds = null;
try {
ss = new ServerSocket(port);
ss.setReuseAddress(true);
ds = new DatagramSocket(port);
ds.setReuseAddress(true);
return true;
} catch (IOException e) {
return false;
} finally {
if (ds != null) {
ds.close();
}
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
/* should not be thrown */
}
}
}
}
/**
* The entire purpose here is to find an available port that can then be
* bound for by server in a unit test without conflicting with other tests.

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.util;
import org.junit.After;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -39,8 +40,14 @@ public class PortUtilTest {
}
}
@After
public void after() {
PortUtil.setPortDelay(null);
}
@Test
public void testPortsAreNotReused() throws InterruptedException {
PortUtil.setPortDelay(0);
List<Integer> ports = Collections.synchronizedList(new ArrayList<>());
List<PortUtil> portUtils = Collections.synchronizedList(new ArrayList<>());
@ -48,7 +55,7 @@ public class PortUtilTest {
int tasksCount = 20;
ExecutorService pool = Executors.newFixedThreadPool(tasksCount);
int portsPerTaskCount = 51;
int portsPerTaskCount = 151;
for (int i = 0; i < tasksCount; i++) {
pool.submit(() -> {
PortUtil portUtil = new PortUtil();

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
@ -23,7 +24,6 @@ import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
@ -169,6 +169,11 @@ public abstract class BaseConfig implements SchedulingConfigurer {
return new PersistenceExceptionTranslationPostProcessor();
}
@Bean
public InterceptorService interceptorRegistry() {
return new InterceptorService("hapi-fhir-jpa");
}
public static void configureEntityManagerFactory(LocalContainerEntityManagerFactoryBean theFactory, FhirContext theCtx) {
theFactory.setJpaDialect(hibernateJpaDialect(theCtx.getLocalizer()));
theFactory.setPackagesToScan("ca.uhn.fhir.jpa.model.entity", "ca.uhn.fhir.jpa.entity");

View File

@ -1003,6 +1003,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
throw new ResourceNotFoundException(theId);
}
validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
entity.setTransientForcedId(theId.getIdPart());
return entity;
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
* 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.
@ -392,7 +392,8 @@ public class SearchBuilder implements ISearchBuilder {
Join<ResourceTable, ResourceLink> join = createJoin(JoinEnum.REFERENCE, theParamName);
List<Predicate> codePredicates = new ArrayList<>();
List<IIdType> targetIds = new ArrayList<>();
List<String> targetQualifiedUrls = new ArrayList<>();
for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
IQueryParameterType nextOr = theList.get(orIdx);
@ -401,173 +402,31 @@ public class SearchBuilder implements ISearchBuilder {
ReferenceParam ref = (ReferenceParam) nextOr;
if (isBlank(ref.getChain())) {
/*
* Handle non-chained search, e.g. Patient?organization=Organization/123
*/
IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
if (dt.hasBaseUrl()) {
if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
dt = dt.toUnqualified();
targetIds.add(dt);
} else {
ourLog.debug("Searching for resource link with target URL: {}", dt.getValue());
Predicate eq = myBuilder.equal(join.get("myTargetResourceUrl"), dt.getValue());
codePredicates.add(eq);
continue;
targetQualifiedUrls.add(dt.getValue());
}
}
List<Long> targetPid;
try {
targetPid = myIdHelperService.translateForcedIdToPids(dt);
} catch (ResourceNotFoundException e) {
// Use a PID that will never exist
targetPid = Collections.singletonList(-1L);
}
for (Long next : targetPid) {
ourLog.debug("Searching for resource link with target PID: {}", next);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = myBuilder.equal(join.get("myTargetResourcePid"), next);
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
} else {
targetIds.add(dt);
}
} else {
final List<Class<? extends IBaseResource>> resourceTypes;
String resourceId;
if (!ref.getValue().matches("[a-zA-Z]+/.*")) {
/*
* Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
*/
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
resourceTypes = new ArrayList<>();
Set<String> targetTypes = param.getTargets();
if (targetTypes != null && !targetTypes.isEmpty()) {
for (String next : targetTypes) {
resourceTypes.add(myContext.getResourceDefinition(next).getImplementingClass());
}
}
if (resourceTypes.isEmpty()) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
if (searchParamByName == null) {
throw new InternalErrorException("Could not find parameter " + theParamName);
}
String paramPath = searchParamByName.getPath();
if (paramPath.endsWith(".as(Reference)")) {
paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
}
if (paramPath.contains(".extension(")) {
int startIdx = paramPath.indexOf(".extension(");
int endIdx = paramPath.indexOf(')', startIdx);
if (startIdx != -1 && endIdx != -1) {
paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
}
}
BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath);
if (def instanceof RuntimeChildChoiceDefinition) {
RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
resourceTypes.addAll(choiceDef.getResourceTypes());
} else if (def instanceof RuntimeChildResourceDefinition) {
RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
resourceTypes.addAll(resDef.getResourceTypes());
if (resourceTypes.size() == 1) {
if (resourceTypes.get(0).isInterface()) {
throw new InvalidRequestException("Unable to perform search for unqualified chain '" + theParamName + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '" + theParamName + ":[ResourceType]' to perform this search.");
}
}
} else {
throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass());
}
}
if (resourceTypes.isEmpty()) {
for (BaseRuntimeElementDefinition<?> next : myContext.getElementDefinitions()) {
if (next instanceof RuntimeResourceDefinition) {
RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
resourceTypes.add(nextResDef.getImplementingClass());
}
}
}
resourceId = ref.getValue();
} else {
try {
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(ref.getResourceType());
resourceTypes = new ArrayList<>(1);
resourceTypes.add(resDef.getImplementingClass());
resourceId = ref.getIdPart();
} catch (DataFormatException e) {
throw new InvalidRequestException("Invalid resource type: " + ref.getResourceType());
}
}
boolean foundChainMatch = false;
for (Class<? extends IBaseResource> nextType : resourceTypes) {
String chain = ref.getChain();
String remainingChain = null;
int chainDotIndex = chain.indexOf('.');
if (chainDotIndex != -1) {
remainingChain = chain.substring(chainDotIndex + 1);
chain = chain.substring(0, chainDotIndex);
}
RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType);
String subResourceName = typeDef.getName();
IFhirResourceDao<?> dao = myCallingDao.getDao(nextType);
if (dao == null) {
ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName());
continue;
}
int qualifierIndex = chain.indexOf(':');
String qualifier = null;
if (qualifierIndex != -1) {
qualifier = chain.substring(qualifierIndex);
chain = chain.substring(0, qualifierIndex);
}
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
RuntimeSearchParam param = null;
if (!isMeta) {
param = mySearchParamRegistry.getSearchParamByName(typeDef, chain);
if (param == null) {
ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
continue;
}
}
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
for (IQueryParameterType next : theList) {
String nextValue = next.getValueAsQueryToken(myContext);
IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue);
if (chainValue == null) {
continue;
}
foundChainMatch = true;
orValues.add(chainValue);
}
Subquery<Long> subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = join.get("myTargetResourcePid").in(subQ);
Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate);
codePredicates.add(andPredicate);
}
if (!foundChainMatch) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + ref.getChain()));
}
myPredicates.add(myBuilder.or(toArray(codePredicates)));
addPredicateReferenceWithChain(theResourceName, theParamName, theList, join, new ArrayList<>(), ref);
return;
}
@ -578,7 +437,173 @@ public class SearchBuilder implements ISearchBuilder {
}
myPredicates.add(myBuilder.or(toArray(codePredicates)));
List<Predicate> codePredicates = new ArrayList<>();
// Resources by ID
List<Long> targetPids = myIdHelperService.translateForcedIdToPids(targetIds);
if (!targetPids.isEmpty()) {
ourLog.debug("Searching for resource link with target PIDs: {}", targetPids);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = join.get("myTargetResourcePid").in(targetPids);
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
}
// Resources by fully qualified URL
if (!targetQualifiedUrls.isEmpty()) {
ourLog.debug("Searching for resource link with target URLs: {}", targetQualifiedUrls);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
Predicate pidPredicate = join.get("myTargetResourceUrl").in(targetQualifiedUrls);
codePredicates.add(myBuilder.and(pathPredicate, pidPredicate));
}
if (codePredicates.size() > 0) {
myPredicates.add(myBuilder.or(toArray(codePredicates)));
} else {
// Add a predicate that will never match
Predicate pidPredicate = join.get("myTargetResourcePid").in(-1L);
myPredicates.clear();
myPredicates.add(pidPredicate);
}
}
private void addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, Join<ResourceTable, ResourceLink> theJoin, List<Predicate> theCodePredicates, ReferenceParam theRef) {
final List<Class<? extends IBaseResource>> resourceTypes;
String resourceId;
if (!theRef.getValue().matches("[a-zA-Z]+/.*")) {
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
resourceTypes = new ArrayList<>();
Set<String> targetTypes = param.getTargets();
if (targetTypes != null && !targetTypes.isEmpty()) {
for (String next : targetTypes) {
resourceTypes.add(myContext.getResourceDefinition(next).getImplementingClass());
}
}
if (resourceTypes.isEmpty()) {
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceName);
RuntimeSearchParam searchParamByName = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName);
if (searchParamByName == null) {
throw new InternalErrorException("Could not find parameter " + theParamName);
}
String paramPath = searchParamByName.getPath();
if (paramPath.endsWith(".as(Reference)")) {
paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
}
if (paramPath.contains(".extension(")) {
int startIdx = paramPath.indexOf(".extension(");
int endIdx = paramPath.indexOf(')', startIdx);
if (startIdx != -1 && endIdx != -1) {
paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
}
}
BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath);
if (def instanceof RuntimeChildChoiceDefinition) {
RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
resourceTypes.addAll(choiceDef.getResourceTypes());
} else if (def instanceof RuntimeChildResourceDefinition) {
RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
resourceTypes.addAll(resDef.getResourceTypes());
if (resourceTypes.size() == 1) {
if (resourceTypes.get(0).isInterface()) {
throw new InvalidRequestException("Unable to perform search for unqualified chain '" + theParamName + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '" + theParamName + ":[ResourceType]' to perform this search.");
}
}
} else {
throw new ConfigurationException("Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass());
}
}
if (resourceTypes.isEmpty()) {
for (BaseRuntimeElementDefinition<?> next : myContext.getElementDefinitions()) {
if (next instanceof RuntimeResourceDefinition) {
RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
resourceTypes.add(nextResDef.getImplementingClass());
}
}
}
resourceId = theRef.getValue();
} else {
try {
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theRef.getResourceType());
resourceTypes = new ArrayList<>(1);
resourceTypes.add(resDef.getImplementingClass());
resourceId = theRef.getIdPart();
} catch (DataFormatException e) {
throw new InvalidRequestException("Invalid resource type: " + theRef.getResourceType());
}
}
boolean foundChainMatch = false;
for (Class<? extends IBaseResource> nextType : resourceTypes) {
String chain = theRef.getChain();
String remainingChain = null;
int chainDotIndex = chain.indexOf('.');
if (chainDotIndex != -1) {
remainingChain = chain.substring(chainDotIndex + 1);
chain = chain.substring(0, chainDotIndex);
}
RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType);
String subResourceName = typeDef.getName();
IFhirResourceDao<?> dao = myCallingDao.getDao(nextType);
if (dao == null) {
ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName());
continue;
}
int qualifierIndex = chain.indexOf(':');
String qualifier = null;
if (qualifierIndex != -1) {
qualifier = chain.substring(qualifierIndex);
chain = chain.substring(0, qualifierIndex);
}
boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
RuntimeSearchParam param = null;
if (!isMeta) {
param = mySearchParamRegistry.getSearchParamByName(typeDef, chain);
if (param == null) {
ourLog.debug("Type {} doesn't have search param {}", nextType.getSimpleName(), param);
continue;
}
}
ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
for (IQueryParameterType next : theList) {
String nextValue = next.getValueAsQueryToken(myContext);
IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue);
if (chainValue == null) {
continue;
}
foundChainMatch = true;
orValues.add(chainValue);
}
Subquery<Long> subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues);
Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin);
Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ);
Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate);
theCodePredicates.add(andPredicate);
}
if (!foundChainMatch) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theRef.getChain()));
}
myPredicates.add(myBuilder.or(toArray(theCodePredicates)));
}
private Subquery<Long> createLinkSubquery(boolean theFoundChainMatch, String theChain, String theSubResourceName, List<IQueryParameterType> theOrValues) {

View File

@ -30,16 +30,15 @@ import org.springframework.data.repository.query.Param;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f FROM ForcedId f WHERE myForcedId = :forced_id")
public List<ForcedId> findByForcedId(@Param("forced_id") String theForcedId);
@Query("SELECT f FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id")
public List<ForcedId> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
// FIXME: JA We should log a performance warning if this is used since it's not indexed
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myForcedId IN (:forced_id)")
List<Long> findByForcedId(@Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId IN (:forced_id)")
List<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") Collection<String> theForcedId);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid = :resource_pid")
public ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
ForcedId findByResourcePid(@Param("resource_pid") Long theResourcePid);
@Query("SELECT f FROM ForcedId f WHERE f.myResourcePid in (:pids)")
Collection<ForcedId> findByResourcePids(@Param("pids") Collection<Long> pids);
}

View File

@ -62,6 +62,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
Long valueOf;
try {
valueOf = myIdHelperService.translateForcedIdToPid(theTypeString, theId);
ourLog.trace("Translated {}/{} to resource PID {}", theType, theId, valueOf);
} catch (ResourceNotFoundException e) {
if (myDaoConfig.isEnforceReferentialIntegrityOnWrite() == false) {
return null;
@ -86,7 +87,9 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
throw new InvalidRequestException("Resource " + resName + "/" + theId + " not found, specified in path: " + theNextPathsUnsplit);
}
ourLog.trace("Resource PID {} is of type {}", valueOf, target.getResourceType());
if (!theTypeString.equals(target.getResourceType())) {
ourLog.error("Resource {} with PID {} was not of type {}", target.getIdDt().getValue(), target.getId(), theTypeString);
throw new UnprocessableEntityException(
"Resource contains reference to " + theNextId.getValue() + " but resource with ID " + theNextId.getIdPart() + " is actually of type " + target.getResourceType());
}

View File

@ -23,16 +23,19 @@ package ca.uhn.fhir.jpa.dao.index;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
import ca.uhn.fhir.jpa.model.entity.ForcedId;
import ca.uhn.fhir.model.dstu2.resource.Specimen;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
@Service
public class IdHelperService {
@ -45,37 +48,54 @@ public class IdHelperService {
myForcedIdDao.delete(forcedId);
}
public Long translateForcedIdToPid(String theResourceName, String theResourceId) {
return translateForcedIdToPids(myDaoConfig, new IdDt(theResourceName, theResourceId), myForcedIdDao).get(0);
public Long translateForcedIdToPid(String theResourceName, String theResourceId) throws ResourceNotFoundException {
// We only pass 1 input in so only 0..1 will come back
IdDt id = new IdDt(theResourceName, theResourceId);
List<Long> matches = translateForcedIdToPids(myDaoConfig, myForcedIdDao, Collections.singletonList(id));
assert matches.size() <= 1;
if (matches.isEmpty()) {
throw new ResourceNotFoundException(id);
}
return matches.get(0);
}
public List<Long> translateForcedIdToPids(IIdType theId) {
return IdHelperService.translateForcedIdToPids(myDaoConfig, theId, myForcedIdDao);
public List<Long> translateForcedIdToPids(Collection<IIdType> theId) {
return IdHelperService.translateForcedIdToPids(myDaoConfig, myForcedIdDao, theId);
}
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IIdType theId, IForcedIdDao theForcedIdDao) {
Validate.isTrue(theId.hasIdPart());
static List<Long> translateForcedIdToPids(DaoConfig theDaoConfig, IForcedIdDao theForcedIdDao, Collection<IIdType> theId) {
theId.forEach(id -> Validate.isTrue(id.hasIdPart()));
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(theId)) {
return Collections.singletonList(theId.getIdPartAsLong());
} else {
List<ForcedId> forcedId;
if (theId.hasResourceType()) {
forcedId = theForcedIdDao.findByTypeAndForcedId(theId.getResourceType(), theId.getIdPart());
if (theId.isEmpty()) {
return Collections.emptyList();
}
List<Long> retVal = new ArrayList<>();
ListMultimap<String, String> typeToIds = MultimapBuilder.hashKeys().arrayListValues().build();
for (IIdType nextId : theId) {
if (theDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY && isValidPid(nextId)) {
retVal.add(nextId.getIdPartAsLong());
} else {
forcedId = theForcedIdDao.findByForcedId(theId.getIdPart());
}
if (!forcedId.isEmpty()) {
List<Long> retVal = new ArrayList<>(forcedId.size());
for (ForcedId next : forcedId) {
retVal.add(next.getResourcePid());
if (nextId.hasResourceType()) {
typeToIds.put(nextId.getResourceType(), nextId.getIdPart());
} else {
typeToIds.put("", nextId.getIdPart());
}
return retVal;
} else {
throw new ResourceNotFoundException(theId);
}
}
for (Map.Entry<String, Collection<String>> nextEntry : typeToIds.asMap().entrySet()) {
String nextResourceType = nextEntry.getKey();
Collection<String> nextIds = nextEntry.getValue();
if (isBlank(nextResourceType)) {
retVal.addAll(theForcedIdDao.findByForcedId(nextIds));
} else {
retVal.addAll(theForcedIdDao.findByTypeAndForcedId(nextResourceType, nextIds));
}
}
return retVal;
}
public String translatePidIdToForcedId(String theResourceType, Long theId) {

View File

@ -193,8 +193,7 @@ public class SearchParamWithInlineReferencesExtractor {
for (String nextQueryString : queryStringsToPopulate) {
if (isNotBlank(nextQueryString)) {
// FIXME: JA change to trace
ourLog.info("Adding composite unique SP: {}", nextQueryString);
ourLog.trace("Adding composite unique SP: {}", nextQueryString);
theParams.myCompositeStringUniques.add(new ResourceIndexedCompositeStringUnique(theEntity, nextQueryString));
}
}

View File

@ -51,14 +51,14 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer
private Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class);
public static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching";
private SubscribableChannel myProcessingChannel;
protected SubscribableChannel myMatchingChannel;
@Autowired
private FhirContext myFhirContext;
@Autowired
private SubscriptionMatchingSubscriber mySubscriptionMatchingSubscriber;
@Autowired
private SubscriptionChannelFactory mySubscriptionChannelFactory;
protected SubscriptionChannelFactory mySubscriptionChannelFactory;
/**
* Constructor
@ -68,11 +68,11 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer
}
public void start() {
if (myProcessingChannel == null) {
myProcessingChannel = mySubscriptionChannelFactory.newMatchingChannel(SUBSCRIPTION_MATCHING_CHANNEL_NAME);
if (myMatchingChannel == null) {
myMatchingChannel = mySubscriptionChannelFactory.newMatchingChannel(SUBSCRIPTION_MATCHING_CHANNEL_NAME);
}
myProcessingChannel.subscribe(mySubscriptionMatchingSubscriber);
ourLog.info("Subscription Matching Subscriber subscribed to Matching Channel {} with name {}", myProcessingChannel.getClass().getName(), SUBSCRIPTION_MATCHING_CHANNEL_NAME);
myMatchingChannel.subscribe(mySubscriptionMatchingSubscriber);
ourLog.info("Subscription Matching Subscriber subscribed to Matching Channel {} with name {}", myMatchingChannel.getClass().getName(), SUBSCRIPTION_MATCHING_CHANNEL_NAME);
}
@ -80,8 +80,8 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer
@PreDestroy
public void preDestroy() {
if (myProcessingChannel != null) {
myProcessingChannel.unsubscribe(mySubscriptionMatchingSubscriber);
if (myMatchingChannel != null) {
myMatchingChannel.unsubscribe(mySubscriptionMatchingSubscriber);
}
}
@ -115,8 +115,8 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer
protected void sendToProcessingChannel(final ResourceModifiedMessage theMessage) {
ourLog.trace("Sending resource modified message to processing channel");
Validate.notNull(myProcessingChannel, "A SubscriptionMatcherInterceptor has been registered without calling start() on it.");
myProcessingChannel.send(new ResourceModifiedJsonMessage(theMessage));
Validate.notNull(myMatchingChannel, "A SubscriptionMatcherInterceptor has been registered without calling start() on it.");
myMatchingChannel.send(new ResourceModifiedJsonMessage(theMessage));
}
public void setFhirContext(FhirContext theCtx) {
@ -152,6 +152,6 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer
@VisibleForTesting
LinkedBlockingQueueSubscribableChannel getProcessingChannelForUnitTest() {
return (LinkedBlockingQueueSubscribableChannel) myProcessingChannel;
return (LinkedBlockingQueueSubscribableChannel) myMatchingChannel;
}
}

View File

@ -24,22 +24,36 @@ import net.ttddyy.dsproxy.ExecutionInfo;
import net.ttddyy.dsproxy.QueryInfo;
import net.ttddyy.dsproxy.proxy.ParameterSetOperation;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.engine.jdbc.internal.BasicFormatterImpl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.*;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.trim;
public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuilder.SingleQueryExecution {
private boolean myCaptureQueryStackTrace;
/**
* This has an impact on performance! Use with caution.
*/
public boolean isCaptureQueryStackTrace() {
return myCaptureQueryStackTrace;
}
/**
* This has an impact on performance! Use with caution.
*/
public void setCaptureQueryStackTrace(boolean theCaptureQueryStackTrace) {
myCaptureQueryStackTrace = theCaptureQueryStackTrace;
}
@Override
public void execute(ExecutionInfo theExecutionInfo, List<QueryInfo> theQueryInfoList) {
final Queue<Query> queryList = provideQueryList();
for (QueryInfo next : theQueryInfoList) {
String sql = StringUtils.trim(next.getQuery());
String sql = trim(next.getQuery());
List<String> params;
if (next.getParametersList().size() > 0 && next.getParametersList().get(0).size() > 0) {
List<ParameterSetOperation> values = next
@ -53,9 +67,14 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
params = Collections.emptyList();
}
StackTraceElement[] stackTraceElements = null;
if (isCaptureQueryStackTrace()) {
stackTraceElements = Thread.currentThread().getStackTrace();
}
long elapsedTime = theExecutionInfo.getElapsedTime();
long startTime = System.currentTimeMillis() - elapsedTime;
queryList.add(new Query(sql, params, startTime, elapsedTime));
queryList.add(new Query(sql, params, startTime, elapsedTime, stackTraceElements));
}
}
@ -67,12 +86,14 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
private final List<String> myParams;
private final long myQueryTimestamp;
private final long myElapsedTime;
private final StackTraceElement[] myStackTrace;
Query(String theSql, List<String> theParams, long theQueryTimestamp, long theElapsedTime) {
Query(String theSql, List<String> theParams, long theQueryTimestamp, long theElapsedTime, StackTraceElement[] theStackTraceElements) {
mySql = theSql;
myParams = Collections.unmodifiableList(theParams);
myQueryTimestamp = theQueryTimestamp;
myElapsedTime = theElapsedTime;
myStackTrace = theStackTraceElements;
}
public long getQueryTimestamp() {
@ -113,10 +134,13 @@ public abstract class BaseCaptureQueriesListener implements ProxyDataSourceBuild
}
}
return retVal;
return trim(retVal);
}
public StackTraceElement[] getStackTrace() {
return myStackTrace;
}
}
}

View File

@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This is a query listener designed to be plugged into a {@link ProxyDataSourceBuilder proxy DataSource}.
@ -70,27 +71,77 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
return Collections.unmodifiableList(retVal);
}
private List<Query> getQueriesForCurrentThreadStartingWith(String theStart) {
String threadName = Thread.currentThread().getName();
return getQueriesStartingWith(theStart, threadName);
}
private List<Query> getQueriesStartingWith(String theStart, String theThreadName) {
return getCapturedQueries()
.stream()
.filter(t -> theThreadName == null || t.getThreadName().equals(theThreadName))
.filter(t -> t.getSql(false, false).toLowerCase().startsWith(theStart))
.collect(Collectors.toList());
}
private List<Query> getQueriesStartingWith(String theStart) {
return getQueriesStartingWith(theStart, null);
}
/**
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getSelectQueries() {
return getQueriesStartingWith("select");
}
/**
* Returns all INSERT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getInsertQueries() {
return getQueriesStartingWith("insert");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getUpdateQueries() {
return getQueriesStartingWith("update");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getDeleteQueries() {
return getQueriesStartingWith("delete");
}
/**
* Returns all SELECT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getSelectQueriesForCurrentThread() {
String currentThreadName = Thread.currentThread().getName();
return getCapturedQueries()
.stream()
.filter(t -> t.getThreadName().equals(currentThreadName))
.filter(t -> t.getSql(false, false).toLowerCase().contains("select"))
.collect(Collectors.toList());
return getQueriesForCurrentThreadStartingWith("select");
}
/**
* Returns all INSERT queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getInsertQueriesForCurrentThread() {
return getCapturedQueries()
.stream()
.filter(t -> t.getThreadName().equals(Thread.currentThread().getName()))
.filter(t -> t.getSql(false, false).toLowerCase().contains("insert"))
.collect(Collectors.toList());
return getQueriesForCurrentThreadStartingWith("insert");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getUpdateQueriesForCurrentThread() {
return getQueriesForCurrentThreadStartingWith("update");
}
/**
* Returns all UPDATE queries executed on the current thread - Index 0 is oldest
*/
public List<Query> getDeleteQueriesForCurrentThread() {
return getQueriesForCurrentThreadStartingWith("delete");
}
/**
@ -104,6 +155,17 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
ourLog.info("Select Queries:\n{}", String.join("\n", queries));
}
/**
* Log all captured SELECT queries
*/
public void logSelectQueries() {
List<String> queries = getSelectQueries()
.stream()
.map(CircularQueueCaptureQueriesListener::formatQueryAsSql)
.collect(Collectors.toList());
ourLog.info("Select Queries:\n{}", String.join("\n", queries));
}
/**
* Log first captured SELECT query
*/
@ -127,9 +189,67 @@ public class CircularQueueCaptureQueriesListener extends BaseCaptureQueriesListe
ourLog.info("Insert Queries:\n{}", String.join("\n", queries));
}
/**
* Log all captured INSERT queries
*/
public void logInsertQueries() {
List<String> queries = getInsertQueries()
.stream()
.map(CircularQueueCaptureQueriesListener::formatQueryAsSql)
.collect(Collectors.toList());
ourLog.info("Insert Queries:\n{}", String.join("\n", queries));
}
public int countSelectQueries() {
return getSelectQueries().size();
}
public int countInsertQueries() {
return getInsertQueries().size();
}
public int countUpdateQueries() {
return getUpdateQueries().size();
}
public int countDeleteQueries() {
return getDeleteQueries().size();
}
public int countSelectQueriesForCurrentThread() {
return getSelectQueriesForCurrentThread().size();
}
public int countInsertQueriesForCurrentThread() {
return getInsertQueriesForCurrentThread().size();
}
public int countUpdateQueriesForCurrentThread() {
return getUpdateQueriesForCurrentThread().size();
}
public int countDeleteQueriesForCurrentThread() {
return getDeleteQueriesForCurrentThread().size();
}
private static String formatQueryAsSql(Query theQuery) {
String formattedSql = theQuery.getSql(true, true);
return "Query at " + new InstantType(new Date(theQuery.getQueryTimestamp())).getValueAsString() + " took " + StopWatch.formatMillis(theQuery.getElapsedTime()) + " on Thread: " + theQuery.getThreadName() + "\nSQL:\n" + formattedSql;
StringBuilder b = new StringBuilder();
b.append("Query at ");
b.append(new InstantType(new Date(theQuery.getQueryTimestamp())).getValueAsString());
b.append(" took ").append(StopWatch.formatMillis(theQuery.getElapsedTime()));
b.append(" on Thread: ").append(theQuery.getThreadName());
b.append("\nSQL:\n").append(formattedSql);
if (theQuery.getStackTrace() != null) {
b.append("\nStack:\n ");
Stream<String> stackTraceStream = Arrays.stream(theQuery.getStackTrace())
.map(StackTraceElement::toString)
.filter(t->t.startsWith("ca."));
b.append(stackTraceStream.collect(Collectors.joining("\n ")));
}
b.append("\n");
return b.toString();
}
}

View File

@ -1,348 +0,0 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.util.TestUtil;
import net.ttddyy.dsproxy.QueryCount;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import static org.junit.Assert.assertEquals;
@TestPropertySource(properties = {
"scheduling_disabled=true"
})
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
@Autowired
private SingleQueryCountHolder myCountHolder;
@After
public void afterResetDao() {
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
}
@Test
public void testWritesPerformMinimalSqlStatements() {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val1");
p.addIdentifier().setSystem("sys2").setValue("val2");
ourLog.info("** About to perform write");
myCountHolder.clear();
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
ourLog.info("** Done performing write");
assertEquals(6, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getUpdate());
/*
* Not update the value
*/
p = new Patient();
p.setId(id);
p.addIdentifier().setSystem("sys1").setValue("val3");
p.addIdentifier().setSystem("sys2").setValue("val4");
ourLog.info("** About to perform write 2");
myCountHolder.clear();
myPatientDao.update(p).getId().toUnqualifiedVersionless();
ourLog.info("** Done performing write 2");
assertEquals(1, getQueryCount().getInsert());
assertEquals(2, getQueryCount().getUpdate());
assertEquals(0, getQueryCount().getDelete());
}
@Test
public void testSearch() {
for (int i = 0; i < 20; i++) {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val" + i);
myPatientDao.create(p);
}
myCountHolder.clear();
ourLog.info("** About to perform search");
IBundleProvider search = myPatientDao.search(new SearchParameterMap());
ourLog.info("** About to retrieve resources");
search.getResources(0, 20);
ourLog.info("** Done retrieving resources");
assertEquals(4, getQueryCount().getSelect());
assertEquals(2, getQueryCount().getInsert());
assertEquals(1, getQueryCount().getUpdate());
assertEquals(0, getQueryCount().getDelete());
}
private QueryCount getQueryCount() {
return myCountHolder.getQueryCountMap().get("");
}
@Test
public void testCreateClientAssignedId() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCountHolder.clear();
ourLog.info("** Starting Update Non-Existing resource with client assigned ID");
Patient p = new Patient();
p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, getQueryCount().getSelect());
assertEquals(4, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getDelete());
// Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID
assertEquals(1, getQueryCount().getUpdate());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count());
assertEquals(1, myForcedIdDao.count());
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
});
// Ok how about an update
myCountHolder.clear();
ourLog.info("** Starting Update Existing resource with client assigned ID");
p = new Patient();
p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(4, getQueryCount().getSelect());
assertEquals(1, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getDelete());
assertEquals(1, getQueryCount().getUpdate());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count());
assertEquals(1, myForcedIdDao.count());
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
});
}
@Test
public void testOneRowPerUpdate() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCountHolder.clear();
Patient p = new Patient();
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
assertEquals(3, getQueryCount().getInsert());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count());
});
myCountHolder.clear();
p = new Patient();
p.setId(id);
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, getQueryCount().getInsert());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count());
});
}
@Test
public void testUpdateReusesIndexes() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCountHolder.clear();
Patient pt = new Patient();
pt.setActive(true);
pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B");
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
myCountHolder.clear();
ourLog.info("** About to update");
pt.setId(id);
pt.getNameFirstRep().addGiven("GIVEN1C");
myPatientDao.update(pt);
assertEquals(0, getQueryCount().getDelete());
assertEquals(2, getQueryCount().getInsert());
}
@Test
public void testUpdateReusesIndexesString() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
SearchParameterMap m1 = new SearchParameterMap().add("family", new StringParam("family1")).setLoadSynchronous(true);
SearchParameterMap m2 = new SearchParameterMap().add("family", new StringParam("family2")).setLoadSynchronous(true);
myCountHolder.clear();
Patient pt = new Patient();
pt.addName().setFamily("FAMILY1");
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
myCountHolder.clear();
assertEquals(1, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
ourLog.info("** About to update");
pt = new Patient();
pt.setId(id);
pt.addName().setFamily("FAMILY2");
myPatientDao.update(pt);
assertEquals(0, getQueryCount().getDelete());
assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER
assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(1, myPatientDao.search(m2).size().intValue());
}
@Test
public void testUpdateReusesIndexesToken() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
SearchParameterMap m1 = new SearchParameterMap().add("gender", new TokenParam("male")).setLoadSynchronous(true);
SearchParameterMap m2 = new SearchParameterMap().add("gender", new TokenParam("female")).setLoadSynchronous(true);
myCountHolder.clear();
Patient pt = new Patient();
pt.setGender(Enumerations.AdministrativeGender.MALE);
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
assertEquals(0, getQueryCount().getSelect());
assertEquals(0, getQueryCount().getDelete());
assertEquals(3, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getUpdate());
assertEquals(1, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
/*
* Change a value
*/
ourLog.info("** About to update");
myCountHolder.clear();
pt = new Patient();
pt.setId(id);
pt.setGender(Enumerations.AdministrativeGender.FEMALE);
myPatientDao.update(pt);
/*
* Current SELECTs:
* Select the resource from HFJ_RESOURCE
* Select the version from HFJ_RES_VER
* Select the current token indexes
*/
assertEquals(3, getQueryCount().getSelect());
assertEquals(0, getQueryCount().getDelete());
assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER
assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(1, myPatientDao.search(m2).size().intValue());
myCountHolder.clear();
/*
* Drop a value
*/
ourLog.info("** About to update again");
pt = new Patient();
pt.setId(id);
myPatientDao.update(pt);
assertEquals(1, getQueryCount().getDelete());
assertEquals(1, getQueryCount().getInsert());
assertEquals(1, getQueryCount().getUpdate());
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
}
@Test
public void testUpdateReusesIndexesResourceLink() {
Organization org1 = new Organization();
org1.setName("org1");
IIdType orgId1 = myOrganizationDao.create(org1).getId().toUnqualifiedVersionless();
Organization org2 = new Organization();
org2.setName("org2");
IIdType orgId2 = myOrganizationDao.create(org2).getId().toUnqualifiedVersionless();
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
SearchParameterMap m1 = new SearchParameterMap().add("organization", new ReferenceParam(orgId1.getValue())).setLoadSynchronous(true);
SearchParameterMap m2 = new SearchParameterMap().add("organization", new ReferenceParam(orgId2.getValue())).setLoadSynchronous(true);
myCountHolder.clear();
Patient pt = new Patient();
pt.getManagingOrganization().setReference(orgId1.getValue());
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
myCountHolder.clear();
assertEquals(1, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
ourLog.info("** About to update");
pt = new Patient();
pt.setId(id);
pt.getManagingOrganization().setReference(orgId2.getValue());
myPatientDao.update(pt);
assertEquals(0, getQueryCount().getDelete());
assertEquals(1, getQueryCount().getInsert()); // Add an entry to HFJ_RES_VER
assertEquals(2, getQueryCount().getUpdate()); // Update SPIDX_STRING and HFJ_RESOURCE
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(1, myPatientDao.search(m2).size().intValue());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -2166,14 +2166,20 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
ourLog.info("P1[{}] P2[{}] O1[{}] O2[{}] D1[{}]", patientId01, patientId02, obsId01, obsId02, drId01);
List<Observation> result = toList(
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId01"))));
List<Observation> result;
// With an ID that exists
result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId01"))));
assertEquals(1, result.size());
assertEquals(obsId01.getIdPart(), result.get(0).getIdElement().getIdPart());
// Now with an alphanumeric ID that doesn't exist
myCaptureQueriesListener.clear();
result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("testSearchResourceLinkWithTextLogicalId99"))));
assertEquals(0, result.size());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(result.toString(),0, result.size());
// And with a numeric ID that doesn't exist
result = toList(myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_SUBJECT, new ReferenceParam("999999999999999"))));
assertEquals(0, result.size());

View File

@ -1,37 +1,47 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchStatusEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.api.SummaryEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean;
import org.springframework.test.context.TestPropertySource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@TestPropertySource(properties = {
"scheduling_disabled=true"
})
@SuppressWarnings({"unchecked", "deprecation", "Duplicates"})
public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@ -41,6 +51,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Before
public void before() {
mySearchCoordinatorSvcImpl = (SearchCoordinatorSvcImpl) AopProxyUtils.getSingletonTarget(mySearchCoordinatorSvc);
myCaptureQueriesListener.setCaptureQueryStackTrace(true);
}
@After
@ -48,10 +59,10 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myCaptureQueriesListener.setCaptureQueryStackTrace(false);
}
@Before
public void start() {
private void create200Patients() {
runInTransaction(() -> {
for (int i = 0; i < 200; i++) {
Patient p = new Patient();
@ -65,6 +76,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchCountOnly() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
@ -82,6 +94,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchCountWithMultipleIndexesOnOneResource() {
create200Patients();
// Already have 200, let's add number 201 with a bunch of similar names
Patient p = new Patient();
@ -136,6 +149,8 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchTotalAccurateForSlowLoading() {
create200Patients();
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(25);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(10);
@ -164,6 +179,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchCountAndData() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
@ -200,6 +216,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchRightUpToActualNumberExistingThenFetchAnotherPage() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(200, -1));
@ -254,6 +271,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchOnlySmallBatches() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
@ -379,6 +397,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchMoreThanFirstPageSizeInFirstPage() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, -1));
@ -414,6 +433,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchUnlimited() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, -1));
@ -472,7 +492,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testFetchSecondBatchInManyThreads() throws Throwable {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, -1));
/*
@ -493,7 +513,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
* 20 should be prefetched since that's the initial page size
*/
waitForSize(20, () -> runInTransaction(()-> mySearchEntityDao.findByUuid(uuid).getNumFound()));
waitForSize(20, () -> runInTransaction(() -> mySearchEntityDao.findByUuid(uuid).getNumFound()));
runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuid(uuid);
assertEquals(20, search.getNumFound());
@ -541,6 +561,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
@Test
public void testSearchThatOnlyReturnsASmallResult() {
create200Patients();
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
@ -568,6 +589,439 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
}
/**
* A search with a big list of OR clauses for references should use a single SELECT ... WHERE .. IN
* and not a whole bunch of SQL ORs.
*/
@Test
public void testReferenceOrLinksUseInList() {
List<Long> ids = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Organization org = new Organization();
org.setActive(true);
ids.add(myOrganizationDao.create(org).getId().getIdPartAsLong());
}
for (int i = 0; i < 5; i++) {
Patient pt = new Patient();
pt.setManagingOrganization(new Reference("Organization/" + ids.get(i)));
myPatientDao.create(pt).getId().getIdPartAsLong();
}
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_ORGANIZATION, new ReferenceOrListParam()
.addOr(new ReferenceParam("Organization/" + ids.get(0)))
.addOr(new ReferenceParam("Organization/" + ids.get(1)))
.addOr(new ReferenceParam("Organization/" + ids.get(2)))
.addOr(new ReferenceParam("Organization/" + ids.get(3)))
.addOr(new ReferenceParam("Organization/" + ids.get(4)))
);
map.setLoadSynchronous(true);
IBundleProvider search = myPatientDao.search(map);
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
List<String> queries = myCaptureQueriesListener
.getSelectQueriesForCurrentThread()
.stream()
.map(t -> t.getSql(true, false))
.collect(Collectors.toList());
String resultingQueryNotFormatted = queries.get(0);
assertEquals(resultingQueryNotFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "Patient.managingOrganization"));
assertThat(resultingQueryNotFormatted, containsString("TARGET_RESOURCE_ID in ('" + ids.get(0) + "' , '" + ids.get(1) + "' , '" + ids.get(2) + "' , '" + ids.get(3) + "' , '" + ids.get(4) + "')"));
// Ensure that the search actually worked
assertEquals(5, search.size().intValue());
}
@After
public void afterResetDao() {
myDaoConfig.setResourceMetaCountHardLimit(new DaoConfig().getResourceMetaCountHardLimit());
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
}
@Test
public void testWritesPerformMinimalSqlStatements() {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val1");
p.addIdentifier().setSystem("sys2").setValue("val2");
ourLog.info("** About to perform write");
myCaptureQueriesListener.clear();
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
ourLog.info("** Done performing write");
assertEquals(6, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
/*
* Not update the value
*/
p = new Patient();
p.setId(id);
p.addIdentifier().setSystem("sys1").setValue("val3");
p.addIdentifier().setSystem("sys2").setValue("val4");
ourLog.info("** About to perform write 2");
myCaptureQueriesListener.clear();
myPatientDao.update(p).getId().toUnqualifiedVersionless();
ourLog.info("** Done performing write 2");
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@Test
public void testSearch() {
create200Patients();
for (int i = 0; i < 20; i++) {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val" + i);
myPatientDao.create(p);
}
myCaptureQueriesListener.clear();
ourLog.info("** About to perform search");
IBundleProvider search = myPatientDao.search(new SearchParameterMap().setLoadSynchronous(false));
ourLog.info("** About to retrieve resources");
search.getResources(0, 20);
ourLog.info("** Done retrieving resources");
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countSelectQueries());
// Batches of 30 are written for each query - so 9 inserts total
assertEquals(9, myCaptureQueriesListener.countInsertQueries());
assertEquals(1, myCaptureQueriesListener.countUpdateQueries());
assertEquals(0, myCaptureQueriesListener.countDeleteQueries());
assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
}
@Test
public void testCreateClientAssignedId() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCaptureQueriesListener.clear();
ourLog.info("** Starting Update Non-Existing resource with client assigned ID");
Patient p = new Patient();
p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
// Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count());
assertEquals(1, myForcedIdDao.count());
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
});
// Ok how about an update
myCaptureQueriesListener.clear();
ourLog.info("** Starting Update Existing resource with client assigned ID");
p = new Patient();
p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count());
assertEquals(1, myForcedIdDao.count());
assertEquals(1, myResourceIndexedSearchParamTokenDao.count());
});
}
@Test
public void testOneRowPerUpdate() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCaptureQueriesListener.clear();
Patient p = new Patient();
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count());
});
myCaptureQueriesListener.clear();
p = new Patient();
p.setId(id);
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count());
});
}
@Test
public void testUpdateReusesIndexes() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCaptureQueriesListener.clear();
Patient pt = new Patient();
pt.setActive(true);
pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B");
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
myCaptureQueriesListener.clear();
ourLog.info("** About to update");
pt.setId(id);
pt.getNameFirstRep().addGiven("GIVEN1C");
myPatientDao.update(pt);
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
}
@Test
public void testUpdateReusesIndexesString() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
SearchParameterMap m1 = new SearchParameterMap().add("family", new StringParam("family1")).setLoadSynchronous(true);
SearchParameterMap m2 = new SearchParameterMap().add("family", new StringParam("family2")).setLoadSynchronous(true);
myCaptureQueriesListener.clear();
Patient pt = new Patient();
pt.addName().setFamily("FAMILY1");
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
myCaptureQueriesListener.clear();
assertEquals(1, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
ourLog.info("** About to update");
pt = new Patient();
pt.setId(id);
pt.addName().setFamily("FAMILY2");
myPatientDao.update(pt);
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(1, myPatientDao.search(m2).size().intValue());
}
@Test
public void testUpdateReusesIndexesToken() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
SearchParameterMap m1 = new SearchParameterMap().add("gender", new TokenParam("male")).setLoadSynchronous(true);
SearchParameterMap m2 = new SearchParameterMap().add("gender", new TokenParam("female")).setLoadSynchronous(true);
myCaptureQueriesListener.clear();
Patient pt = new Patient();
pt.setGender(Enumerations.AdministrativeGender.MALE);
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(3, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(1, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
/*
* Change a value
*/
ourLog.info("** About to update");
myCaptureQueriesListener.clear();
pt = new Patient();
pt.setId(id);
pt.setGender(Enumerations.AdministrativeGender.FEMALE);
myPatientDao.update(pt);
/*
* Current SELECTs:
* Select the resource from HFJ_RESOURCE
* Select the version from HFJ_RES_VER
* Select the current token indexes
*/
assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread());
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(1, myPatientDao.search(m2).size().intValue());
myCaptureQueriesListener.clear();
/*
* Drop a value
*/
ourLog.info("** About to update again");
pt = new Patient();
pt.setId(id);
myPatientDao.update(pt);
assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread());
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
}
@Test
public void testUpdateReusesIndexesResourceLink() {
Organization org1 = new Organization();
org1.setName("org1");
IIdType orgId1 = myOrganizationDao.create(org1).getId().toUnqualifiedVersionless();
Organization org2 = new Organization();
org2.setName("org2");
IIdType orgId2 = myOrganizationDao.create(org2).getId().toUnqualifiedVersionless();
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
SearchParameterMap m1 = new SearchParameterMap().add("organization", new ReferenceParam(orgId1.getValue())).setLoadSynchronous(true);
SearchParameterMap m2 = new SearchParameterMap().add("organization", new ReferenceParam(orgId2.getValue())).setLoadSynchronous(true);
myCaptureQueriesListener.clear();
Patient pt = new Patient();
pt.getManagingOrganization().setReference(orgId1.getValue());
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
myCaptureQueriesListener.clear();
assertEquals(1, myPatientDao.search(m1).size().intValue());
assertEquals(0, myPatientDao.search(m2).size().intValue());
ourLog.info("** About to update");
pt = new Patient();
pt.setId(id);
pt.getManagingOrganization().setReference(orgId2.getValue());
myPatientDao.update(pt);
assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread());
assertEquals(1, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); // Add an entry to HFJ_RES_VER
assertEquals(2, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); // Update SPIDX_STRING and HFJ_RESOURCE
assertEquals(0, myPatientDao.search(m1).size().intValue());
assertEquals(1, myPatientDao.search(m2).size().intValue());
}
@Test
public void testReferenceOrLinksUseInList_ForcedIds() {
List<String> ids = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Organization org = new Organization();
org.setId("ORG"+i);
org.setActive(true);
runInTransaction(()->{
IIdType id = myOrganizationDao.update(org).getId();
ids.add(id.getIdPart());
});
// org = myOrganizationDao.read(id);
// assertTrue(org.getActive());
}
runInTransaction(()->{
for (ResourceTable next : myResourceTableDao.findAll()) {
ourLog.info("Resource pid {} of type {}", next.getId(), next.getResourceType());
}
});
for (int i = 0; i < 5; i++) {
Patient pt = new Patient();
pt.setManagingOrganization(new Reference("Organization/" + ids.get(i)));
myPatientDao.create(pt).getId().getIdPartAsLong();
}
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_ORGANIZATION, new ReferenceOrListParam()
.addOr(new ReferenceParam("Organization/" + ids.get(0)))
.addOr(new ReferenceParam("Organization/" + ids.get(1)))
.addOr(new ReferenceParam("Organization/" + ids.get(2)))
.addOr(new ReferenceParam("Organization/" + ids.get(3)))
.addOr(new ReferenceParam("Organization/" + ids.get(4)))
);
map.setLoadSynchronous(true);
IBundleProvider search = myPatientDao.search(map);
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
List<String> queries = myCaptureQueriesListener
.getSelectQueriesForCurrentThread()
.stream()
.map(t -> t.getSql(true, false))
.collect(Collectors.toList());
// Forced ID resolution
String resultingQueryNotFormatted = queries.get(0);
assertThat(resultingQueryNotFormatted, containsString("RESOURCE_TYPE='Organization'"));
assertThat(resultingQueryNotFormatted, containsString("FORCED_ID in ('ORG0' , 'ORG1' , 'ORG2' , 'ORG3' , 'ORG4')"));
// The search itself
resultingQueryNotFormatted = queries.get(1);
assertEquals(resultingQueryNotFormatted, 1, StringUtils.countMatches(resultingQueryNotFormatted, "Patient.managingOrganization"));
assertThat(resultingQueryNotFormatted, matchesPattern(".*TARGET_RESOURCE_ID in \\('[0-9]+' , '[0-9]+' , '[0-9]+' , '[0-9]+' , '[0-9]+'\\).*"));
// Ensure that the search actually worked
assertEquals(5, search.size().intValue());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -90,6 +90,38 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
}
@Test
public void testSearchByExternalReference() {
myDaoConfig.setAllowExternalReferences(true);
Patient patient = new Patient();
patient.addName().setFamily("FooName");
IIdType patientId = ourClient.create().resource(patient).execute().getId();
//Reference patientReference = new Reference("Patient/" + patientId.getIdPart()); <--- this works
Reference patientReference = new Reference(patientId); // <--- this is seen as an external reference
Media media = new Media();
Attachment attachment = new Attachment();
attachment.setLanguage("ENG");
media.setContent(attachment);
media.setSubject(patientReference);
media.setType(Media.DigitalMediaType.AUDIO);
IIdType mediaId = ourClient.create().resource(media).execute().getId();
// Act
Bundle returnedBundle = ourClient.search()
.forResource(Observation.class)
.where(Observation.CONTEXT.hasId(patientReference.getReference()))
.returnBundle(Bundle.class)
.execute();
// Assert
assertEquals(0, returnedBundle.getEntry().size());
}
/**
* See #872
*/
@ -1843,10 +1875,9 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
assertEquals(id.withVersion("3").getValue(), history.getEntry().get(0).getResource().getId());
assertEquals(1, ((Patient) history.getEntry().get(0).getResource()).getName().size());
assertEquals(id.withVersion("2").getValue(), history.getEntry().get(1).getResource().getId());
assertEquals(HTTPVerb.DELETE, history.getEntry().get(1).getRequest().getMethodElement().getValue());
assertEquals("http://localhost:" + ourPort + "/fhir/context/Patient/" + id.getIdPart() + "/_history/2", history.getEntry().get(1).getRequest().getUrl());
assertEquals(0, ((Patient) history.getEntry().get(1).getResource()).getName().size());
assertEquals(null, history.getEntry().get(1).getResource());
assertEquals(id.withVersion("1").getValue(), history.getEntry().get(2).getResource().getId());
assertEquals(1, ((Patient) history.getEntry().get(2).getResource()).getName().size());

View File

@ -339,7 +339,7 @@ public class ExpungeR4Test extends BaseResourceProviderR4Test {
IBundleProvider search = myPatientDao.search(new SearchParameterMap());
assertEquals(2, search.size().intValue());
search.getResources(0, 2);
assertEquals(2, search.getResources(0, 2).size());
runInTransaction(() -> {
assertEquals(2, mySearchResultDao.count());

View File

@ -703,6 +703,41 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
}
}
@Test
public void testSearchByExternalReference() {
myDaoConfig.setAllowExternalReferences(true);
Patient patient = new Patient();
patient.addName().setFamily("FooName");
IIdType patientId = ourClient.create().resource(patient).execute().getId();
//Reference patientReference = new Reference("Patient/" + patientId.getIdPart()); <--- this works
Reference patientReference = new Reference(patientId); // <--- this is seen as an external reference
Media media = new Media();
Attachment attachment = new Attachment();
attachment.setLanguage("ENG");
media.setContent(attachment);
media.setSubject(patientReference);
IIdType mediaId = ourClient.create().resource(media).execute().getId();
// Search for wrong type
Bundle returnedBundle = ourClient.search()
.forResource(Observation.class)
.where(Observation.ENCOUNTER.hasId(patientReference.getReference()))
.returnBundle(Bundle.class)
.execute();
assertEquals(0, returnedBundle.getEntry().size());
// Search for right type
returnedBundle = ourClient.search()
.forResource(Media.class)
.where(Media.SUBJECT.hasId(patientReference.getReference()))
.returnBundle(Bundle.class)
.execute();
assertEquals(mediaId, returnedBundle.getEntryFirstRep().getResource().getIdElement());
}
@Test
public void testCreateResourceConditional() throws IOException {
String methodName = "testCreateResourceConditional";
@ -2161,15 +2196,16 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
patient.setId(id);
ourClient.update().resource(patient).execute();
ourLog.info("Res ID: {}", id);
Bundle history = ourClient.history().onInstance(id).andReturnBundle(Bundle.class).prettyPrint().summaryMode(SummaryEnum.DATA).execute();
assertEquals(3, history.getEntry().size());
assertEquals(id.withVersion("3").getValue(), history.getEntry().get(0).getResource().getId());
assertEquals(1, ((Patient) history.getEntry().get(0).getResource()).getName().size());
assertEquals(id.withVersion("2").getValue(), history.getEntry().get(1).getResource().getId());
assertEquals(HTTPVerb.DELETE, history.getEntry().get(1).getRequest().getMethodElement().getValue());
assertEquals("http://localhost:" + ourPort + "/fhir/context/Patient/" + id.getIdPart() + "/_history/2", history.getEntry().get(1).getRequest().getUrl());
assertEquals(0, ((Patient) history.getEntry().get(1).getResource()).getName().size());
assertEquals(null, history.getEntry().get(1).getResource());
assertEquals(id.withVersion("1").getValue(), history.getEntry().get(2).getResource().getId());
assertEquals(1, ((Patient) history.getEntry().get(2).getResource()).getName().size());

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.model.entity;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.Constants;
import org.hibernate.annotations.OptimisticLock;
import javax.persistence.*;
@ -61,6 +62,22 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
@OptimisticLock(excluded = true)
private Date myUpdated;
/**
* This is stored as an optimization to avoid neeind to query for this
* after an update
*/
@Transient
private transient String myTransientForcedId;
public String getTransientForcedId() {
return myTransientForcedId;
}
public void setTransientForcedId(String theTransientForcedId) {
myTransientForcedId = theTransientForcedId;
}
public abstract BaseTag addTag(TagDefinition theDef);
@Override
@ -94,7 +111,16 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
public abstract Long getId();
@Override
public abstract IdDt getIdDt();
public IdDt getIdDt() {
if (getForcedId() == null) {
Long id = getResourceId();
return new IdDt(getResourceType() + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
} else {
// Avoid a join query if possible
String forcedId = getTransientForcedId() != null ? getTransientForcedId() : getForcedId().getForcedId();
return new IdDt(getResourceType() + '/' + forcedId + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
}
}
@Override
public InstantDt getPublished() {
@ -109,6 +135,10 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
myPublished = thePublished;
}
public void setPublished(InstantDt thePublished) {
myPublished = thePublished.getValue();
}
@Override
public abstract Long getResourceId();
@ -126,6 +156,10 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
myUpdated = theUpdated;
}
public void setUpdated(InstantDt theUpdated) {
myUpdated = theUpdated.getValue();
}
@Override
public Date getUpdatedDate() {
return myUpdated;
@ -143,12 +177,4 @@ public abstract class BaseHasResource implements IBaseResourceEntity {
myHasTags = theHasTags;
}
public void setPublished(InstantDt thePublished) {
myPublished = thePublished.getValue();
}
public void setUpdated(InstantDt theUpdated) {
myUpdated = theUpdated.getValue();
}
}

View File

@ -51,7 +51,7 @@ public class ForcedId {
private Long myId;
@JoinColumn(name = "RESOURCE_PID", nullable = false, updatable = false, foreignKey = @ForeignKey(name = "FK_FORCEDID_RESOURCE"))
@OneToOne()
@OneToOne(fetch = FetchType.LAZY)
private ResourceTable myResource;
@Column(name = "RESOURCE_PID", nullable = false, updatable = false, insertable = false)

View File

@ -124,16 +124,6 @@ public class ResourceHistoryTable extends BaseHasResource implements Serializabl
myId = theId;
}
@Override
public IdDt getIdDt() {
if (getForcedId() == null) {
Long id = myResourceId;
return new IdDt(myResourceType + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
} else {
return new IdDt(getForcedId().getResourceType() + '/' + getForcedId().getForcedId() + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
}
}
public byte[] getResource() {
return myResource;
}

View File

@ -260,16 +260,6 @@ public class ResourceTable extends BaseHasResource implements Serializable {
myId = theId;
}
@Override
public IdDt getIdDt() {
if (getForcedId() == null) {
Long id = myId;
return new IdDt(myResourceType + '/' + id + '/' + Constants.PARAM_HISTORY + '/' + myVersion);
} else {
return new IdDt(getForcedId().getResourceType() + '/' + getForcedId().getForcedId() + '/' + Constants.PARAM_HISTORY + '/' + myVersion);
}
}
public Long getIndexStatus() {
return myIndexStatus;
}
@ -577,6 +567,7 @@ public class ResourceTable extends BaseHasResource implements Serializable {
retVal.setResourceId(myId);
retVal.setResourceType(myResourceType);
retVal.setVersion(myVersion);
retVal.setTransientForcedId(getTransientForcedId());
retVal.setPublished(getPublished());
retVal.setUpdated(getUpdated());

View File

@ -30,7 +30,6 @@ import com.google.common.collect.Sets;
import org.apache.commons.lang3.Validate;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
import java.lang.reflect.InvocationTargetException;
@ -39,20 +38,21 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Component
public class InterceptorService implements IInterceptorRegistry, IInterceptorBroadcaster {
private final List<Object> myInterceptors = new ArrayList<>();
private final ListMultimap<Pointcut, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create();
private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
private final Object myRegistryMutex = new Object();
private String myName;
private final ThreadLocal<ListMultimap<Pointcut, BaseInvoker>> myThreadlocalInvokers = new ThreadLocal<>();
private boolean myThreadlocalInvokersEnabled;
/**
* Constructor
*/
public InterceptorService() {
public InterceptorService(String theName) {
super();
myName = theName;
}
public boolean isThreadlocalInvokersEnabled() {
@ -75,6 +75,10 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
registerAnonymousHookForUnitTest(thePointcut, DEFAULT_ORDER, theHook);
}
public void setName(String theName) {
myName = theName;
}
@Override
public void registerAnonymousHookForUnitTest(Pointcut thePointcut, int theOrder, IAnonymousLambdaHook theHook) {
Validate.notNull(thePointcut);

View File

@ -331,8 +331,10 @@ public class InterceptorServiceTest {
@ComponentScan(basePackages = "ca.uhn.fhir.jpa.model")
static class InterceptorRegistryTestCtxConfig {
@Autowired
private IInterceptorRegistry myInterceptorRegistry;
@Bean
public InterceptorService interceptorService() {
return new InterceptorService("test");
}
/**
* Note: Orders are deliberately reversed to make sure we get the orders right
@ -341,7 +343,7 @@ public class InterceptorServiceTest {
@Bean
public MyTestInterceptorTwo interceptor1() {
MyTestInterceptorTwo retVal = new MyTestInterceptorTwo();
myInterceptorRegistry.registerInterceptor(retVal);
interceptorService().registerInterceptor(retVal);
return retVal;
}
@ -352,7 +354,7 @@ public class InterceptorServiceTest {
@Bean
public MyTestInterceptorOne interceptor2() {
MyTestInterceptorOne retVal = new MyTestInterceptorOne();
myInterceptorRegistry.registerInterceptor(retVal);
interceptorService().registerInterceptor(retVal);
return retVal;
}

View File

@ -308,6 +308,28 @@ public class CanonicalSubscription implements Serializable, Cloneable {
public void setSubjectTemplate(String theSubjectTemplate) {
mySubjectTemplate = theSubjectTemplate;
}
@Override
public boolean equals(Object theO) {
if (this == theO) return true;
if (theO == null || getClass() != theO.getClass()) return false;
EmailDetails that = (EmailDetails) theO;
return new EqualsBuilder()
.append(myFrom, that.myFrom)
.append(mySubjectTemplate, that.mySubjectTemplate)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(myFrom)
.append(mySubjectTemplate)
.toHashCode();
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)

View File

@ -51,8 +51,12 @@ import static java.util.stream.Collectors.toList;
public class SubscriptionCanonicalizer<S extends IBaseResource> {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionCanonicalizer.class);
final FhirContext myFhirContext;
@Autowired
FhirContext myFhirContext;
public SubscriptionCanonicalizer(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
public CanonicalSubscription canonicalize(S theSubscription) {
switch (myFhirContext.getVersion().getVersion()) {

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription.module.config;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.subscription.module.cache.ISubscribableChannelFactory;
import ca.uhn.fhir.jpa.subscription.module.cache.LinkedBlockingQueueSubscribableChannelFactory;
import org.springframework.context.annotation.Bean;
@ -38,4 +39,11 @@ public abstract class BaseSubscriptionConfig {
public ISubscribableChannelFactory blockingQueueSubscriptionDeliveryChannelFactory() {
return new LinkedBlockingQueueSubscribableChannelFactory();
}
@Bean
public InterceptorService interceptorRegistry() {
return new InterceptorService("hapi-fhir-jpa-subscription");
}
}

View File

@ -0,0 +1,67 @@
package ca.uhn.fhir.jpa.subscription.module.config;
/*-
* #%L
* HAPI FHIR Subscription Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu2;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryDstu2;
import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport;
import org.hl7.fhir.instance.hapi.validation.IValidationSupport;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
public class SubscriptionDstu2Config extends BaseSubscriptionConfig {
@Override
public FhirContext fhirContext() {
return fhirContextDstu2();
}
@Bean
@Primary
public FhirContext fhirContextDstu2() {
FhirContext retVal = FhirContext.forDstu2();
// Don't strip versions in some places
ParserOptions parserOptions = retVal.getParserOptions();
parserOptions.setDontStripVersionsFromReferencesAtPaths("AuditEvent.entity.reference");
return retVal;
}
@Bean
public ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryDstu2();
}
@Bean(autowire = Autowire.BY_TYPE)
public SearchParamExtractorDstu2 searchParamExtractor() {
return new SearchParamExtractorDstu2();
}
@Primary
@Bean(autowire = Autowire.BY_NAME, name = "myJpaValidationSupportChainDstu2")
public IValidationSupport validationSupportChainDstu2() {
return new DefaultProfileValidationSupport();
}
}

View File

@ -82,8 +82,17 @@ public class StandaloneSubscriptionMessageHandler implements MessageHandler {
}
private boolean isSubscription(ResourceModifiedMessage theResourceModifiedMessage) {
String resourceType;
IIdType id = theResourceModifiedMessage.getId(myFhirContext);
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(id.getResourceType());
if (id != null) {
resourceType = id.getResourceType();
} else {
resourceType = theResourceModifiedMessage.getNewPayload(myFhirContext).getIdElement().getResourceType();
}
if (resourceType == null) {
return false;
}
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(resourceType);
return resourceDef.getName().equals(ResourceTypeEnum.SUBSCRIPTION.getCode());
}

View File

@ -1,10 +1,13 @@
package ca.uhn.fhir.jpa.subscription.module;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionCanonicalizer;
import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryJsonMessage;
import ca.uhn.fhir.jpa.subscription.module.subscriber.ResourceDeliveryMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.util.Lists;
import org.hamcrest.Matchers;
import org.hl7.fhir.r4.model.Subscription;
import org.junit.Test;
import java.io.IOException;
@ -12,6 +15,7 @@ import java.util.HashMap;
import java.util.List;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public class CanonicalSubscriptionTest {
@ -49,6 +53,22 @@ public class CanonicalSubscriptionTest {
assertThat(s.getChannelExtensions("key3"), Matchers.empty());
}
@Test
public void emailDetailsEquals() {
SubscriptionCanonicalizer<Subscription> canonicalizer = new SubscriptionCanonicalizer<>(FhirContext.forR4());
CanonicalSubscription sub1 = canonicalizer.canonicalize(makeEmailSubscription());
CanonicalSubscription sub2 = canonicalizer.canonicalize(makeEmailSubscription());
assertTrue(sub1.equals(sub2));
}
private Subscription makeEmailSubscription() {
Subscription retval = new Subscription();
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
channel.setType(Subscription.SubscriptionChannelType.EMAIL);
retval.setChannel(channel);
return retval;
}
private CanonicalSubscription serializeAndDeserialize(CanonicalSubscription theSubscription) throws IOException {
ResourceDeliveryJsonMessage resourceDeliveryMessage = new ResourceDeliveryJsonMessage();

View File

@ -121,6 +121,10 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory {
entry.getRequest().getMethodElement().setValueAsString(httpVerb);
entry.getRequest().getUrlElement().setValue(next.getId());
}
if ("DELETE".equals(httpVerb)) {
entry.setResource(null);
}
}
/*
@ -212,6 +216,9 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory {
entry.getRequest().setUrl(id.getValue());
}
}
if ("DELETE".equals(httpVerb)) {
entry.setResource(null);
}
String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource);
if (searchMode != null) {

View File

@ -123,6 +123,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
entry.getRequest().getMethodElement().setValueAsString(httpVerb);
entry.getRequest().getUrlElement().setValue(next.getId());
}
if ("DELETE".equals(httpVerb)) {
entry.setResource(null);
}
}
/*
@ -216,6 +219,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
entry.getRequest().setUrl(id.getValue());
}
}
if ("DELETE".equals(httpVerb)) {
entry.setResource(null);
}
// Populate Response
if ("1".equals(id.getVersionIdPart())) {

View File

@ -212,6 +212,7 @@
<configuration>
<attachClasses>true</attachClasses>
<failOnMissingWebXml>false</failOnMissingWebXml>
<packagingExcludes>WEB-INF/lib/*.jar</packagingExcludes>
</configuration>
</plugin>
</plugins>

20
pom.xml
View File

@ -529,7 +529,7 @@
<activation_api_version>1.2.0</activation_api_version>
<apache_karaf_version>4.2.2</apache_karaf_version>
<aries_spifly_version>1.2</aries_spifly_version>
<caffeine_version>2.6.2</caffeine_version>
<caffeine_version>2.7.0</caffeine_version>
<commons_codec_version>1.11</commons_codec_version>
<commons_text_version>1.6</commons_text_version>
<commons_io_version>2.6</commons_io_version>
@ -547,14 +547,14 @@
<jetty_version>9.4.14.v20181114</jetty_version>
<jsr305_version>3.0.2</jsr305_version>
<!--<hibernate_version>5.2.10.Final</hibernate_version>-->
<hibernate_version>5.4.1.Final</hibernate_version>
<hibernate_version>5.4.2.Final</hibernate_version>
<!-- Update lucene version when you update hibernate-search version -->
<hibernate_search_version>5.11.1.Final</hibernate_search_version>
<lucene_version>5.5.5</lucene_version>
<hibernate_validator_version>5.4.1.Final</hibernate_validator_version>
<hibernate_validator_version>5.4.2.Final</hibernate_validator_version>
<httpcore_version>4.4.6</httpcore_version>
<httpclient_version>4.5.3</httpclient_version>
<jackson_version>2.9.7</jackson_version>
<jackson_version>2.9.8</jackson_version>
<maven_assembly_plugin_version>3.1.0</maven_assembly_plugin_version>
<maven_license_plugin_version>1.8</maven_license_plugin_version>
<resteasy_version>4.0.0.Beta3</resteasy_version>
@ -564,8 +564,8 @@
<servicemix_saxon_version>9.5.1-5_1</servicemix_saxon_version>
<servicemix_xmlresolver_version>1.2_5</servicemix_xmlresolver_version>
<slf4j_version>1.7.25</slf4j_version>
<spring_version>5.1.5.RELEASE</spring_version>
<spring_data_version>2.1.3.RELEASE</spring_data_version>
<spring_version>5.1.6.RELEASE</spring_version>
<spring_data_version>2.1.5.RELEASE</spring_data_version>
<spring_boot_version>2.1.1.RELEASE</spring_boot_version>
<spring_retry_version>1.2.2.RELEASE</spring_retry_version>
@ -1087,7 +1087,7 @@
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>1.16</version>
<version>1.17.1</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
@ -1187,7 +1187,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<version>2.25.1</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
@ -1454,7 +1454,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -1530,7 +1530,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>1.16</version>
<version>1.19</version>
<configuration>
<verbose>true</verbose>
<addSvnKeyWords>false</addSvnKeyWords>

View File

@ -11,9 +11,14 @@
The version of a few dependencies have been bumped to the
latest versions (dependent HAPI modules listed in brackets):
<![CDATA[
<ul>
<li>Spring (JPA): 5.1.3.RELEASE -&gt; 5.1.5.RELEASE</li>
</ul>
<ul>
<li>Hibernate (JPA): 5.4.1 -&gt; 5.4.2</li>
<li>Jackson (JPA): 2.9.7 -&gt; 2.9.8</li>
<li>Spring (JPA): 5.1.3.RELEASE -&gt; 5.1.6.RELEASE</li>
<li>Spring-Data (JPA): 2.1.3.RELEASE -&gt; 2.1.5.RELEASE</li>
<li>Caffeine (JPA): 2.6.2 -&gt; 2.7.0</li>
<li>JANSI (CLI): 1.16 -&gt; 1.17.1</li>
</ul>
]]>
</action>
<action type="add">
@ -103,6 +108,23 @@
<action type="fix">
A non-threadsafe use of DateFormat was cleaned up in the StopWatch class.
</action>
<action type="add">
When performing a search in the JPA server where one of the parameters is a
reference with multiple values (e.g. Patient?organization=A,B) the generated
SQL was previously a set of OR clauses and this has been collapsed into a single
IN clause for better performance.
</action>
<action type="fix">
When returning the results of a history operation from a HAPI FHIR server,
any entries with a method of DELETE contained a stub resource in
Bundle.entry.resource, even though the FHIR spec states that this field
should be empty. This was corrected.
</action>
<action type="change">
The hapi-fhir-testpage-overlay project no longer includes any library JARs
in the built WAR, in order to prevent duplicates and conflicts in implementing
projects.
</action>
</release>
<release version="3.7.0" date="2019-02-06" description="Gale">
<action type="add">