Merge branch 'master' into ja_interceptor_jpa_queries
This commit is contained in:
commit
3eeca4a8fd
|
@ -232,7 +232,9 @@ public class DateParam extends BaseParamWithPrefix<DateParam> implements /*IQuer
|
|||
/**
|
||||
* Constructor
|
||||
*/
|
||||
DateParamDateTimeHolder() {
|
||||
// LEAVE THIS AS PUBLIC!!
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public DateParamDateTimeHolder() {
|
||||
super();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -212,6 +212,7 @@
|
|||
<configuration>
|
||||
<attachClasses>true</attachClasses>
|
||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||
<packagingExcludes>WEB-INF/lib/*.jar</packagingExcludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
20
pom.xml
20
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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 -> 5.1.5.RELEASE</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>Hibernate (JPA): 5.4.1 -> 5.4.2</li>
|
||||
<li>Jackson (JPA): 2.9.7 -> 2.9.8</li>
|
||||
<li>Spring (JPA): 5.1.3.RELEASE -> 5.1.6.RELEASE</li>
|
||||
<li>Spring-Data (JPA): 2.1.3.RELEASE -> 2.1.5.RELEASE</li>
|
||||
<li>Caffeine (JPA): 2.6.2 -> 2.7.0</li>
|
||||
<li>JANSI (CLI): 1.16 -> 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">
|
||||
|
|
Loading…
Reference in New Issue