Resource change listener (#2191)
* init rev * fix build * Tweaked the POM config settings and also added some dependency exclusions (commented out for now). * More re-factoring of the CQL Unit Tests. * Removed a LogMessages.html file and a minor Unit Test change. * Unit Tests. * added debug logging to troubleshoot the dao that has no name * added debug logging to troubleshoot the dao that has no name * workaround to get past null dao resourceName issue * fix jsons to get test to pass. Test still fails with library id problem * gitignore * gitignore * test passes! Woohoo! * undo troubleshooting logging * added timer and logging. * added asserts and time multiple evaluations and measure the average * readme * adding explanations * added more explanatory notes * measure 2 patients * move pom to use cqf snapshot * roughed out cache * roughed out cache * Added code to VersionChangeCache class. * added tests * added polling test * wrote init version * wrote init version * optimized versioncache * worked on getting tests to pass * redesigned interfaces * all tests pass * fixmes * fixmes * rename param * Added Unit Tests. * javadoc * Fixed the 2-Patient Unit Test. * More Unit Test work. * make ResourceVersionMap immutable * Fixed a Unit Test that was failing intermittently by adding a new way to refresh the cache. * Use a new method called doRefreshAllCaches(0) to force a refresh and have all Listeners called immediately. * Cleaned up IVersionChangeListenerRegistry interface to make methods more clear and resolved all Unit Tests. * disabled tests * disabled tests * removed unused test method * fixed refresh logic and added asserts * moved cache so it can be used by searchparamregistry * Updated the Cql Unit Tests to be properly configured for Dstu3 or R4. * started rewriting SearchParamRegistryImpl to use new cache added init method to listener interface * added fixmes * adding tests * tests pass * added tests * Fixed the way CqlProviderFactory Autowires Beans so it can work with both Dstu3 and R4 contexts. * moar tests * fix test * work tests * reverting unneccessary refactors * undo unneccessary import changes to reduce MR size * undo unneccessary import changes to reduce MR size * Unit Test fixes...more to come... * add unregister * fix tests * Changed ResourceVersionCache to use a Map of Maps. * searchparam test * test passes * resolved fixme * fixmies * strengthen test asserts * More Unit Test changes and added some FIMXME items. * changed from long to changeresult * renamed VersionChange -> ResourceChange * fixed delete bug * organize imports * fix test * add update test * add test reset function * fix stack overflow * fix startup race condition (might still be intermittent) * found the problem. delete doesn't work because we can't look up the deleted resource to find out what its name is * fixed regression * abandoned idea of incrementally updating searchparam registry. Rebuilding every time--it doesn't change that often. * fix test * begin with failing test * test passes * fixmes and javadoc * fix test * fixme * fix test * whack-a-mole. Either subs pass or cql passes. Something's fishy with the FhirContext * fix subscription test initialization * fix method name * Re-factored the CqlProvider Unit Tests. * changed ResourceChange API * add interface * add interface * fix test * add schedule test * add doc * init rev * FIXME * modify FhirContext change * change fhirContext.getResourceTypes to lazy load * converted subscriptions * converted subscriptions * begin with failing test * test passes * fix test * test coverage * test coverage * test coverage * test coverage * good coverage now * pre-review cleanup. I think I found a bug. * moved cache into listener entry tests pass with fixmes * fix test * fix test * fix test * fixme * FIXMEs * merge cache and registry * method reorg * javadoc * javadoc done. all FIXMEs resolved. * change log * changes needed by cdr * spring config cleanup * james feedback * james feedback * might not work. Try moving resourcechangeconfig into searchparam config * merge ResourceChangeListenerRegistryConfig.java into SearchParamConfig * fix test * fix SubscriptionLoader * fix SubscriptionLoader * create ResourceVersionMap from resources * added cache handle interface * fix test * javadoc * fix test * fix test * James feedback: clone searchparametermap * fix startup * fix test * fix test * fix intermittent * pre-review cleanup * FIXME * final FIXME yay! * Address a couple of my own reviw comments Co-authored-by: Kevin Dougan <kevin.dougan@smilecdr.com> Co-authored-by: jamesagnew <jamesagnew@gmail.com>
This commit is contained in:
parent
1e5def260c
commit
3d3242cf9a
|
@ -33,8 +33,10 @@ import org.hl7.fhir.instance.model.api.IBase;
|
|||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
|
@ -118,6 +120,7 @@ public class FhirContext {
|
|||
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
|
||||
private IValidationSupport myValidationSupport;
|
||||
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
|
||||
private volatile Set<String> myResourceNames;
|
||||
|
||||
/**
|
||||
* @deprecated It is recommended that you use one of the static initializer methods instead
|
||||
|
@ -553,29 +556,31 @@ public class FhirContext {
|
|||
* @since 5.1.0
|
||||
*/
|
||||
public Set<String> getResourceTypes() {
|
||||
Set<String> resourceNames = new HashSet<>();
|
||||
Set<String> resourceNames = myResourceNames;
|
||||
if (resourceNames == null) {
|
||||
resourceNames = buildResourceNames();
|
||||
myResourceNames = resourceNames;
|
||||
}
|
||||
return resourceNames;
|
||||
}
|
||||
|
||||
if (myNameToResourceDefinition.isEmpty()) {
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
props.load(myVersion.getFhirVersionPropertiesFile());
|
||||
} catch (IOException theE) {
|
||||
throw new ConfigurationException("Failed to load version properties file");
|
||||
}
|
||||
Enumeration<?> propNames = props.propertyNames();
|
||||
while (propNames.hasMoreElements()) {
|
||||
String next = (String) propNames.nextElement();
|
||||
if (next.startsWith("resource.")) {
|
||||
resourceNames.add(next.substring("resource.".length()).trim());
|
||||
}
|
||||
@Nonnull
|
||||
private Set<String> buildResourceNames() {
|
||||
Set<String> retVal = new HashSet<>();
|
||||
Properties props = new Properties();
|
||||
try (InputStream propFile = myVersion.getFhirVersionPropertiesFile()) {
|
||||
props.load(propFile);
|
||||
} catch (IOException e) {
|
||||
throw new ConfigurationException("Failed to load version properties file", e);
|
||||
}
|
||||
Enumeration<?> propNames = props.propertyNames();
|
||||
while (propNames.hasMoreElements()) {
|
||||
String next = (String) propNames.nextElement();
|
||||
if (next.startsWith("resource.")) {
|
||||
retVal.add(next.substring("resource.".length()).trim());
|
||||
}
|
||||
}
|
||||
|
||||
for (RuntimeResourceDefinition next : myNameToResourceDefinition.values()) {
|
||||
resourceNames.add(next.getName());
|
||||
}
|
||||
|
||||
return Collections.unmodifiableSet(resourceNames);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,7 +17,9 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
|||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.*;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
|
@ -154,10 +156,15 @@ public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdTy
|
|||
myResourceType = theResourceType;
|
||||
myUnqualifiedId = theId;
|
||||
myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionId, null);
|
||||
myHaveComponentParts = true;
|
||||
if (isBlank(myBaseUrl) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) {
|
||||
myHaveComponentParts = false;
|
||||
}
|
||||
setHaveComponentParts(this);
|
||||
}
|
||||
|
||||
public IdDt(IIdType theId) {
|
||||
myBaseUrl = theId.getBaseUrl();
|
||||
myResourceType = theId.getResourceType();
|
||||
myUnqualifiedId = theId.getIdPart();
|
||||
myUnqualifiedVersionId = theId.getVersionIdPart();
|
||||
setHaveComponentParts(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,6 +174,21 @@ public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdTy
|
|||
setValue(theUrl.getValueAsString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Constructor
|
||||
*/
|
||||
public IdDt(IdDt theIdDt) {
|
||||
this(theIdDt.myBaseUrl, theIdDt.myResourceType, theIdDt.myUnqualifiedId, theIdDt.myUnqualifiedVersionId);
|
||||
}
|
||||
|
||||
private void setHaveComponentParts(IdDt theIdDt) {
|
||||
if (isBlank(myBaseUrl) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) {
|
||||
myHaveComponentParts = false;
|
||||
} else {
|
||||
myHaveComponentParts = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTo(IBaseResource theResouce) {
|
||||
if (theResouce == null) {
|
||||
|
@ -642,7 +664,9 @@ public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdTy
|
|||
value = existingValue;
|
||||
}
|
||||
|
||||
return new IdDt(value + '/' + Constants.PARAM_HISTORY + '/' + theVersion);
|
||||
IdDt retval = new IdDt(this);
|
||||
retval.myUnqualifiedVersionId = theVersion;
|
||||
return retval;
|
||||
}
|
||||
|
||||
public static boolean isValidLong(String id) {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
type: add
|
||||
issue: 2191
|
||||
title: "Added a new IResourceChangeListenerRegistry service and modified SearchParamRegistry and SubscriptionRegistry to use it.
|
||||
|
||||
This service contains an in-memory list of all registered {@link IResourceChangeListener} instances along
|
||||
with their caches and other details needed to maintain those caches. Register an {@link IResourceChangeListener} instance
|
||||
with this service to be notified when resources you care about are changed. This service quickly notifies listeners
|
||||
of changes that happened on the local process and also eventually notifies listeners of changes that were made by
|
||||
remote processes."
|
40
hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/cache/ResourceVersionSvcDaoImpl.java
vendored
Normal file
40
hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/cache/ResourceVersionSvcDaoImpl.java
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* This service builds a map of resource ids to versions based on a SearchParameterMap.
|
||||
* It is used by the in-memory resource-version cache to detect when resource versions have been changed by remote processes.
|
||||
*/
|
||||
@Service
|
||||
public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
|
||||
private static final Logger myLogger = LoggerFactory.getLogger(ResourceVersionMap.class);
|
||||
|
||||
@Autowired
|
||||
DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
IResourceTableDao myResourceTableDao;
|
||||
|
||||
@Nonnull
|
||||
public ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap) {
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
|
||||
|
||||
List<Long> matchingIds = dao.searchForIds(theSearchParamMap, null).stream()
|
||||
.map(ResourcePersistentId::getIdAsLong)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResourceVersionMap.fromResourceTableEntities(myResourceTableDao.findAllById(matchingIds));
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
|
|||
import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc;
|
||||
import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider;
|
||||
import ca.uhn.fhir.jpa.bulk.svc.BulkDataExportSvcImpl;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceVersionSvcDaoImpl;
|
||||
import ca.uhn.fhir.jpa.dao.HistoryBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory;
|
||||
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
|
||||
|
@ -88,9 +90,8 @@ import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
|
|||
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.utilities.npm.BasePackageCacheManager;
|
||||
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
|
||||
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
|
||||
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
|
||||
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
|
||||
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -148,6 +149,7 @@ import java.util.Date;
|
|||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.subscription.*"),
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.searchparam.*"),
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.empi.*"),
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.cache.*"),
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.starter.*"),
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.batch.*")
|
||||
})
|
||||
|
@ -457,6 +459,11 @@ public abstract class BaseConfig {
|
|||
return new HistoryBuilderFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IResourceVersionSvc resourceVersionSvc() {
|
||||
return new ResourceVersionSvcDaoImpl();
|
||||
}
|
||||
|
||||
/* **************************************************************** *
|
||||
* Prototype Beans Below *
|
||||
* **************************************************************** */
|
||||
|
|
|
@ -49,6 +49,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|||
@EnableTransactionManagement
|
||||
public class BaseDstu3Config extends BaseConfigDstu3Plus {
|
||||
|
||||
public static FhirContext ourFhirContext = FhirContext.forDstu3();
|
||||
|
||||
@Override
|
||||
public FhirContext fhirContext() {
|
||||
return fhirContextDstu3();
|
||||
|
@ -63,7 +65,7 @@ public class BaseDstu3Config extends BaseConfigDstu3Plus {
|
|||
@Bean
|
||||
@Primary
|
||||
public FhirContext fhirContextDstu3() {
|
||||
FhirContext retVal = FhirContext.forDstu3();
|
||||
FhirContext retVal = ourFhirContext;
|
||||
|
||||
// Don't strip versions in some places
|
||||
ParserOptions parserOptions = retVal.getParserOptions();
|
||||
|
|
|
@ -21,11 +21,13 @@ package ca.uhn.fhir.jpa.dao;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -39,15 +41,15 @@ public class DaoSearchParamProvider implements ISearchParamProvider {
|
|||
|
||||
@Override
|
||||
public IBundleProvider search(SearchParameterMap theParams) {
|
||||
return myDaoRegistry.getResourceDao(ResourceTypeEnum.SEARCHPARAMETER.getCode()).search(theParams);
|
||||
return getSearchParamDao().search(theParams);
|
||||
}
|
||||
|
||||
private IFhirResourceDao getSearchParamDao() {
|
||||
return myDaoRegistry.getResourceDao(ResourceTypeEnum.SEARCHPARAMETER.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval) {
|
||||
int retVal = 0;
|
||||
if (myDaoRegistry.getResourceDaoOrNull("SearchParameter") != null) {
|
||||
retVal = theSearchParamRegistry.doRefresh(theRefreshInterval);
|
||||
}
|
||||
return retVal;
|
||||
public IBaseResource read(IIdType theSearchParamId) {
|
||||
return getSearchParamDao().read(theSearchParamId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,8 +69,8 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
@Autowired
|
||||
protected PlatformTransactionManager myTransactionMgr;
|
||||
private boolean myProcessDeferred = true;
|
||||
final private List<TermCodeSystem> myDefferedCodeSystemsDeletions = Collections.synchronizedList(new ArrayList<>());
|
||||
final private List<TermCodeSystemVersion> myDefferedCodeSystemVersionsDeletions = Collections.synchronizedList(new ArrayList<>());
|
||||
final private List<TermCodeSystem> myDeferredCodeSystemsDeletions = Collections.synchronizedList(new ArrayList<>());
|
||||
final private List<TermCodeSystemVersion> myDeferredCodeSystemVersionsDeletions = Collections.synchronizedList(new ArrayList<>());
|
||||
final private List<TermConcept> myDeferredConcepts = Collections.synchronizedList(new ArrayList<>());
|
||||
final private List<ValueSet> myDeferredValueSets = Collections.synchronizedList(new ArrayList<>());
|
||||
final private List<ConceptMap> myDeferredConceptMaps = Collections.synchronizedList(new ArrayList<>());
|
||||
|
@ -113,7 +113,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
public void deleteCodeSystem(TermCodeSystem theCodeSystem) {
|
||||
theCodeSystem.setCodeSystemUri("urn:uuid:" + UUID.randomUUID().toString());
|
||||
myCodeSystemDao.save(theCodeSystem);
|
||||
myDefferedCodeSystemsDeletions.add(theCodeSystem);
|
||||
myDeferredCodeSystemsDeletions.add(theCodeSystem);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -122,7 +122,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
List<TermCodeSystemVersion> codeSystemVersionsToDelete = myCodeSystemVersionDao.findByCodeSystemResourcePid(theCodeSystemToDelete.getResourceId());
|
||||
for (TermCodeSystemVersion codeSystemVersionToDelete : codeSystemVersionsToDelete){
|
||||
if (codeSystemVersionToDelete != null) {
|
||||
myDefferedCodeSystemVersionsDeletions.add(codeSystemVersionToDelete);
|
||||
myDeferredCodeSystemVersionsDeletions.add(codeSystemVersionToDelete);
|
||||
}
|
||||
}
|
||||
TermCodeSystem codeSystemToDelete = myCodeSystemDao.findByResourcePid(theCodeSystemToDelete.getResourceId());
|
||||
|
@ -223,11 +223,13 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
*/
|
||||
@VisibleForTesting
|
||||
public synchronized void clearDeferred() {
|
||||
myProcessDeferred = true;
|
||||
myDeferredValueSets.clear();
|
||||
myDeferredConceptMaps.clear();
|
||||
myDeferredConcepts.clear();
|
||||
myDefferedCodeSystemsDeletions.clear();
|
||||
myDeferredCodeSystemsDeletions.clear();
|
||||
myConceptLinksToSaveLater.clear();
|
||||
myDeferredCodeSystemVersionsDeletions.clear();
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.NEVER)
|
||||
|
@ -284,15 +286,15 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
|
||||
private void processDeferredCodeSystemDeletions() {
|
||||
|
||||
for (TermCodeSystemVersion next : myDefferedCodeSystemVersionsDeletions) {
|
||||
for (TermCodeSystemVersion next : myDeferredCodeSystemVersionsDeletions) {
|
||||
myCodeSystemStorageSvc.deleteCodeSystemVersion(next);
|
||||
}
|
||||
|
||||
myDefferedCodeSystemVersionsDeletions.clear();
|
||||
for (TermCodeSystem next : myDefferedCodeSystemsDeletions) {
|
||||
myDeferredCodeSystemVersionsDeletions.clear();
|
||||
for (TermCodeSystem next : myDeferredCodeSystemsDeletions) {
|
||||
myCodeSystemStorageSvc.deleteCodeSystem(next);
|
||||
}
|
||||
myDefferedCodeSystemsDeletions.clear();
|
||||
myDeferredCodeSystemsDeletions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -322,7 +324,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
|
|||
}
|
||||
|
||||
private boolean isDeferredCodeSystemDeletions() {
|
||||
return !myDefferedCodeSystemsDeletions.isEmpty() || !myDefferedCodeSystemVersionsDeletions.isEmpty();
|
||||
return !myDeferredCodeSystemsDeletions.isEmpty() || !myDeferredCodeSystemVersionsDeletions.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isDeferredConcepts() {
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.test.concurrency.IPointcutLatch;
|
||||
import ca.uhn.test.concurrency.PointcutLatch;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
|
||||
private static final long TEST_REFRESH_INTERVAL = DateUtils.MILLIS_PER_DAY;
|
||||
@Autowired
|
||||
ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
|
||||
@Autowired
|
||||
ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
|
||||
|
||||
private final static String RESOURCE_NAME = "Patient";
|
||||
private TestCallback myMaleTestCallback = new TestCallback("MALE");
|
||||
private TestCallback myFemaleTestCallback = new TestCallback("FEMALE");
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
myMaleTestCallback.clear();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
myResourceChangeListenerRegistry.clearListenersForUnitTest();
|
||||
myResourceChangeListenerRegistry.clearCachesForUnitTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterListener() throws InterruptedException {
|
||||
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, SearchParameterMap.newSynchronous(), myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
|
||||
Patient patient = createPatientWithInitLatch(null, myMaleTestCallback);
|
||||
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
IdDt patientId = new IdDt(patient.getIdElement().toUnqualifiedVersionless());
|
||||
|
||||
patient.setActive(false);
|
||||
patient.setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
myPatientDao.update(patient);
|
||||
|
||||
myMaleTestCallback.setExpectedCount(1);
|
||||
ResourceChangeResult result = cache.forceRefresh();
|
||||
myMaleTestCallback.awaitExpected();
|
||||
|
||||
assertResult(result, 0, 1, 0);
|
||||
assertEquals(2L, myMaleTestCallback.getUpdateResourceId().getVersionIdPartAsLong());
|
||||
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
// Calling forceRefresh with no changes does not call listener
|
||||
result = cache.forceRefresh();
|
||||
assertResult(result, 0, 0, 0);
|
||||
|
||||
myMaleTestCallback.setExpectedCount(1);
|
||||
myPatientDao.delete(patientId.toVersionless());
|
||||
result = cache.forceRefresh();
|
||||
assertResult(result, 0, 0, 1);
|
||||
myMaleTestCallback.awaitExpected();
|
||||
assertEquals(patientId, myMaleTestCallback.getDeletedResourceId());
|
||||
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonInMemorySearchParamCannotBeRegistered() {
|
||||
try {
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.setLastUpdated(new DateRangeParam("1965", "1970"));
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, map, myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("SearchParameterMap SearchParameterMap[] cannot be evaluated in-memory: Parameter: <_lastUpdated> Reason: Standard parameters not supported. Only search parameter maps that can be evaluated in-memory may be registered.", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertResult(ResourceChangeResult theResult, long theExpectedCreated, long theExpectedUpdated, long theExpectedDeleted) {
|
||||
assertEquals(theExpectedCreated, theResult.created, "created results");
|
||||
assertEquals(theExpectedUpdated, theResult.updated, "updated results");
|
||||
assertEquals(theExpectedDeleted, theResult.deleted, "deleted results");
|
||||
}
|
||||
|
||||
private void assertEmptyResult(ResourceChangeResult theResult) {
|
||||
assertResult(theResult, 0, 0, 0);
|
||||
}
|
||||
|
||||
private Patient createPatientWithInitLatch(Enumerations.AdministrativeGender theGender, TestCallback theTestCallback) throws InterruptedException {
|
||||
Patient patient = new Patient();
|
||||
patient.setActive(true);
|
||||
if (theGender != null) {
|
||||
patient.setGender(theGender);
|
||||
}
|
||||
theTestCallback.setInitExpectedCount(1);
|
||||
IdDt patientId = createPatientAndRefreshCache(patient, theTestCallback, 1);
|
||||
theTestCallback.awaitInitExpected();
|
||||
|
||||
List<IIdType> resourceIds = theTestCallback.getInitResourceIds();
|
||||
assertThat(resourceIds, hasSize(1));
|
||||
IIdType resourceId = resourceIds.get(0);
|
||||
assertEquals(patientId.toString(), resourceId.toString());
|
||||
assertEquals(1L, resourceId.getVersionIdPartAsLong());
|
||||
|
||||
return patient;
|
||||
}
|
||||
|
||||
private IdDt createPatientAndRefreshCache(Patient thePatient, TestCallback theTestCallback, long theExpectedCount) throws InterruptedException {
|
||||
IIdType retval = myPatientDao.create(thePatient).getId();
|
||||
ResourceChangeResult result = myResourceChangeListenerCacheRefresher.forceRefreshAllCachesForUnitTest();
|
||||
assertResult(result, theExpectedCount, 0, 0);
|
||||
return new IdDt(retval);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterPolling() throws InterruptedException {
|
||||
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, SearchParameterMap.newSynchronous(), myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
|
||||
Patient patient = createPatientWithInitLatch(null, myMaleTestCallback);
|
||||
IdDt patientId = new IdDt(patient.getIdElement());
|
||||
|
||||
// Pretend we're on a different process in the cluster and so our cache doesn't have the cache yet
|
||||
myResourceChangeListenerRegistry.clearCachesForUnitTest();
|
||||
myMaleTestCallback.setExpectedCount(1);
|
||||
ResourceChangeResult result = cache.forceRefresh();
|
||||
assertResult(result, 1, 0, 0);
|
||||
List<HookParams> calledWith = myMaleTestCallback.awaitExpected();
|
||||
ResourceChangeEvent resourceChangeEvent = (ResourceChangeEvent) PointcutLatch.getLatchInvocationParameter(calledWith);
|
||||
assertEquals(patientId, resourceChangeEvent.getCreatedResourceIds().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterInterceptorFor2Patients() throws InterruptedException {
|
||||
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.MALE), myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
|
||||
createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
|
||||
|
||||
myMaleTestCallback.clear();
|
||||
|
||||
Patient patientFemale = new Patient();
|
||||
patientFemale.setActive(true);
|
||||
patientFemale.setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// NOTE: This scenario does not invoke the myTestCallback listener so just call the DAO directly
|
||||
IIdType patientIdFemale = new IdDt(myPatientDao.create(patientFemale).getId());
|
||||
ResourceChangeResult result = cache.forceRefresh();
|
||||
assertEmptyResult(result);
|
||||
assertNotNull(patientIdFemale.toString());
|
||||
assertNull(myMaleTestCallback.getResourceChangeEvent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegister2InterceptorsFor2Patients() throws InterruptedException {
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.MALE), myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
|
||||
myMaleTestCallback.clear();
|
||||
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.FEMALE), myFemaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
createPatientWithInitLatch(Enumerations.AdministrativeGender.FEMALE, myFemaleTestCallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterPollingFor2Patients() throws InterruptedException {
|
||||
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.MALE), myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
|
||||
Patient patientMale = createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
|
||||
IdDt patientIdMale = new IdDt(patientMale.getIdElement());
|
||||
|
||||
Patient patientFemale = new Patient();
|
||||
patientFemale.setActive(true);
|
||||
patientFemale.setGender(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
// NOTE: This scenario does not invoke the myTestCallback listener so just call the DAO directly
|
||||
IIdType patientIdFemale = new IdDt(myPatientDao.create(patientFemale).getId());
|
||||
ResourceChangeResult result = cache.forceRefresh();
|
||||
assertEmptyResult(result);
|
||||
assertNotNull(patientIdFemale.toString());
|
||||
assertNull(myMaleTestCallback.getResourceChangeEvent());
|
||||
|
||||
// Pretend we're on a different process in the cluster and so our cache doesn't have the cache yet
|
||||
myResourceChangeListenerRegistry.clearCachesForUnitTest();
|
||||
myMaleTestCallback.setExpectedCount(1);
|
||||
result = cache.forceRefresh();
|
||||
// We should still only get one matching result
|
||||
assertResult(result, 1, 0, 0);
|
||||
List<HookParams> calledWith = myMaleTestCallback.awaitExpected();
|
||||
ResourceChangeEvent resourceChangeEvent = (ResourceChangeEvent) PointcutLatch.getLatchInvocationParameter(calledWith);
|
||||
assertEquals(patientIdMale, resourceChangeEvent.getCreatedResourceIds().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoListenersSameMap() throws InterruptedException {
|
||||
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
SearchParameterMap searchParameterMap = createSearchParameterMap(Enumerations.AdministrativeGender.MALE);
|
||||
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, searchParameterMap, myMaleTestCallback, TEST_REFRESH_INTERVAL);
|
||||
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
|
||||
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
TestCallback otherTestCallback = new TestCallback("OTHER_MALE");
|
||||
IResourceChangeListenerCache otherCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, searchParameterMap, otherTestCallback, TEST_REFRESH_INTERVAL);
|
||||
|
||||
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
otherCache.forceRefresh();
|
||||
assertEquals(2, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(myMaleTestCallback);
|
||||
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(otherTestCallback);
|
||||
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
}
|
||||
|
||||
private SearchParameterMap createSearchParameterMap(Enumerations.AdministrativeGender theGender) {
|
||||
return SearchParameterMap.newSynchronous().add(Patient.SP_GENDER, new TokenParam(null, theGender.toCode()));
|
||||
}
|
||||
|
||||
private static class TestCallback implements IResourceChangeListener, IPointcutLatch {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(TestCallback.class);
|
||||
private final PointcutLatch myHandleLatch;
|
||||
private final PointcutLatch myInitLatch;
|
||||
private final String myName;
|
||||
|
||||
private IResourceChangeEvent myResourceChangeEvent;
|
||||
private Collection<IIdType> myInitResourceIds;
|
||||
|
||||
public TestCallback(String theName) {
|
||||
myName = theName;
|
||||
myHandleLatch = new PointcutLatch(theName + " ResourceChangeListener handle called");
|
||||
myInitLatch = new PointcutLatch(theName + " ResourceChangeListener init called");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
|
||||
ourLog.info("{} TestCallback.handleChange() called with {}", myName, theResourceChangeEvent);
|
||||
myResourceChangeEvent = theResourceChangeEvent;
|
||||
myHandleLatch.call(theResourceChangeEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleInit(Collection<IIdType> theResourceIds) {
|
||||
myInitResourceIds = theResourceIds;
|
||||
myInitLatch.call(theResourceIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
myResourceChangeEvent = null;
|
||||
myInitResourceIds = null;
|
||||
myHandleLatch.clear();
|
||||
myInitLatch.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExpectedCount(int theCount) {
|
||||
myHandleLatch.setExpectedCount(theCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HookParams> awaitExpected() throws InterruptedException {
|
||||
return myHandleLatch.awaitExpected();
|
||||
}
|
||||
|
||||
public List<IIdType> getInitResourceIds() {
|
||||
return new ArrayList<>(myInitResourceIds);
|
||||
}
|
||||
|
||||
public IResourceChangeEvent getResourceChangeEvent() {
|
||||
return myResourceChangeEvent;
|
||||
}
|
||||
|
||||
public void setInitExpectedCount(int theCount) {
|
||||
myInitLatch.setExpectedCount(theCount);
|
||||
}
|
||||
|
||||
public void awaitInitExpected() throws InterruptedException {
|
||||
myInitLatch.awaitExpected();
|
||||
}
|
||||
|
||||
public IIdType getUpdateResourceId() {
|
||||
assertThat(myResourceChangeEvent.getUpdatedResourceIds(), hasSize(1));
|
||||
return myResourceChangeEvent.getUpdatedResourceIds().get(0);
|
||||
}
|
||||
|
||||
public IIdType getDeletedResourceId() {
|
||||
assertThat(myResourceChangeEvent.getDeletedResourceIds(), hasSize(1));
|
||||
return myResourceChangeEvent.getDeletedResourceIds().get(0);
|
||||
}
|
||||
}
|
||||
}
|
32
hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/cache/ResourceVersionCacheSvcTest.java
vendored
Normal file
32
hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/cache/ResourceVersionCacheSvcTest.java
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ResourceVersionCacheSvcTest extends BaseJpaR4Test {
|
||||
@Autowired
|
||||
IResourceVersionSvc myResourceVersionCacheSvc;
|
||||
|
||||
@Test
|
||||
public void testGetVersionMap() {
|
||||
Patient patient = new Patient();
|
||||
patient.setActive(true);
|
||||
IIdType patientId = myPatientDao.create(patient).getId();
|
||||
ResourceVersionMap versionMap = myResourceVersionCacheSvc.getVersionMap("Patient", SearchParameterMap.newSynchronous());
|
||||
assertEquals(1, versionMap.size());
|
||||
assertEquals("1", versionMap.getVersion(patientId));
|
||||
|
||||
patient.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
myPatientDao.update(patient);
|
||||
versionMap = myResourceVersionCacheSvc.getVersionMap("Patient", SearchParameterMap.newSynchronous());
|
||||
assertEquals(1, versionMap.size());
|
||||
assertEquals("2", versionMap.getVersion(patientId));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,20 @@
|
|||
package ca.uhn.fhir.jpa.config;
|
||||
|
||||
import java.sql.*;
|
||||
import java.sql.Array;
|
||||
import java.sql.Blob;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.Clob;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.NClob;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLClientInfoException;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLWarning;
|
||||
import java.sql.SQLXML;
|
||||
import java.sql.Savepoint;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Struct;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.Executor;
|
||||
|
@ -252,7 +266,7 @@ public class ConnectionWrapper implements Connection {
|
|||
|
||||
@Override
|
||||
public void setReadOnly(boolean theReadOnly) throws SQLException {
|
||||
ourLog.info("Setting connection as readonly");
|
||||
ourLog.debug("Setting connection as readonly");
|
||||
myWrap.setReadOnly(theReadOnly);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||
|
@ -11,11 +10,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
|||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.model.dstu2.valueset.XPathUsageTypeEnum;
|
||||
import ca.uhn.fhir.model.primitive.IntegerDt;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.NumberParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
|
||||
|
@ -24,7 +19,6 @@ import ca.uhn.fhir.rest.param.StringParam;
|
|||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Appointment;
|
||||
|
@ -56,11 +50,10 @@ import org.hl7.fhir.r4.model.SearchParameter;
|
|||
import org.hl7.fhir.r4.model.ServiceRequest;
|
||||
import org.hl7.fhir.r4.model.Specimen;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.internal.util.collections.ListUtil;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
|
@ -1433,6 +1426,25 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
|
|||
foundResources = toUnqualifiedVersionlessIdValues(results);
|
||||
assertThat(foundResources, contains(patId.getValue()));
|
||||
|
||||
// Retire the param
|
||||
fooSp.setId(spId);
|
||||
fooSp.setStatus(Enumerations.PublicationStatus.RETIRED);
|
||||
|
||||
mySearchParameterDao.update(fooSp, mySrd);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
myResourceReindexingSvc.forceReindexingPass();
|
||||
|
||||
// Expect error since searchparam is now retired
|
||||
map = new SearchParameterMap();
|
||||
map.add("foo", new TokenParam(null, "male"));
|
||||
try {
|
||||
myPatientDao.search(map).size();
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, _lastUpdated, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
|
||||
}
|
||||
|
||||
// Delete the param
|
||||
mySearchParameterDao.delete(spId, mySrd);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
|||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
|
@ -18,9 +19,9 @@ import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
|||
import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||
|
@ -33,7 +34,6 @@ import org.hl7.fhir.r4.model.Patient;
|
|||
import org.hl7.fhir.r4.model.Quantity;
|
||||
import org.hl7.fhir.r4.model.Reference;
|
||||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -350,13 +350,13 @@ public class SearchParamExtractorR4Test {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshCacheIfNecessary() {
|
||||
public ResourceChangeResult refreshCacheIfNecessary() {
|
||||
// nothing
|
||||
return false;
|
||||
return new ResourceChangeResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
|
||||
public ReadOnlySearchParamCache getActiveSearchParams() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -1186,7 +1186,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME KHS
|
||||
@Test
|
||||
public void testDeleteExpungeAllowed() {
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.util.AopTestUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -44,9 +45,10 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
|
|||
@AfterEach
|
||||
public void after() {
|
||||
myDaoConfig.setDeferIndexingForCodesystemsOfSize(new DaoConfig().getDeferIndexingForCodesystemsOfSize());
|
||||
TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTermDeferredStorageSvc);
|
||||
termDeferredStorageSvc.clearDeferred();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAddRootConcepts() {
|
||||
createNotPresentCodeSystem();
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<logger name="ca.uhn.fhir.jpa.dao" additivity="false" level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</logger>
|
||||
|
||||
|
||||
<!-- Set to 'trace' to enable SQL logging -->
|
||||
<logger name="org.hibernate.SQL" additivity="false" level="info">
|
||||
<appender-ref ref="STDOUT" />
|
||||
|
|
20
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeEvent.java
vendored
Normal file
20
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeEvent.java
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Registered IResourceChangeListener instances are called with this event to provide them with a list of ids of resources
|
||||
* that match the search parameters and that changed from the last time they were checked.
|
||||
*/
|
||||
public interface IResourceChangeEvent {
|
||||
List<IIdType> getCreatedResourceIds();
|
||||
List<IIdType> getUpdatedResourceIds();
|
||||
List<IIdType> getDeletedResourceIds();
|
||||
|
||||
/**
|
||||
* @return true when all three lists are empty
|
||||
*/
|
||||
boolean isEmpty();
|
||||
}
|
22
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeListener.java
vendored
Normal file
22
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceChangeListener.java
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* To be notified of resource changes in the repository, implement this interface and register your instance with
|
||||
* {@link IResourceChangeListenerRegistry}.
|
||||
*/
|
||||
public interface IResourceChangeListener {
|
||||
/**
|
||||
* This method is called within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of a listener registration
|
||||
* @param theResourceIds the ids of all resources that match the search parameters the listener was registered with
|
||||
*/
|
||||
void handleInit(Collection<IIdType> theResourceIds);
|
||||
|
||||
/**
|
||||
* Called by the {@link IResourceChangeListenerRegistry} when matching resource changes are detected
|
||||
*/
|
||||
void handleChange(IResourceChangeEvent theResourceChangeEvent);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* This is a handle to the cache created by {@link IResourceChangeListenerRegistry} when a listener is registered.
|
||||
* This this handle can be used to refresh the cache if required.
|
||||
*/
|
||||
public interface IResourceChangeListenerCache {
|
||||
/**
|
||||
* @return the search parameter map the listener was registered with
|
||||
*/
|
||||
SearchParameterMap getSearchParameterMap();
|
||||
|
||||
/**
|
||||
* @return whether the cache has been initialized. (If not, the cache will be empty.)
|
||||
*/
|
||||
boolean isInitialized();
|
||||
|
||||
/**
|
||||
* @return the name of the resource type the listener was registered with
|
||||
*/
|
||||
String getResourceName();
|
||||
|
||||
/**
|
||||
* @return the next scheduled time the cache will search the repository, update its cache and notify
|
||||
* its listener of any changes
|
||||
*/
|
||||
Instant getNextRefreshTime();
|
||||
|
||||
/**
|
||||
* sets the nextRefreshTime to {@link Instant.MIN} so that the cache will be refreshed and listeners notified in another thread
|
||||
* the next time cache refresh times are checked (every {@link ResourceChangeListenerCacheRefresherImpl.LOCAL_REFRESH_INTERVAL_MS}.
|
||||
*/
|
||||
void requestRefresh();
|
||||
|
||||
/**
|
||||
* Refresh the cache immediately in the current thread and notify its listener if there are any changes
|
||||
* @return counts of detected resource creates, updates and deletes
|
||||
*/
|
||||
ResourceChangeResult forceRefresh();
|
||||
|
||||
/**
|
||||
* If nextRefreshTime is in the past, then update the cache with the current repository contents and notify its listener of any changes
|
||||
* @return counts of detected resource creates, updates and deletes
|
||||
*/
|
||||
ResourceChangeResult refreshCacheIfNecessary();
|
||||
|
||||
// TODO KHS in the future support adding new listeners to existing caches
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
/**
|
||||
* This is an internal service and is not intended to be used outside this package. Implementers should only directly
|
||||
* call the {@link IResourceChangeListenerRegistry}.
|
||||
*
|
||||
* This service refreshes a {@link ResourceChangeListenerCache} cache and notifies its listener when
|
||||
* the cache changes.
|
||||
*/
|
||||
public interface IResourceChangeListenerCacheRefresher {
|
||||
/**
|
||||
* If the current time is past the next refresh time of the registered listener, then check if any of its
|
||||
* resources have changed and notify the listener accordingly
|
||||
* @return an aggregate of all changes sent to all listeners
|
||||
*/
|
||||
ResourceChangeResult refreshExpiredCachesAndNotifyListeners();
|
||||
|
||||
/**
|
||||
* Refresh the cache in this entry and notify the entry's listener if the cache changed
|
||||
* @param theEntry the {@link IResourceChangeListenerCache} with the cache and the listener
|
||||
* @return the number of resources that have been created, updated and deleted since the last time the cache was refreshed
|
||||
*/
|
||||
ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListenerCache theEntry);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
/**
|
||||
* This component holds an in-memory list of all registered {@link IResourceChangeListener} instances along
|
||||
* with their caches and other details needed to maintain those caches. Register an {@link IResourceChangeListener} instance
|
||||
* with this service to be notified when resources you care about are changed. This service quickly notifies listeners
|
||||
* of changes that happened on the local process and also eventually notifies listeners of changes that were made by
|
||||
* remote processes.
|
||||
*/
|
||||
public interface IResourceChangeListenerRegistry {
|
||||
|
||||
/**
|
||||
* Register a listener in order to be notified whenever a resource matching the provided SearchParameterMap
|
||||
* changes in any way. If the change happened on the same jvm process where this registry resides, then the listener will be called
|
||||
* within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of the change happening. If the change happened
|
||||
* on a different jvm process, then the listener will be called within the time specified in theRemoteRefreshIntervalMs parameter.
|
||||
* @param theResourceName the type of the resource the listener should be notified about (e.g. "Subscription" or "SearchParameter")
|
||||
* @param theSearchParameterMap the listener will only be notified of changes to resources that match this map
|
||||
* @param theResourceChangeListener the listener that will be called whenever resource changes are detected
|
||||
* @param theRemoteRefreshIntervalMs the number of milliseconds between checking the database for changed resources that match the search parameter map
|
||||
* @throws ca.uhn.fhir.parser.DataFormatException if theResourceName is not a valid resource type in the FhirContext
|
||||
* @throws IllegalArgumentException if theSearchParamMap cannot be evaluated in-memory
|
||||
* @return RegisteredResourceChangeListener a handle to the created cache that can be used to manually refresh the cache if required
|
||||
*/
|
||||
IResourceChangeListenerCache registerResourceResourceChangeListener(String theResourceName, SearchParameterMap theSearchParameterMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs);
|
||||
|
||||
/**
|
||||
* Unregister a listener from this service
|
||||
*
|
||||
* @param theResourceChangeListener
|
||||
*/
|
||||
void unregisterResourceResourceChangeListener(IResourceChangeListener theResourceChangeListener);
|
||||
|
||||
/**
|
||||
* Unregister a listener from this service using its cache handle
|
||||
*
|
||||
* @param theResourceChangeListenerCache
|
||||
*/
|
||||
void unregisterResourceResourceChangeListener(IResourceChangeListenerCache theResourceChangeListenerCache);
|
||||
|
||||
@VisibleForTesting
|
||||
void clearListenersForUnitTest();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param theCache
|
||||
* @return true if theCache is registered
|
||||
*/
|
||||
boolean contains(IResourceChangeListenerCache theCache);
|
||||
|
||||
/**
|
||||
* Called by the {@link ResourceChangeListenerRegistryInterceptor} when a resource is changed to invalidate matching
|
||||
* caches so their listeners are notified the next time the caches are refreshed.
|
||||
* @param theResource the resource that changed that might trigger a refresh
|
||||
*/
|
||||
|
||||
void requestRefreshIfWatching(IBaseResource theResource);
|
||||
|
||||
}
|
14
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceVersionSvc.java
vendored
Normal file
14
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/IResourceVersionSvc.java
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* This interface is used by the {@link IResourceChangeListenerCacheRefresher} to read resources matching the provided
|
||||
* search parameter map in the repository and compare them to caches stored in the {@link IResourceChangeListenerRegistry}.
|
||||
*/
|
||||
public interface IResourceVersionSvc {
|
||||
@Nonnull
|
||||
ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap);
|
||||
}
|
67
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeEvent.java
vendored
Normal file
67
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeEvent.java
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An immutable list of resource ids that have been changed, updated, or deleted.
|
||||
*/
|
||||
public class ResourceChangeEvent implements IResourceChangeEvent {
|
||||
private final List<IIdType> myCreatedResourceIds;
|
||||
private final List<IIdType> myUpdatedResourceIds;
|
||||
private final List<IIdType> myDeletedResourceIds;
|
||||
|
||||
private ResourceChangeEvent(Collection<IIdType> theCreatedResourceIds, Collection<IIdType> theUpdatedResourceIds, Collection<IIdType> theDeletedResourceIds) {
|
||||
myCreatedResourceIds = copyFrom(theCreatedResourceIds);
|
||||
myUpdatedResourceIds = copyFrom(theUpdatedResourceIds);
|
||||
myDeletedResourceIds = copyFrom(theDeletedResourceIds);
|
||||
}
|
||||
|
||||
public static ResourceChangeEvent fromCreatedResourceIds(Collection<IIdType> theCreatedResourceIds) {
|
||||
return new ResourceChangeEvent(theCreatedResourceIds, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public static ResourceChangeEvent fromCreatedUpdatedDeletedResourceIds(List<IIdType> theCreatedResourceIds, List<IIdType> theUpdatedResourceIds, List<IIdType> theDeletedResourceIds) {
|
||||
return new ResourceChangeEvent(theCreatedResourceIds, theUpdatedResourceIds, theDeletedResourceIds);
|
||||
}
|
||||
|
||||
private List<IIdType> copyFrom(Collection<IIdType> theResourceIds) {
|
||||
ArrayList<IdDt> retval = new ArrayList<>();
|
||||
theResourceIds.forEach(id -> retval.add(new IdDt(id)));
|
||||
return Collections.unmodifiableList(retval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IIdType> getCreatedResourceIds() {
|
||||
return myCreatedResourceIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IIdType> getUpdatedResourceIds() {
|
||||
return myUpdatedResourceIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IIdType> getDeletedResourceIds() {
|
||||
return myDeletedResourceIds;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return myCreatedResourceIds.isEmpty() && myUpdatedResourceIds.isEmpty() && myDeletedResourceIds.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("myCreatedResourceIds", myCreatedResourceIds)
|
||||
.append("myUpdatedResourceIds", myUpdatedResourceIds)
|
||||
.append("myDeletedResourceIds", myDeletedResourceIds)
|
||||
.toString();
|
||||
}
|
||||
}
|
192
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerCache.java
vendored
Normal file
192
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeListenerCache.java
vendored
Normal file
|
@ -0,0 +1,192 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.SerializationUtils;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
@Component
|
||||
@Scope("prototype")
|
||||
public class ResourceChangeListenerCache implements IResourceChangeListenerCache {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCache.class);
|
||||
private static final int MAX_RETRIES = 60;
|
||||
|
||||
private static Instant ourNowForUnitTests;
|
||||
|
||||
@Autowired
|
||||
IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;
|
||||
@Autowired
|
||||
SearchParamMatcher mySearchParamMatcher;
|
||||
|
||||
private final String myResourceName;
|
||||
private final IResourceChangeListener myResourceChangeListener;
|
||||
private final SearchParameterMap mySearchParameterMap;
|
||||
private final ResourceVersionCache myResourceVersionCache = new ResourceVersionCache();
|
||||
private final long myRemoteRefreshIntervalMs;
|
||||
|
||||
private boolean myInitialized = false;
|
||||
private Instant myNextRefreshTime = Instant.MIN;
|
||||
|
||||
public ResourceChangeListenerCache(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
|
||||
myResourceName = theResourceName;
|
||||
myResourceChangeListener = theResourceChangeListener;
|
||||
mySearchParameterMap = SerializationUtils.clone(theSearchParameterMap);
|
||||
myRemoteRefreshIntervalMs = theRemoteRefreshIntervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that the cache be refreshed at the next convenient time (in a different thread)
|
||||
*/
|
||||
@Override
|
||||
public void requestRefresh() {
|
||||
myNextRefreshTime = Instant.MIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that a cache be refreshed now, in the current thread
|
||||
*/
|
||||
@Override
|
||||
public ResourceChangeResult forceRefresh() {
|
||||
requestRefresh();
|
||||
return refreshCacheWithRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the cache if theResource matches our SearchParameterMap
|
||||
* @param theResource
|
||||
*/
|
||||
public void requestRefreshIfWatching(IBaseResource theResource) {
|
||||
if (matches(theResource)) {
|
||||
requestRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean matches(IBaseResource theResource) {
|
||||
InMemoryMatchResult result = mySearchParamMatcher.match(mySearchParameterMap, theResource);
|
||||
if (!result.supported()) {
|
||||
// This should never happen since we enforce only in-memory SearchParamMaps at registration time
|
||||
throw new IllegalStateException("Search Parameter Map " + mySearchParameterMap + " cannot be processed in-memory: " + result.getUnsupportedReason());
|
||||
}
|
||||
return result.matched();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceChangeResult refreshCacheIfNecessary() {
|
||||
ResourceChangeResult retval = new ResourceChangeResult();
|
||||
if (isTimeToRefresh()) {
|
||||
retval = refreshCacheWithRetry();
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
private boolean isTimeToRefresh() {
|
||||
return myNextRefreshTime.isBefore(now());
|
||||
}
|
||||
|
||||
private static Instant now() {
|
||||
if (ourNowForUnitTests != null) {
|
||||
return ourNowForUnitTests;
|
||||
}
|
||||
return Instant.now();
|
||||
}
|
||||
|
||||
public ResourceChangeResult refreshCacheWithRetry() {
|
||||
ResourceChangeResult retval;
|
||||
try {
|
||||
retval = refreshCacheAndNotifyListenersWithRetry();
|
||||
} finally {
|
||||
myNextRefreshTime = now().plus(Duration.ofMillis(myRemoteRefreshIntervalMs));
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
private ResourceChangeResult refreshCacheAndNotifyListenersWithRetry() {
|
||||
Retrier<ResourceChangeResult> refreshCacheRetrier = new Retrier<>(() -> {
|
||||
synchronized (this) {
|
||||
return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
|
||||
}
|
||||
}, MAX_RETRIES);
|
||||
return refreshCacheRetrier.runWithRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getNextRefreshTime() {
|
||||
return myNextRefreshTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchParameterMap getSearchParameterMap() {
|
||||
return mySearchParameterMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return myInitialized;
|
||||
}
|
||||
|
||||
public ResourceChangeListenerCache setInitialized(boolean theInitialized) {
|
||||
myInitialized = theInitialized;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourceName() {
|
||||
return myResourceName;
|
||||
}
|
||||
|
||||
public ResourceVersionCache getResourceVersionCache() {
|
||||
return myResourceVersionCache;
|
||||
}
|
||||
|
||||
public IResourceChangeListener getResourceChangeListener() {
|
||||
return myResourceChangeListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param theTime has format like "12:34:56" i.e. HH:MM:SS
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static void setNowForUnitTests(String theTime) {
|
||||
if (theTime == null) {
|
||||
ourNowForUnitTests = null;
|
||||
return;
|
||||
}
|
||||
String datetime = "2020-11-16T" + theTime + "Z";
|
||||
Clock clock = Clock.fixed(Instant.parse(datetime), ZoneId.systemDefault());
|
||||
ourNowForUnitTests = Instant.now(clock);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Instant getNextRefreshTimeForUnitTest() {
|
||||
return myNextRefreshTime;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void clearForUnitTest() {
|
||||
requestRefresh();
|
||||
myResourceVersionCache.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("myResourceName", myResourceName)
|
||||
.append("mySearchParameterMap", mySearchParameterMap)
|
||||
.append("myInitialized", myInitialized)
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceChangeListenerCacheFactory {
|
||||
@Autowired
|
||||
ApplicationContext myApplicationContext;
|
||||
|
||||
public ResourceChangeListenerCache create(String theResourceName, SearchParameterMap theMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs) {
|
||||
return myApplicationContext.getBean(ResourceChangeListenerCache.class, theResourceName, theResourceChangeListener, theMap, theRemoteRefreshIntervalMs);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.model.sched.HapiJob;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This service refreshes the {@link IResourceChangeListenerCache} caches and notifies their listener when
|
||||
* those caches change.
|
||||
*
|
||||
* Think of it like a Ferris Wheel that completes a full rotation once every 10 seconds.
|
||||
* Every time a chair passes the bottom it checks to see if it's time to refresh that seat. If so,
|
||||
* the Ferris Wheel stops, removes the riders, and loads a fresh cache for that chair, and calls the listener
|
||||
* if any entries in the new cache are different from the last time that cache was loaded.
|
||||
*/
|
||||
@Service
|
||||
public class ResourceChangeListenerCacheRefresherImpl implements IResourceChangeListenerCacheRefresher {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCacheRefresherImpl.class);
|
||||
|
||||
/**
|
||||
* All cache entries are checked at this interval to see if they need to be refreshed
|
||||
*/
|
||||
static long LOCAL_REFRESH_INTERVAL_MS = 10 * DateUtils.MILLIS_PER_SECOND;
|
||||
|
||||
@Autowired
|
||||
private ISchedulerService mySchedulerService;
|
||||
@Autowired
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
@Autowired
|
||||
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
|
||||
jobDetail.setId(getClass().getName());
|
||||
jobDetail.setJobClass(Job.class);
|
||||
mySchedulerService.scheduleLocalJob(LOCAL_REFRESH_INTERVAL_MS, jobDetail);
|
||||
}
|
||||
|
||||
public static class Job implements HapiJob {
|
||||
@Autowired
|
||||
private IResourceChangeListenerCacheRefresher myTarget;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext theContext) {
|
||||
myTarget.refreshExpiredCachesAndNotifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceChangeResult refreshExpiredCachesAndNotifyListeners() {
|
||||
ResourceChangeResult retval = new ResourceChangeResult();
|
||||
Iterator<ResourceChangeListenerCache> iterator = myResourceChangeListenerRegistry.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ResourceChangeListenerCache entry = iterator.next();
|
||||
retval = retval.plus(entry.refreshCacheIfNecessary());
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ResourceChangeResult forceRefreshAllCachesForUnitTest() {
|
||||
ResourceChangeResult retval = new ResourceChangeResult();
|
||||
Iterator<ResourceChangeListenerCache> iterator = myResourceChangeListenerRegistry.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
IResourceChangeListenerCache entry = iterator.next();
|
||||
retval = retval.plus(entry.forceRefresh());
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
public ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListenerCache theCache) {
|
||||
ResourceChangeResult retval = new ResourceChangeResult();
|
||||
if (!myResourceChangeListenerRegistry.contains(theCache)) {
|
||||
ourLog.warn("Requesting cache refresh for unregistered listener {}. Aborting.", theCache);
|
||||
return new ResourceChangeResult();
|
||||
}
|
||||
SearchParameterMap searchParamMap = theCache.getSearchParameterMap();
|
||||
ResourceVersionMap newResourceVersionMap = myResourceVersionSvc.getVersionMap(theCache.getResourceName(), searchParamMap);
|
||||
retval = retval.plus(notifyListener(theCache, newResourceVersionMap));
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify a listener with all matching resources if it hasn't been initialized yet, otherwise only notify it if
|
||||
* any resources have changed
|
||||
* @param theCache
|
||||
* @param theNewResourceVersionMap the measured new resources
|
||||
* @return the list of created, updated and deleted ids
|
||||
*/
|
||||
ResourceChangeResult notifyListener(IResourceChangeListenerCache theCache, ResourceVersionMap theNewResourceVersionMap) {
|
||||
ResourceChangeResult retval;
|
||||
ResourceChangeListenerCache cache = (ResourceChangeListenerCache) theCache;
|
||||
IResourceChangeListener resourceChangeListener = cache.getResourceChangeListener();
|
||||
if (theCache.isInitialized()) {
|
||||
retval = compareLastVersionMapToNewVersionMapAndNotifyListenerOfChanges(resourceChangeListener, cache.getResourceVersionCache(), theNewResourceVersionMap);
|
||||
} else {
|
||||
cache.getResourceVersionCache().initialize(theNewResourceVersionMap);
|
||||
resourceChangeListener.handleInit(theNewResourceVersionMap.getSourceIds());
|
||||
retval = ResourceChangeResult.fromCreated(theNewResourceVersionMap.size());
|
||||
cache.setInitialized(true);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
private ResourceChangeResult compareLastVersionMapToNewVersionMapAndNotifyListenerOfChanges(IResourceChangeListener theListener, ResourceVersionCache theOldResourceVersionCache, ResourceVersionMap theNewResourceVersionMap) {
|
||||
// If the new ResourceVersionMap does not have the old key - delete it
|
||||
List<IIdType> deletedIds = new ArrayList<>();
|
||||
theOldResourceVersionCache.keySet()
|
||||
.forEach(id -> {
|
||||
if (!theNewResourceVersionMap.containsKey(id)) {
|
||||
deletedIds.add(id);
|
||||
}
|
||||
});
|
||||
deletedIds.forEach(theOldResourceVersionCache::removeResourceId);
|
||||
|
||||
List<IIdType> createdIds = new ArrayList<>();
|
||||
List<IIdType> updatedIds = new ArrayList<>();
|
||||
|
||||
for (IIdType id : theNewResourceVersionMap.keySet()) {
|
||||
String previousValue = theOldResourceVersionCache.put(id, theNewResourceVersionMap.get(id));
|
||||
IIdType newId = id.withVersion(theNewResourceVersionMap.get(id));
|
||||
if (previousValue == null) {
|
||||
createdIds.add(newId);
|
||||
} else if (!theNewResourceVersionMap.get(id).equals(previousValue)) {
|
||||
updatedIds.add(newId);
|
||||
}
|
||||
}
|
||||
|
||||
IResourceChangeEvent resourceChangeEvent = ResourceChangeEvent.fromCreatedUpdatedDeletedResourceIds(createdIds, updatedIds, deletedIds);
|
||||
if (!resourceChangeEvent.isEmpty()) {
|
||||
theListener.handleChange(resourceChangeEvent);
|
||||
}
|
||||
return ResourceChangeResult.fromResourceChangeEvent(resourceChangeEvent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Iterator;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* This component holds an in-memory list of all registered {@link IResourceChangeListener} instances along
|
||||
* with their caches and other details needed to maintain those caches. Register an {@link IResourceChangeListener} instance
|
||||
* with this service to be notified when resources you care about are changed. This service quickly notifies listeners
|
||||
* of changes that happened on the local process and also eventually notifies listeners of changes that were made by
|
||||
* remote processes.
|
||||
*/
|
||||
@Component
|
||||
public class ResourceChangeListenerRegistryImpl implements IResourceChangeListenerRegistry {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerRegistryImpl.class);
|
||||
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private InMemoryResourceMatcher myInMemoryResourceMatcher;
|
||||
@Autowired
|
||||
ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
|
||||
|
||||
private final Queue<ResourceChangeListenerCache> myListenerEntries = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/**
|
||||
* Register a listener in order to be notified whenever a resource matching the provided SearchParameterMap
|
||||
* changes in any way. If the change happened on the same jvm process where this registry resides, then the listener will be called
|
||||
* within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of the change happening. If the change happened
|
||||
* on a different jvm process, then the listener will be called within theRemoteRefreshIntervalMs.
|
||||
* @param theResourceName the type of the resource the listener should be notified about (e.g. "Subscription" or "SearchParameter")
|
||||
* @param theSearchParameterMap the listener will only be notified of changes to resources that match this map
|
||||
* @param theResourceChangeListener the listener that will be called whenever resource changes are detected
|
||||
* @param theRemoteRefreshIntervalMs the number of milliseconds between checking the database for changed resources that match the search parameter map
|
||||
* @throws ca.uhn.fhir.parser.DataFormatException if theResourceName is not a valid resource type in our FhirContext
|
||||
* @throws IllegalArgumentException if theSearchParamMap cannot be evaluated in-memory
|
||||
* @return RegisteredResourceChangeListener that stores the resource id cache, and the next refresh time
|
||||
*/
|
||||
@Override
|
||||
public IResourceChangeListenerCache registerResourceResourceChangeListener(String theResourceName, SearchParameterMap theSearchParameterMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs) {
|
||||
// Clone searchparameter map
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceName);
|
||||
InMemoryMatchResult inMemoryMatchResult = myInMemoryResourceMatcher.canBeEvaluatedInMemory(theSearchParameterMap, resourceDef);
|
||||
if (!inMemoryMatchResult.supported()) {
|
||||
throw new IllegalArgumentException("SearchParameterMap " + theSearchParameterMap + " cannot be evaluated in-memory: " + inMemoryMatchResult.getUnsupportedReason() + ". Only search parameter maps that can be evaluated in-memory may be registered.");
|
||||
}
|
||||
return add(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a listener from this service
|
||||
*
|
||||
* @param theResourceChangeListener
|
||||
*/
|
||||
@Override
|
||||
public void unregisterResourceResourceChangeListener(IResourceChangeListener theResourceChangeListener) {
|
||||
myListenerEntries.removeIf(l -> l.getResourceChangeListener().equals(theResourceChangeListener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterResourceResourceChangeListener(IResourceChangeListenerCache theResourceChangeListenerCache) {
|
||||
myListenerEntries.remove(theResourceChangeListenerCache);
|
||||
}
|
||||
|
||||
private IResourceChangeListenerCache add(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theMap, long theRemoteRefreshIntervalMs) {
|
||||
ResourceChangeListenerCache retval = myResourceChangeListenerCacheFactory.create(theResourceName, theMap, theResourceChangeListener, theRemoteRefreshIntervalMs);
|
||||
myListenerEntries.add(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Iterator<ResourceChangeListenerCache> iterator() {
|
||||
return myListenerEntries.iterator();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return myListenerEntries.size();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void clearCachesForUnitTest() {
|
||||
myListenerEntries.forEach(ResourceChangeListenerCache::clearForUnitTest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(IResourceChangeListenerCache theCache) {
|
||||
return myListenerEntries.contains(theCache);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public int getResourceVersionCacheSizeForUnitTest() {
|
||||
int retval = 0;
|
||||
for (ResourceChangeListenerCache entry : myListenerEntries) {
|
||||
retval += entry.getResourceVersionCache().size();
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestRefreshIfWatching(IBaseResource theResource) {
|
||||
String resourceName = myFhirContext.getResourceType(theResource);
|
||||
for (ResourceChangeListenerCache entry : myListenerEntries) {
|
||||
if (resourceName.equals(entry.getResourceName())) {
|
||||
entry.requestRefreshIfWatching(theResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@VisibleForTesting
|
||||
public void clearListenersForUnitTest() {
|
||||
myListenerEntries.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* This interceptor watches all resource changes on the server and compares them to the {@link IResourceChangeListenerCache}
|
||||
* entries. If the resource matches the resource type and search parameter map of that entry, then the corresponding cache
|
||||
* will be expired so it is refreshed and listeners are notified of that change within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS}.
|
||||
*/
|
||||
@Service
|
||||
public class ResourceChangeListenerRegistryInterceptor {
|
||||
@Autowired
|
||||
private IInterceptorService myInterceptorBroadcaster;
|
||||
@Autowired
|
||||
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
myInterceptorBroadcaster.registerInterceptor(this);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
myInterceptorBroadcaster.unregisterInterceptor(this);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
||||
public void created(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
|
||||
public void deleted(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
|
||||
public void updated(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
private void handle(IBaseResource theResource) {
|
||||
if (theResource == null) {
|
||||
return;
|
||||
}
|
||||
myResourceChangeListenerRegistry.requestRefreshIfWatching(theResource);
|
||||
}
|
||||
}
|
46
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeResult.java
vendored
Normal file
46
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceChangeResult.java
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
/**
|
||||
* An immutable object containing the count of resource creates, updates and deletes detected by a cache refresh operation.
|
||||
* Used internally for testing.
|
||||
*/
|
||||
public class ResourceChangeResult {
|
||||
public final long created;
|
||||
public final long updated;
|
||||
public final long deleted;
|
||||
|
||||
public ResourceChangeResult() {
|
||||
created = 0;
|
||||
updated = 0;
|
||||
deleted = 0;
|
||||
}
|
||||
|
||||
private ResourceChangeResult(long theCreated, long theUpdated, long theDeleted) {
|
||||
created = theCreated;
|
||||
updated = theUpdated;
|
||||
deleted = theDeleted;
|
||||
}
|
||||
|
||||
public static ResourceChangeResult fromCreated(int theCreated) {
|
||||
return new ResourceChangeResult(theCreated, 0, 0);
|
||||
}
|
||||
|
||||
public static ResourceChangeResult fromResourceChangeEvent(IResourceChangeEvent theResourceChangeEvent) {
|
||||
return new ResourceChangeResult(theResourceChangeEvent.getCreatedResourceIds().size(), theResourceChangeEvent.getUpdatedResourceIds().size(), theResourceChangeEvent.getDeletedResourceIds().size());
|
||||
}
|
||||
|
||||
public ResourceChangeResult plus(ResourceChangeResult theResult) {
|
||||
return new ResourceChangeResult(created + theResult.created, updated + theResult.updated, deleted + theResult.deleted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("created", created)
|
||||
.append("updated", updated)
|
||||
.append("deleted", deleted)
|
||||
.toString();
|
||||
}
|
||||
}
|
51
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceVersionCache.java
vendored
Normal file
51
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceVersionCache.java
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This maintains a mapping of resource id to resource version. We cache these in order to
|
||||
* detect resources that were modified on remote servers in our cluster.
|
||||
*/
|
||||
public class ResourceVersionCache {
|
||||
private final Map<IIdType, String> myVersionMap = new HashMap<>();
|
||||
|
||||
public void clear() {
|
||||
myVersionMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param theResourceId
|
||||
* @param theVersion
|
||||
* @return previous value
|
||||
*/
|
||||
public String put(IIdType theResourceId, String theVersion) {
|
||||
return myVersionMap.put(new IdDt(theResourceId).toVersionless(), theVersion);
|
||||
}
|
||||
|
||||
public String getVersionForResourceId(IIdType theResourceId) {
|
||||
return myVersionMap.get(new IdDt(theResourceId));
|
||||
}
|
||||
|
||||
public String removeResourceId(IIdType theResourceId) {
|
||||
return myVersionMap.remove(new IdDt(theResourceId));
|
||||
}
|
||||
|
||||
public void initialize(ResourceVersionMap theResourceVersionMap) {
|
||||
for (IIdType resourceId : theResourceVersionMap.keySet()) {
|
||||
myVersionMap.put(resourceId, theResourceVersionMap.get(resourceId));
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return myVersionMap.size();
|
||||
}
|
||||
|
||||
public Set<IIdType> keySet() {
|
||||
return myVersionMap.keySet();
|
||||
}
|
||||
}
|
68
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceVersionMap.java
vendored
Normal file
68
hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/cache/ResourceVersionMap.java
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This immutable map holds a copy of current resource versions read from the repository.
|
||||
*/
|
||||
public class ResourceVersionMap {
|
||||
private final Set<IIdType> mySourceIds = new HashSet<>();
|
||||
private final Map<IIdType, String> myMap = new HashMap<>();
|
||||
private ResourceVersionMap() {}
|
||||
|
||||
public static ResourceVersionMap fromResourceTableEntities(List<ResourceTable> theEntities) {
|
||||
ResourceVersionMap retval = new ResourceVersionMap();
|
||||
theEntities.forEach(entity -> retval.add(entity.getIdDt()));
|
||||
return retval;
|
||||
}
|
||||
|
||||
public static ResourceVersionMap fromResources(List<? extends IBaseResource> theResources) {
|
||||
ResourceVersionMap retval = new ResourceVersionMap();
|
||||
theResources.forEach(resource -> retval.add(resource.getIdElement()));
|
||||
return retval;
|
||||
}
|
||||
|
||||
public static ResourceVersionMap empty() {
|
||||
return new ResourceVersionMap();
|
||||
}
|
||||
|
||||
private void add(IIdType theId) {
|
||||
IdDt id = new IdDt(theId);
|
||||
mySourceIds.add(id);
|
||||
myMap.put(id.toUnqualifiedVersionless(), id.getVersionIdPart());
|
||||
}
|
||||
|
||||
public String getVersion(IIdType theResourceId) {
|
||||
return myMap.get(new IdDt(theResourceId.toUnqualifiedVersionless()));
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return myMap.size();
|
||||
}
|
||||
|
||||
public Set<IIdType> keySet() {
|
||||
return Collections.unmodifiableSet(myMap.keySet());
|
||||
}
|
||||
|
||||
public Set<IIdType> getSourceIds() {
|
||||
return Collections.unmodifiableSet(mySourceIds);
|
||||
}
|
||||
|
||||
public String get(IIdType theId) {
|
||||
return myMap.get(new IdDt(theId.toUnqualifiedVersionless()));
|
||||
}
|
||||
|
||||
public boolean containsKey(IIdType theId) {
|
||||
return myMap.containsKey(new IdDt(theId.toUnqualifiedVersionless()));
|
||||
}
|
||||
}
|
|
@ -21,7 +21,15 @@ package ca.uhn.fhir.jpa.searchparam.config;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCacheRefresher;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCache;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu2;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
|
||||
|
@ -38,10 +46,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class SearchParamConfig {
|
||||
|
||||
@Autowired
|
||||
|
@ -94,13 +101,32 @@ public class SearchParamConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public InMemoryResourceMatcher InMemoryResourceMatcher() {
|
||||
public InMemoryResourceMatcher inMemoryResourceMatcher() {
|
||||
return new InMemoryResourceMatcher();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SearchParamMatcher SearchParamMatcher() {
|
||||
public SearchParamMatcher searchParamMatcher() {
|
||||
return new SearchParamMatcher();
|
||||
}
|
||||
|
||||
@Bean
|
||||
IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
|
||||
return new ResourceChangeListenerRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
IResourceChangeListenerCacheRefresher resourceChangeListenerCacheRefresher() {
|
||||
return new ResourceChangeListenerCacheRefresherImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ResourceChangeListenerCacheFactory registeredResourceListenerFactory() {
|
||||
return new ResourceChangeListenerCacheFactory();
|
||||
}
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
ResourceChangeListenerCache registeredResourceChangeListener(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
|
||||
return new ResourceChangeListenerCache(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,13 +26,25 @@ public class InMemoryMatchResult {
|
|||
public static final String CHAIN = "Chained parameters are not supported";
|
||||
public static final String PARAM = "Parameter not supported";
|
||||
public static final String QUALIFIER = "Qualified parameter not supported";
|
||||
public static final String LOCATION_NEAR = "Location.position near not supported";
|
||||
public static final String LOCATION_NEAR = "Location.position near not supported";
|
||||
|
||||
private final boolean myMatch;
|
||||
private final boolean myMatch;
|
||||
/**
|
||||
* True if it is expected that a search will be performed in-memory
|
||||
*/
|
||||
private final boolean mySupported;
|
||||
/**
|
||||
* if mySupported is false, then the parameter responsible for in-memory search not being supported
|
||||
*/
|
||||
private final String myUnsupportedParameter;
|
||||
/**
|
||||
* if mySupported is false, then the reason in-memory search is not supported
|
||||
*/
|
||||
private final String myUnsupportedReason;
|
||||
|
||||
/**
|
||||
* Only used by CompositeInMemoryDaoSubscriptionMatcher to track whether we had to go
|
||||
* out to the database to resolve the match.
|
||||
*/
|
||||
private boolean myInMemory = false;
|
||||
|
||||
private InMemoryMatchResult(boolean theMatch) {
|
||||
|
@ -43,10 +55,10 @@ public class InMemoryMatchResult {
|
|||
}
|
||||
|
||||
private InMemoryMatchResult(String theUnsupportedParameter, String theUnsupportedReason) {
|
||||
this.myMatch = false;
|
||||
this.mySupported = false;
|
||||
this.myUnsupportedParameter = theUnsupportedParameter;
|
||||
this.myUnsupportedReason = theUnsupportedReason;
|
||||
myMatch = false;
|
||||
mySupported = false;
|
||||
myUnsupportedParameter = theUnsupportedParameter;
|
||||
myUnsupportedReason = theUnsupportedReason;
|
||||
}
|
||||
|
||||
public static InMemoryMatchResult successfulMatch() {
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -83,17 +84,42 @@ public class InMemoryResourceMatcher {
|
|||
return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.PARSE_FAIL);
|
||||
}
|
||||
searchParameterMap.clean();
|
||||
if (searchParameterMap.getLastUpdated() != null) {
|
||||
return match(searchParameterMap, theResource, resourceDefinition, theSearchParams);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param theCriteria
|
||||
* @return result.supported() will be true if theCriteria can be evaluated in-memory
|
||||
*/
|
||||
public InMemoryMatchResult canBeEvaluatedInMemory(String theCriteria) {
|
||||
return match(theCriteria, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param theSearchParameterMap
|
||||
* @param theResourceDefinition
|
||||
* @return result.supported() will be true if theSearchParameterMap can be evaluated in-memory
|
||||
*/
|
||||
public InMemoryMatchResult canBeEvaluatedInMemory(SearchParameterMap theSearchParameterMap, RuntimeResourceDefinition theResourceDefinition) {
|
||||
return match(theSearchParameterMap, null, theResourceDefinition, null);
|
||||
}
|
||||
|
||||
|
||||
@Nonnull
|
||||
public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
|
||||
if (theSearchParameterMap.getLastUpdated() != null) {
|
||||
return InMemoryMatchResult.unsupportedFromParameterAndReason(Constants.PARAM_LASTUPDATED, InMemoryMatchResult.STANDARD_PARAMETER);
|
||||
}
|
||||
if (searchParameterMap.containsKey(Location.SP_NEAR)) {
|
||||
if (theSearchParameterMap.containsKey(Location.SP_NEAR)) {
|
||||
return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.LOCATION_NEAR);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<List<IQueryParameterType>>> entry : searchParameterMap.entrySet()) {
|
||||
for (Map.Entry<String, List<List<IQueryParameterType>>> entry : theSearchParameterMap.entrySet()) {
|
||||
String theParamName = entry.getKey();
|
||||
List<List<IQueryParameterType>> theAndOrParams = entry.getValue();
|
||||
InMemoryMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, resourceDefinition, theResource, theSearchParams);
|
||||
InMemoryMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, theResource, theSearchParams);
|
||||
if (!result.matched()) {
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -20,12 +20,19 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SearchParamMatcher {
|
||||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private IndexedSearchParamExtractor myIndexedSearchParamExtractor;
|
||||
@Autowired
|
||||
|
@ -35,4 +42,13 @@ public class SearchParamMatcher {
|
|||
ResourceIndexedSearchParams resourceIndexedSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, theRequest);
|
||||
return myInMemoryResourceMatcher.match(theCriteria, theResource, resourceIndexedSearchParams);
|
||||
}
|
||||
|
||||
public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource) {
|
||||
if (theSearchParameterMap.isEmpty()) {
|
||||
return InMemoryMatchResult.successfulMatch();
|
||||
}
|
||||
ResourceIndexedSearchParams resourceIndexedSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, null);
|
||||
RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResource);
|
||||
return myInMemoryResourceMatcher.match(theSearchParameterMap, theResource, resourceDefinition, resourceIndexedSearchParams);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,10 @@ package ca.uhn.fhir.jpa.searchparam.registry;
|
|||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
public interface ISearchParamProvider {
|
||||
IBundleProvider search(SearchParameterMap theParams);
|
||||
|
||||
int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval);
|
||||
IBaseResource read(IIdType theSearchParamId);
|
||||
}
|
||||
|
|
|
@ -23,16 +23,13 @@ package ca.uhn.fhir.jpa.searchparam.registry;
|
|||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public interface ISearchParamRegistry {
|
||||
|
||||
|
@ -46,9 +43,12 @@ public interface ISearchParamRegistry {
|
|||
*/
|
||||
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
|
||||
|
||||
boolean refreshCacheIfNecessary();
|
||||
/**
|
||||
* @return the number of search parameter entries changed
|
||||
*/
|
||||
ResourceChangeResult refreshCacheIfNecessary();
|
||||
|
||||
Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams();
|
||||
ReadOnlySearchParamCache getActiveSearchParams();
|
||||
|
||||
Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName);
|
||||
|
||||
|
@ -79,9 +79,6 @@ public interface ISearchParamRegistry {
|
|||
* such as <code>_id</code> and <code>_lastUpdated</code>.
|
||||
*/
|
||||
default Collection<String> getValidSearchParameterNamesIncludingMeta(String theResourceName) {
|
||||
TreeSet<String> retVal = new TreeSet<>(getActiveSearchParams().get(theResourceName).keySet());
|
||||
retVal.add(IAnyResource.SP_RES_ID);
|
||||
retVal.add(Constants.PARAM_LASTUPDATED);
|
||||
return retVal;
|
||||
return getActiveSearchParams().getValidSearchParameterNamesIncludingMeta(theResourceName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.registry;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JpaSearchParamCache {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(JpaSearchParamCache.class);
|
||||
|
||||
private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap();
|
||||
private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap();
|
||||
|
||||
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName) {
|
||||
List<JpaRuntimeSearchParam> retval = myActiveUniqueSearchParams.get(theResourceName);
|
||||
if (retval == null) {
|
||||
retval = Collections.emptyList();
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName, Set<String> theParamNames) {
|
||||
Map<Set<String>, List<JpaRuntimeSearchParam>> paramNamesToParams = myActiveParamNamesToUniqueSearchParams.get(theResourceName);
|
||||
if (paramNamesToParams == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<JpaRuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames);
|
||||
if (retVal == null) {
|
||||
retVal = Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableList(retVal);
|
||||
}
|
||||
|
||||
void populateActiveSearchParams(IInterceptorService theInterceptorBroadcaster, IPhoneticEncoder theDefaultPhoneticEncoder, RuntimeSearchParamCache theActiveSearchParams) {
|
||||
Map<String, List<JpaRuntimeSearchParam>> activeUniqueSearchParams = new HashMap<>();
|
||||
Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> activeParamNamesToUniqueSearchParams = new HashMap<>();
|
||||
|
||||
Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>();
|
||||
List<JpaRuntimeSearchParam> jpaSearchParams = new ArrayList<>();
|
||||
|
||||
/*
|
||||
* Loop through parameters and find JPA params
|
||||
*/
|
||||
for (String theResourceName : theActiveSearchParams.getResourceNameKeys()) {
|
||||
Map<String, RuntimeSearchParam> searchParamMap = theActiveSearchParams.getSearchParamMap(theResourceName);
|
||||
List<JpaRuntimeSearchParam> uniqueSearchParams = activeUniqueSearchParams.computeIfAbsent(theResourceName, k -> new ArrayList<>());
|
||||
Collection<RuntimeSearchParam> nextSearchParamsForResourceName = searchParamMap.values();
|
||||
|
||||
ourLog.trace("Resource {} has {} params", theResourceName, searchParamMap.size());
|
||||
|
||||
for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) {
|
||||
|
||||
ourLog.trace("Resource {} has parameter {} with ID {}", theResourceName, nextCandidate.getName(), nextCandidate.getId());
|
||||
|
||||
if (nextCandidate.getId() != null) {
|
||||
idToRuntimeSearchParam.put(nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate);
|
||||
}
|
||||
|
||||
if (nextCandidate instanceof JpaRuntimeSearchParam) {
|
||||
JpaRuntimeSearchParam nextCandidateCasted = (JpaRuntimeSearchParam) nextCandidate;
|
||||
jpaSearchParams.add(nextCandidateCasted);
|
||||
if (nextCandidateCasted.isUnique()) {
|
||||
uniqueSearchParams.add(nextCandidateCasted);
|
||||
}
|
||||
}
|
||||
|
||||
setPhoneticEncoder(theDefaultPhoneticEncoder, nextCandidate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ourLog.trace("Have {} search params loaded", idToRuntimeSearchParam.size());
|
||||
|
||||
Set<String> haveSeen = new HashSet<>();
|
||||
for (JpaRuntimeSearchParam next : jpaSearchParams) {
|
||||
if (!haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<String> paramNames = new HashSet<>();
|
||||
for (JpaRuntimeSearchParam.Component nextComponent : next.getComponents()) {
|
||||
String nextRef = nextComponent.getReference().getReferenceElement().toUnqualifiedVersionless().getValue();
|
||||
RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef);
|
||||
if (componentTarget != null) {
|
||||
next.getCompositeOf().add(componentTarget);
|
||||
paramNames.add(componentTarget.getName());
|
||||
} else {
|
||||
String existingParams = idToRuntimeSearchParam
|
||||
.keySet()
|
||||
.stream()
|
||||
.sorted()
|
||||
.collect(Collectors.joining(", "));
|
||||
String message = "Search parameter " + next.getId().toUnqualifiedVersionless().getValue() + " refers to unknown component " + nextRef + ", ignoring this parameter (valid values: " + existingParams + ")";
|
||||
ourLog.warn(message);
|
||||
|
||||
// Interceptor broadcast: JPA_PERFTRACE_WARNING
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, null)
|
||||
.add(ServletRequestDetails.class, null)
|
||||
.add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message));
|
||||
theInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params);
|
||||
}
|
||||
}
|
||||
|
||||
if (next.getCompositeOf() != null) {
|
||||
next.getCompositeOf().sort((theO1, theO2) -> StringUtils.compare(theO1.getName(), theO2.getName()));
|
||||
for (String nextBase : next.getBase()) {
|
||||
activeParamNamesToUniqueSearchParams.computeIfAbsent(nextBase, v -> new HashMap<>());
|
||||
activeParamNamesToUniqueSearchParams.get(nextBase).computeIfAbsent(paramNames, t -> new ArrayList<>());
|
||||
activeParamNamesToUniqueSearchParams.get(nextBase).get(paramNames).add(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ourLog.info("Have {} unique search params", activeParamNamesToUniqueSearchParams.size());
|
||||
|
||||
myActiveUniqueSearchParams = activeUniqueSearchParams;
|
||||
myActiveParamNamesToUniqueSearchParams = activeParamNamesToUniqueSearchParams;
|
||||
}
|
||||
|
||||
void setPhoneticEncoder(IPhoneticEncoder theDefaultPhoneticEncoder, RuntimeSearchParam searchParam) {
|
||||
if ("phonetic".equals(searchParam.getName())) {
|
||||
ourLog.debug("Setting search param {} on {} phonetic encoder to {}",
|
||||
searchParam.getName(), searchParam.getPath(), theDefaultPhoneticEncoder == null ? "null" : theDefaultPhoneticEncoder.name());
|
||||
searchParam.setPhoneticEncoder(theDefaultPhoneticEncoder);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.registry;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ReadOnlySearchParamCache {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ReadOnlySearchParamCache.class);
|
||||
// resourceName -> searchParamName -> searchparam
|
||||
protected final Map<String, Map<String, RuntimeSearchParam>> myMap;
|
||||
|
||||
ReadOnlySearchParamCache() {
|
||||
myMap = new HashMap<>();
|
||||
}
|
||||
|
||||
private ReadOnlySearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) {
|
||||
myMap = theRuntimeSearchParamCache.myMap;
|
||||
}
|
||||
|
||||
public static ReadOnlySearchParamCache fromFhirContext(FhirContext theFhirContext) {
|
||||
ReadOnlySearchParamCache retval = new ReadOnlySearchParamCache();
|
||||
|
||||
Set<String> resourceNames = theFhirContext.getResourceTypes();
|
||||
|
||||
for (String resourceName : resourceNames) {
|
||||
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
|
||||
String nextResourceName = nextResDef.getName();
|
||||
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
|
||||
retval.myMap.put(nextResourceName, nameToParam);
|
||||
|
||||
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
|
||||
nameToParam.put(nextSp.getName(), nextSp);
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
public static ReadOnlySearchParamCache fromRuntimeSearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) {
|
||||
return new ReadOnlySearchParamCache(theRuntimeSearchParamCache);
|
||||
}
|
||||
|
||||
public Stream<RuntimeSearchParam> getSearchParamStream() {
|
||||
return myMap.values().stream().flatMap(entry -> entry.values().stream());
|
||||
}
|
||||
|
||||
protected Map<String, RuntimeSearchParam> getSearchParamMap(String theResourceName) {
|
||||
Map<String, RuntimeSearchParam> retval = myMap.get(theResourceName);
|
||||
if (retval == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return Collections.unmodifiableMap(myMap.get(theResourceName));
|
||||
}
|
||||
|
||||
public Collection<String> getValidSearchParameterNamesIncludingMeta(String theResourceName) {
|
||||
TreeSet<String> retval;
|
||||
Map<String, RuntimeSearchParam> searchParamMap = myMap.get(theResourceName);
|
||||
if (searchParamMap == null) {
|
||||
retval = new TreeSet<>();
|
||||
} else {
|
||||
retval = new TreeSet<>(searchParamMap.keySet());
|
||||
}
|
||||
retval.add(IAnyResource.SP_RES_ID);
|
||||
retval.add(Constants.PARAM_LASTUPDATED);
|
||||
return retval;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return myMap.size();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.registry;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class RuntimeSearchParamCache extends ReadOnlySearchParamCache {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(RuntimeSearchParamCache.class);
|
||||
|
||||
protected RuntimeSearchParamCache() {
|
||||
}
|
||||
|
||||
public static RuntimeSearchParamCache fromReadOnlySearchParmCache(ReadOnlySearchParamCache theBuiltInSearchParams) {
|
||||
RuntimeSearchParamCache retval = new RuntimeSearchParamCache();
|
||||
retval.putAll(theBuiltInSearchParams);
|
||||
return retval;
|
||||
}
|
||||
|
||||
public void add(String theResourceName, String theName, RuntimeSearchParam theSearchParam) {
|
||||
getSearchParamMap(theResourceName).put(theName, theSearchParam);
|
||||
}
|
||||
|
||||
public void remove(String theResourceName, String theName) {
|
||||
if (!myMap.containsKey(theResourceName)) {
|
||||
return;
|
||||
}
|
||||
myMap.get(theResourceName).remove(theName);
|
||||
}
|
||||
|
||||
private void putAll(ReadOnlySearchParamCache theReadOnlySearchParamCache) {
|
||||
Set<Map.Entry<String, Map<String, RuntimeSearchParam>>> builtInSps = theReadOnlySearchParamCache.myMap.entrySet();
|
||||
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : builtInSps) {
|
||||
for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
|
||||
String nextResourceName = nextBuiltInEntry.getKey();
|
||||
getSearchParamMap(nextResourceName).put(nextParam.getName(), nextParam);
|
||||
}
|
||||
|
||||
ourLog.trace("Have {} built-in SPs for: {}", nextBuiltInEntry.getValue().size(), nextBuiltInEntry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
public RuntimeSearchParam get(String theResourceName, String theParamName) {
|
||||
RuntimeSearchParam retVal = null;
|
||||
Map<String, RuntimeSearchParam> params = myMap.get(theResourceName);
|
||||
if (params != null) {
|
||||
retVal = params.get(theParamName);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public Set<String> getResourceNameKeys() {
|
||||
return myMap.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, RuntimeSearchParam> getSearchParamMap(String theResourceName) {
|
||||
return myMap.computeIfAbsent(theResourceName, k -> new HashMap<>());
|
||||
}
|
||||
}
|
|
@ -24,39 +24,31 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.interceptor.api.Hook;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.sched.HapiJob;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.util.SearchParameterUtil;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -64,12 +56,11 @@ import java.util.stream.Collectors;
|
|||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
public class SearchParamRegistryImpl implements ISearchParamRegistry {
|
||||
|
||||
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
|
||||
public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceChangeListener {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class);
|
||||
private static final int MAX_RETRIES = 60; // 5 minutes
|
||||
private static long REFRESH_INTERVAL = 60 * DateUtils.MILLIS_PER_MINUTE;
|
||||
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
|
||||
private static long REFRESH_INTERVAL = DateUtils.MILLIS_PER_HOUR;
|
||||
|
||||
@Autowired
|
||||
private ModelConfig myModelConfig;
|
||||
@Autowired
|
||||
|
@ -77,277 +68,139 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
|
|||
@Autowired
|
||||
private FhirContext myFhirContext;
|
||||
@Autowired
|
||||
private ISchedulerService mySchedulerService;
|
||||
@Autowired
|
||||
private SearchParameterCanonicalizer mySearchParameterCanonicalizer;
|
||||
@Autowired
|
||||
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
|
||||
|
||||
private Map<String, Map<String, RuntimeSearchParam>> myBuiltInSearchParams;
|
||||
private IPhoneticEncoder myPhoneticEncoder;
|
||||
|
||||
private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap();
|
||||
private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap();
|
||||
private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams;
|
||||
private volatile long myLastRefresh;
|
||||
private volatile ReadOnlySearchParamCache myBuiltInSearchParams;
|
||||
private volatile IPhoneticEncoder myPhoneticEncoder;
|
||||
private volatile JpaSearchParamCache myJpaSearchParamCache = new JpaSearchParamCache();
|
||||
private volatile RuntimeSearchParamCache myActiveSearchParams;
|
||||
|
||||
@Autowired
|
||||
private IInterceptorService myInterceptorBroadcaster;
|
||||
private RefreshSearchParameterCacheOnUpdate myInterceptor;
|
||||
private IResourceChangeListenerCache myResourceChangeListenerCache;
|
||||
|
||||
@Override
|
||||
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
|
||||
|
||||
requiresActiveSearchParams();
|
||||
RuntimeSearchParam retVal = null;
|
||||
Map<String, RuntimeSearchParam> params = myActiveSearchParams.get(theResourceName);
|
||||
if (params != null) {
|
||||
retVal = params.get(theParamName);
|
||||
|
||||
// Can still be null in unit test scenarios
|
||||
if (myActiveSearchParams != null) {
|
||||
return myActiveSearchParams.get(theResourceName, theParamName);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
|
||||
requiresActiveSearchParams();
|
||||
return getActiveSearchParams().get(theResourceName);
|
||||
return getActiveSearchParams().getSearchParamMap(theResourceName);
|
||||
}
|
||||
|
||||
private void requiresActiveSearchParams() {
|
||||
if (myActiveSearchParams == null) {
|
||||
refreshCacheWithRetry();
|
||||
myResourceChangeListenerCache.forceRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName) {
|
||||
List<JpaRuntimeSearchParam> retVal = myActiveUniqueSearchParams.get(theResourceName);
|
||||
if (retVal == null) {
|
||||
retVal = Collections.emptyList();
|
||||
}
|
||||
return retVal;
|
||||
return myJpaSearchParamCache.getActiveUniqueSearchParams(theResourceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName, Set<String> theParamNames) {
|
||||
|
||||
Map<Set<String>, List<JpaRuntimeSearchParam>> paramNamesToParams = myActiveParamNamesToUniqueSearchParams.get(theResourceName);
|
||||
if (paramNamesToParams == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<JpaRuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames);
|
||||
if (retVal == null) {
|
||||
retVal = Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableList(retVal);
|
||||
return myJpaSearchParamCache.getActiveUniqueSearchParams(theResourceName, theParamNames);
|
||||
}
|
||||
|
||||
private Map<String, Map<String, RuntimeSearchParam>> getBuiltInSearchParams() {
|
||||
private void rebuildActiveSearchParams() {
|
||||
ourLog.info("Rebuilding SearchParamRegistry");
|
||||
SearchParameterMap params = new SearchParameterMap();
|
||||
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
|
||||
|
||||
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
|
||||
int size = allSearchParamsBp.size();
|
||||
|
||||
ourLog.trace("Loaded {} search params from the DB", size);
|
||||
|
||||
// Just in case..
|
||||
if (size >= MAX_MANAGED_PARAM_COUNT) {
|
||||
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
|
||||
size = MAX_MANAGED_PARAM_COUNT;
|
||||
}
|
||||
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
|
||||
initializeActiveSearchParams(allSearchParams);
|
||||
}
|
||||
|
||||
private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearchParams) {
|
||||
StopWatch sw = new StopWatch();
|
||||
|
||||
RuntimeSearchParamCache searchParams = RuntimeSearchParamCache.fromReadOnlySearchParmCache(getBuiltInSearchParams());
|
||||
long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams);
|
||||
ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
|
||||
removeInactiveSearchParams(searchParams);
|
||||
myActiveSearchParams = searchParams;
|
||||
|
||||
myJpaSearchParamCache.populateActiveSearchParams(myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
|
||||
ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
|
||||
}
|
||||
|
||||
private ReadOnlySearchParamCache getBuiltInSearchParams() {
|
||||
if (myBuiltInSearchParams == null) {
|
||||
myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext);
|
||||
}
|
||||
return myBuiltInSearchParams;
|
||||
}
|
||||
|
||||
private Map<String, RuntimeSearchParam> getSearchParamMap(Map<String, Map<String, RuntimeSearchParam>> searchParams, String theResourceName) {
|
||||
Map<String, RuntimeSearchParam> retVal = searchParams.computeIfAbsent(theResourceName, k -> new HashMap<>());
|
||||
return retVal;
|
||||
private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) {
|
||||
for (String resourceName : theSearchParams.getResourceNameKeys()) {
|
||||
Map<String, RuntimeSearchParam> map = theSearchParams.getSearchParamMap(resourceName);
|
||||
map.entrySet().removeIf(entry -> entry.getValue().getStatus() != RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private void populateActiveSearchParams(Map<String, Map<String, RuntimeSearchParam>> theActiveSearchParams) {
|
||||
|
||||
Map<String, List<JpaRuntimeSearchParam>> activeUniqueSearchParams = new HashMap<>();
|
||||
Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> activeParamNamesToUniqueSearchParams = new HashMap<>();
|
||||
|
||||
Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>();
|
||||
List<JpaRuntimeSearchParam> jpaSearchParams = new ArrayList<>();
|
||||
|
||||
/*
|
||||
* Loop through parameters and find JPA params
|
||||
*/
|
||||
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextResourceNameToEntries : theActiveSearchParams.entrySet()) {
|
||||
List<JpaRuntimeSearchParam> uniqueSearchParams = activeUniqueSearchParams.computeIfAbsent(nextResourceNameToEntries.getKey(), k -> new ArrayList<>());
|
||||
Collection<RuntimeSearchParam> nextSearchParamsForResourceName = nextResourceNameToEntries.getValue().values();
|
||||
|
||||
ourLog.trace("Resource {} has {} params", nextResourceNameToEntries.getKey(), nextResourceNameToEntries.getValue().size());
|
||||
|
||||
for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) {
|
||||
|
||||
ourLog.trace("Resource {} has parameter {} with ID {}", nextResourceNameToEntries.getKey(), nextCandidate.getName(), nextCandidate.getId());
|
||||
|
||||
if (nextCandidate.getId() != null) {
|
||||
idToRuntimeSearchParam.put(nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate);
|
||||
}
|
||||
|
||||
if (nextCandidate instanceof JpaRuntimeSearchParam) {
|
||||
JpaRuntimeSearchParam nextCandidateCasted = (JpaRuntimeSearchParam) nextCandidate;
|
||||
jpaSearchParams.add(nextCandidateCasted);
|
||||
if (nextCandidateCasted.isUnique()) {
|
||||
uniqueSearchParams.add(nextCandidateCasted);
|
||||
}
|
||||
}
|
||||
|
||||
setPhoneticEncoder(nextCandidate);
|
||||
}
|
||||
|
||||
private long overrideBuiltinSearchParamsWithActiveJpaSearchParams(RuntimeSearchParamCache theSearchParamCache, Collection<IBaseResource> theSearchParams) {
|
||||
if (!myModelConfig.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ourLog.trace("Have {} search params loaded", idToRuntimeSearchParam.size());
|
||||
long retval = 0;
|
||||
for (IBaseResource searchParam : theSearchParams) {
|
||||
retval += overrideSearchParam(theSearchParamCache, searchParam);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
Set<String> haveSeen = new HashSet<>();
|
||||
for (JpaRuntimeSearchParam next : jpaSearchParams) {
|
||||
if (!haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) {
|
||||
private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) {
|
||||
if (theSearchParameter == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter);
|
||||
if (runtimeSp == null) {
|
||||
return 0;
|
||||
}
|
||||
if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long retval = 0;
|
||||
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter)) {
|
||||
if (isBlank(nextBaseName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<String> paramNames = new HashSet<>();
|
||||
for (JpaRuntimeSearchParam.Component nextComponent : next.getComponents()) {
|
||||
String nextRef = nextComponent.getReference().getReferenceElement().toUnqualifiedVersionless().getValue();
|
||||
RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef);
|
||||
if (componentTarget != null) {
|
||||
next.getCompositeOf().add(componentTarget);
|
||||
paramNames.add(componentTarget.getName());
|
||||
} else {
|
||||
String existingParams = idToRuntimeSearchParam
|
||||
.keySet()
|
||||
.stream()
|
||||
.sorted()
|
||||
.collect(Collectors.joining(", "));
|
||||
String message = "Search parameter " + next.getId().toUnqualifiedVersionless().getValue() + " refers to unknown component " + nextRef + ", ignoring this parameter (valid values: " + existingParams + ")";
|
||||
ourLog.warn(message);
|
||||
|
||||
// Interceptor broadcast: JPA_PERFTRACE_WARNING
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, null)
|
||||
.add(ServletRequestDetails.class, null)
|
||||
.add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message));
|
||||
myInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params);
|
||||
}
|
||||
}
|
||||
|
||||
if (next.getCompositeOf() != null) {
|
||||
next.getCompositeOf().sort((theO1, theO2) -> StringUtils.compare(theO1.getName(), theO2.getName()));
|
||||
for (String nextBase : next.getBase()) {
|
||||
activeParamNamesToUniqueSearchParams.computeIfAbsent(nextBase, v -> new HashMap<>());
|
||||
activeParamNamesToUniqueSearchParams.get(nextBase).computeIfAbsent(paramNames, t -> new ArrayList<>());
|
||||
activeParamNamesToUniqueSearchParams.get(nextBase).get(paramNames).add(next);
|
||||
}
|
||||
}
|
||||
Map<String, RuntimeSearchParam> searchParamMap = theSearchParams.getSearchParamMap(nextBaseName);
|
||||
String name = runtimeSp.getName();
|
||||
ourLog.debug("Adding search parameter {}.{} to SearchParamRegistry", nextBaseName, StringUtils.defaultString(name, "[composite]"));
|
||||
searchParamMap.put(name, runtimeSp);
|
||||
retval++;
|
||||
}
|
||||
|
||||
ourLog.trace("Have {} unique search params", activeParamNamesToUniqueSearchParams.size());
|
||||
|
||||
myActiveUniqueSearchParams = activeUniqueSearchParams;
|
||||
myActiveParamNamesToUniqueSearchParams = activeParamNamesToUniqueSearchParams;
|
||||
return retval;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
myBuiltInSearchParams = createBuiltInSearchParamMap(myFhirContext);
|
||||
|
||||
myInterceptor = new RefreshSearchParameterCacheOnUpdate();
|
||||
myInterceptorBroadcaster.registerInterceptor(myInterceptor);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
myInterceptorBroadcaster.unregisterInterceptor(myInterceptor);
|
||||
}
|
||||
|
||||
public int doRefresh(long theRefreshInterval) {
|
||||
if (System.currentTimeMillis() - theRefreshInterval > myLastRefresh) {
|
||||
StopWatch sw = new StopWatch();
|
||||
|
||||
Map<String, Map<String, RuntimeSearchParam>> searchParams = new HashMap<>();
|
||||
Set<Map.Entry<String, Map<String, RuntimeSearchParam>>> builtInSps = getBuiltInSearchParams().entrySet();
|
||||
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : builtInSps) {
|
||||
for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
|
||||
String nextResourceName = nextBuiltInEntry.getKey();
|
||||
getSearchParamMap(searchParams, nextResourceName).put(nextParam.getName(), nextParam);
|
||||
}
|
||||
|
||||
ourLog.trace("Have {} built-in SPs for: {}", nextBuiltInEntry.getValue().size(), nextBuiltInEntry.getKey());
|
||||
}
|
||||
|
||||
SearchParameterMap params = new SearchParameterMap();
|
||||
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
|
||||
|
||||
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
|
||||
int size = allSearchParamsBp.size();
|
||||
|
||||
ourLog.trace("Loaded {} search params from the DB", size);
|
||||
|
||||
// Just in case..
|
||||
if (size >= MAX_MANAGED_PARAM_COUNT) {
|
||||
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
|
||||
size = MAX_MANAGED_PARAM_COUNT;
|
||||
}
|
||||
|
||||
int overriddenCount = 0;
|
||||
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
|
||||
for (IBaseResource nextResource : allSearchParams) {
|
||||
IBaseResource nextSp = nextResource;
|
||||
if (nextSp == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(nextSp);
|
||||
if (runtimeSp == null) {
|
||||
continue;
|
||||
}
|
||||
if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, nextSp)) {
|
||||
if (isBlank(nextBaseName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, RuntimeSearchParam> searchParamMap = getSearchParamMap(searchParams, nextBaseName);
|
||||
String name = runtimeSp.getName();
|
||||
if (!searchParamMap.containsKey(name) || myModelConfig.isDefaultSearchParamsCanBeOverridden()) {
|
||||
searchParamMap.put(name, runtimeSp);
|
||||
overriddenCount++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
|
||||
|
||||
Map<String, Map<String, RuntimeSearchParam>> activeSearchParams = new HashMap<>();
|
||||
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextEntry : searchParams.entrySet()) {
|
||||
for (RuntimeSearchParam nextSp : nextEntry.getValue().values()) {
|
||||
String nextName = nextSp.getName();
|
||||
if (nextSp.getStatus() != RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE) {
|
||||
nextSp = null;
|
||||
}
|
||||
|
||||
if (!activeSearchParams.containsKey(nextEntry.getKey())) {
|
||||
activeSearchParams.put(nextEntry.getKey(), new HashMap<>());
|
||||
}
|
||||
if (activeSearchParams.containsKey(nextEntry.getKey())) {
|
||||
ourLog.debug("Replacing existing/built in search param {}:{} with new one", nextEntry.getKey(), nextName);
|
||||
}
|
||||
|
||||
if (nextSp != null) {
|
||||
activeSearchParams.get(nextEntry.getKey()).put(nextName, nextSp);
|
||||
} else {
|
||||
activeSearchParams.get(nextEntry.getKey()).remove(nextName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
myActiveSearchParams = activeSearchParams;
|
||||
|
||||
populateActiveSearchParams(activeSearchParams);
|
||||
|
||||
myLastRefresh = System.currentTimeMillis();
|
||||
ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
|
||||
return myActiveSearchParams.size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
|
||||
Map<String, RuntimeSearchParam> params = getActiveSearchParams(theResourceDef.getName());
|
||||
|
@ -361,48 +214,36 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
|
|||
|
||||
@Override
|
||||
public void requestRefresh() {
|
||||
synchronized (this) {
|
||||
myLastRefresh = 0;
|
||||
}
|
||||
myResourceChangeListenerCache.requestRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceRefresh() {
|
||||
requestRefresh();
|
||||
refreshCacheWithRetry();
|
||||
myResourceChangeListenerCache.forceRefresh();
|
||||
}
|
||||
|
||||
int refreshCacheWithRetry() {
|
||||
Retrier<Integer> refreshCacheRetrier = new Retrier<>(() -> {
|
||||
synchronized (SearchParamRegistryImpl.this) {
|
||||
return mySearchParamProvider.refreshCache(this, REFRESH_INTERVAL);
|
||||
}
|
||||
}, MAX_RETRIES);
|
||||
return refreshCacheRetrier.runWithRetry();
|
||||
@Override
|
||||
public ResourceChangeResult refreshCacheIfNecessary() {
|
||||
return myResourceChangeListenerCache.refreshCacheIfNecessary();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void scheduleJob() {
|
||||
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
|
||||
jobDetail.setId(getClass().getName());
|
||||
jobDetail.setJobClass(Job.class);
|
||||
mySchedulerService.scheduleLocalJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail);
|
||||
public void registerListener() {
|
||||
myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("SearchParameter", SearchParameterMap.newSynchronous(), this, REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void unregisterListener() {
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshCacheIfNecessary() {
|
||||
if (myActiveSearchParams == null || System.currentTimeMillis() - REFRESH_INTERVAL > myLastRefresh) {
|
||||
refreshCacheWithRetry();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
|
||||
public ReadOnlySearchParamCache getActiveSearchParams() {
|
||||
requiresActiveSearchParams();
|
||||
return Collections.unmodifiableMap(myActiveSearchParams);
|
||||
if (myActiveSearchParams == null) {
|
||||
throw new IllegalStateException("SearchParamRegistry has not been initialized");
|
||||
}
|
||||
return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -417,72 +258,36 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
|
|||
if (myActiveSearchParams == null) {
|
||||
return;
|
||||
}
|
||||
for (Map<String, RuntimeSearchParam> activeUniqueSearchParams : myActiveSearchParams.values()) {
|
||||
for (RuntimeSearchParam searchParam : activeUniqueSearchParams.values()) {
|
||||
setPhoneticEncoder(searchParam);
|
||||
}
|
||||
}
|
||||
myActiveSearchParams.getSearchParamStream().forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam));
|
||||
}
|
||||
|
||||
private void setPhoneticEncoder(RuntimeSearchParam searchParam) {
|
||||
if ("phonetic".equals(searchParam.getName())) {
|
||||
ourLog.debug("Setting search param {} on {} phonetic encoder to {}",
|
||||
searchParam.getName(), searchParam.getPath(), myPhoneticEncoder == null ? "null" : myPhoneticEncoder.name());
|
||||
searchParam.setPhoneticEncoder(myPhoneticEncoder);
|
||||
@Override
|
||||
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
|
||||
if (theResourceChangeEvent.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent);
|
||||
if (result.created > 0) {
|
||||
ourLog.info("Adding {} search parameters to SearchParamRegistry", result.created);
|
||||
}
|
||||
if (result.updated > 0) {
|
||||
ourLog.info("Updating {} search parameters in SearchParamRegistry", result.updated);
|
||||
}
|
||||
if (result.created > 0) {
|
||||
ourLog.info("Deleting {} search parameters from SearchParamRegistry", result.deleted);
|
||||
}
|
||||
rebuildActiveSearchParams();
|
||||
}
|
||||
|
||||
@Interceptor
|
||||
public class RefreshSearchParameterCacheOnUpdate {
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
|
||||
public void created(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
|
||||
public void deleted(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
|
||||
public void updated(IBaseResource theResource) {
|
||||
handle(theResource);
|
||||
}
|
||||
|
||||
private void handle(IBaseResource theResource) {
|
||||
if (theResource != null && myFhirContext.getResourceType(theResource).equals("SearchParameter")) {
|
||||
requestRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleInit(Collection<IIdType> theResourceIds) {
|
||||
List<IBaseResource> searchParams = theResourceIds.stream().map(id -> mySearchParamProvider.read(id)).collect(Collectors.toList());
|
||||
initializeActiveSearchParams(searchParams);
|
||||
}
|
||||
|
||||
public static class Job implements HapiJob {
|
||||
@Autowired
|
||||
private ISearchParamRegistry myTarget;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext theContext) {
|
||||
myTarget.refreshCacheIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Map<String, RuntimeSearchParam>> createBuiltInSearchParamMap(FhirContext theFhirContext) {
|
||||
Map<String, Map<String, RuntimeSearchParam>> resourceNameToSearchParams = new HashMap<>();
|
||||
|
||||
Set<String> resourceNames = theFhirContext.getResourceTypes();
|
||||
|
||||
for (String resourceName : resourceNames) {
|
||||
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
|
||||
String nextResourceName = nextResDef.getName();
|
||||
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
|
||||
resourceNameToSearchParams.put(nextResourceName, nameToParam);
|
||||
|
||||
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
|
||||
nameToParam.put(nextSp.getName(), nextSp);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(resourceNameToSearchParams);
|
||||
@VisibleForTesting
|
||||
public void resetForUnitTest() {
|
||||
handleInit(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,15 +28,11 @@ import org.springframework.beans.factory.BeanCreationException;
|
|||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.RetryListener;
|
||||
import org.springframework.retry.RetryPolicy;
|
||||
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
|
||||
import org.springframework.retry.listener.RetryListenerSupport;
|
||||
import org.springframework.retry.policy.ExceptionClassifierRetryPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class Retrier<T> {
|
||||
|
@ -63,7 +59,8 @@ public class Retrier<T> {
|
|||
|
||||
@Override
|
||||
public boolean canRetry(RetryContext context) {
|
||||
if (context.getLastThrowable() instanceof BeanCreationException) {
|
||||
Throwable lastThrowable = context.getLastThrowable();
|
||||
if (lastThrowable instanceof BeanCreationException || lastThrowable instanceof NullPointerException) {
|
||||
return false;
|
||||
}
|
||||
return super.canRetry(context);
|
||||
|
@ -76,7 +73,7 @@ public class Retrier<T> {
|
|||
@Override
|
||||
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
|
||||
super.onError(context, callback, throwable);
|
||||
if (throwable instanceof NullPointerException || throwable instanceof UnsupportedOperationException) {
|
||||
if (throwable instanceof NullPointerException || throwable instanceof UnsupportedOperationException || "true".equals(System.getProperty("unit_test_mode"))) {
|
||||
ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.getMessage(), throwable);
|
||||
} else {
|
||||
ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.toString());
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class ResourceChangeListenerCacheRefresherImplTest {
|
||||
public static final String PATIENT_RESOURCE_NAME = "Patient";
|
||||
private static final SearchParameterMap ourMap = SearchParameterMap.newSynchronous();
|
||||
private static final long TEST_REFRESH_INTERVAL_MS = DateUtils.MILLIS_PER_HOUR;
|
||||
|
||||
@Autowired
|
||||
ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
|
||||
@MockBean
|
||||
private ISchedulerService mySchedulerService;
|
||||
@MockBean
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
@MockBean
|
||||
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
|
||||
|
||||
@Configuration
|
||||
@Import(RegisteredResourceListenerFactoryConfig.class)
|
||||
static class SpringContext {
|
||||
@Bean
|
||||
IResourceChangeListenerCacheRefresher resourceChangeListenerCacheRefresher() {
|
||||
return new ResourceChangeListenerCacheRefresherImpl();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyListenersEmptyEmptyNotInitialized() {
|
||||
IResourceChangeListener listener = mock(IResourceChangeListener.class);
|
||||
ResourceChangeListenerCache cache = new ResourceChangeListenerCache(PATIENT_RESOURCE_NAME, listener, ourMap, TEST_REFRESH_INTERVAL_MS);
|
||||
ResourceVersionMap newResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(Collections.emptyList());
|
||||
assertFalse(cache.isInitialized());
|
||||
myResourceChangeListenerCacheRefresher.notifyListener(cache, newResourceVersionMap);
|
||||
assertTrue(cache.isInitialized());
|
||||
verify(listener, times(1)).handleInit(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyListenersEmptyEmptyInitialized() {
|
||||
IResourceChangeListener listener = mock(IResourceChangeListener.class);
|
||||
ResourceChangeListenerCache cache = new ResourceChangeListenerCache(PATIENT_RESOURCE_NAME, listener, ourMap, TEST_REFRESH_INTERVAL_MS);
|
||||
ResourceVersionMap newResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(Collections.emptyList());
|
||||
cache.setInitialized(true);
|
||||
assertTrue(cache.isInitialized());
|
||||
myResourceChangeListenerCacheRefresher.notifyListener(cache, newResourceVersionMap);
|
||||
assertTrue(cache.isInitialized());
|
||||
verifyNoInteractions(listener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = RegisteredResourceListenerFactoryConfig.class)
|
||||
class ResourceChangeListenerCacheTest {
|
||||
private static final String TEST_RESOURCE_NAME = "Foo";
|
||||
private static final long TEST_REFRESH_INTERVAL = DateUtils.MILLIS_PER_HOUR;
|
||||
private static final IResourceChangeListener ourListener = mock(IResourceChangeListener.class);
|
||||
private static final SearchParameterMap ourMap = SearchParameterMap.newSynchronous();
|
||||
private static final Patient ourPatient = new Patient();
|
||||
|
||||
@Autowired
|
||||
private ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
|
||||
|
||||
@MockBean
|
||||
ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
|
||||
@MockBean
|
||||
SearchParamMatcher mySearchParamMatcher;
|
||||
|
||||
@Test
|
||||
public void doNotRefreshIfNotMatches() {
|
||||
ResourceChangeListenerCache cache = myResourceChangeListenerCacheFactory.create(TEST_RESOURCE_NAME, ourMap, mock(IResourceChangeListener.class), TEST_REFRESH_INTERVAL);
|
||||
cache.forceRefresh();
|
||||
assertNotEquals(Instant.MIN, cache.getNextRefreshTimeForUnitTest());
|
||||
|
||||
// Don't reset timer if it doesn't match any searchparams
|
||||
mockInMemorySupported(cache, InMemoryMatchResult.fromBoolean(false));
|
||||
cache.requestRefreshIfWatching(ourPatient);
|
||||
assertNotEquals(Instant.MIN, cache.getNextRefreshTimeForUnitTest());
|
||||
|
||||
// Reset timer if it does match searchparams
|
||||
mockInMemorySupported(cache, InMemoryMatchResult.successfulMatch());
|
||||
cache.requestRefreshIfWatching(ourPatient);
|
||||
assertEquals(Instant.MIN, cache.getNextRefreshTimeForUnitTest());
|
||||
}
|
||||
|
||||
private void mockInMemorySupported(ResourceChangeListenerCache thecache, InMemoryMatchResult theTheInMemoryMatchResult) {
|
||||
when(mySearchParamMatcher.match(thecache.getSearchParameterMap(), ourPatient)).thenReturn(theTheInMemoryMatchResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchedule() {
|
||||
ResourceChangeListenerCache cache = myResourceChangeListenerCacheFactory.create(TEST_RESOURCE_NAME, ourMap, ourListener, TEST_REFRESH_INTERVAL);
|
||||
ResourceChangeListenerCache.setNowForUnitTests("08:00:00");
|
||||
cache.refreshCacheIfNecessary();
|
||||
verify(myResourceChangeListenerCacheRefresher, times(1)).refreshCacheAndNotifyListener(any());
|
||||
|
||||
reset(myResourceChangeListenerCacheRefresher);
|
||||
ResourceChangeListenerCache.setNowForUnitTests("08:00:01");
|
||||
cache.refreshCacheIfNecessary();
|
||||
verify(myResourceChangeListenerCacheRefresher, never()).refreshCacheAndNotifyListener(any());
|
||||
|
||||
reset(myResourceChangeListenerCacheRefresher);
|
||||
ResourceChangeListenerCache.setNowForUnitTests("08:59:59");
|
||||
cache.refreshCacheIfNecessary();
|
||||
verify(myResourceChangeListenerCacheRefresher, never()).refreshCacheAndNotifyListener(any());
|
||||
|
||||
|
||||
reset(myResourceChangeListenerCacheRefresher);
|
||||
ResourceChangeListenerCache.setNowForUnitTests("09:00:00");
|
||||
cache.refreshCacheIfNecessary();
|
||||
verify(myResourceChangeListenerCacheRefresher, never()).refreshCacheAndNotifyListener(any());
|
||||
|
||||
reset(myResourceChangeListenerCacheRefresher);
|
||||
// Now that we passed TEST_REFRESH_INTERVAL, the cache should refresh
|
||||
ResourceChangeListenerCache.setNowForUnitTests("09:00:01");
|
||||
cache.refreshCacheIfNecessary();
|
||||
verify(myResourceChangeListenerCacheRefresher, times(1)).refreshCacheAndNotifyListener(any());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class ResourceChangeListenerRegistryImplTest {
|
||||
private static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
public static final String PATIENT_RESOURCE_NAME = "Patient";
|
||||
public static final String OBSERVATION_RESOURCE_NAME = "Observation";
|
||||
private static final long TEST_REFRESH_INTERVAL_MS = DateUtils.MILLIS_PER_HOUR;
|
||||
|
||||
@Autowired
|
||||
ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
|
||||
@Autowired
|
||||
ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
|
||||
@MockBean
|
||||
private ISchedulerService mySchedulerService;
|
||||
@MockBean
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
@MockBean
|
||||
private ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
|
||||
@MockBean
|
||||
private InMemoryResourceMatcher myInMemoryResourceMatcher;
|
||||
@MockBean
|
||||
private SearchParamMatcher mySearchParamMatcher;
|
||||
|
||||
private final IResourceChangeListener myTestListener = mock(IResourceChangeListener.class);
|
||||
private static final SearchParameterMap ourMap = SearchParameterMap.newSynchronous();
|
||||
|
||||
@Configuration
|
||||
@Import(RegisteredResourceListenerFactoryConfig.class)
|
||||
static class SpringContext {
|
||||
@Bean
|
||||
public IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
|
||||
return new ResourceChangeListenerRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FhirContext fhirContext() {
|
||||
return ourFhirContext;
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
Set<IResourceChangeListenerCache> entries = new HashSet<>();
|
||||
IResourceChangeListenerCache cache = myResourceChangeListenerCacheFactory.create(PATIENT_RESOURCE_NAME, ourMap, myTestListener, TEST_REFRESH_INTERVAL_MS);
|
||||
entries.add(cache);
|
||||
when(myInMemoryResourceMatcher.canBeEvaluatedInMemory(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addingListenerForNonResourceFails() {
|
||||
try {
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener("Foo", ourMap, myTestListener, TEST_REFRESH_INTERVAL_MS);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
||||
assertEquals("Unknown resource name \"Foo\" (this name is not known in FHIR version \"R4\")", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addingNonInMemorySearchParamFails() {
|
||||
try {
|
||||
mockInMemorySupported(InMemoryMatchResult.unsupportedFromReason("TEST REASON"));
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(PATIENT_RESOURCE_NAME, ourMap, myTestListener, TEST_REFRESH_INTERVAL_MS);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("SearchParameterMap SearchParameterMap[] cannot be evaluated in-memory: TEST REASON. Only search parameter maps that can be evaluated in-memory may be registered.", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void mockInMemorySupported(InMemoryMatchResult theTheInMemoryMatchResult) {
|
||||
when(myInMemoryResourceMatcher.canBeEvaluatedInMemory(ourMap, ourFhirContext.getResourceDefinition(PATIENT_RESOURCE_NAME))).thenReturn(theTheInMemoryMatchResult);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
myResourceChangeListenerRegistry.clearListenersForUnitTest();
|
||||
ResourceChangeListenerCache.setNowForUnitTests(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUnregister() {
|
||||
IResourceChangeListener listener1 = mock(IResourceChangeListener.class);
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(PATIENT_RESOURCE_NAME, ourMap, listener1, TEST_REFRESH_INTERVAL_MS);
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(OBSERVATION_RESOURCE_NAME, ourMap, listener1, TEST_REFRESH_INTERVAL_MS);
|
||||
|
||||
when(mySearchParamMatcher.match(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
|
||||
|
||||
assertEquals(2, myResourceChangeListenerRegistry.size());
|
||||
|
||||
IResourceChangeListener listener2 = mock(IResourceChangeListener.class);
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(PATIENT_RESOURCE_NAME, ourMap, listener2, TEST_REFRESH_INTERVAL_MS);
|
||||
assertEquals(3, myResourceChangeListenerRegistry.size());
|
||||
|
||||
List<ResourceChangeListenerCache> entries = Lists.newArrayList(myResourceChangeListenerRegistry.iterator());
|
||||
assertThat(entries, hasSize(3));
|
||||
|
||||
List<IResourceChangeListener> listeners = entries.stream().map(ResourceChangeListenerCache::getResourceChangeListener).collect(Collectors.toList());
|
||||
assertThat(listeners, contains(listener1, listener1, listener2));
|
||||
|
||||
List<String> resourceNames = entries.stream().map(IResourceChangeListenerCache::getResourceName).collect(Collectors.toList());
|
||||
assertThat(resourceNames, contains(PATIENT_RESOURCE_NAME, OBSERVATION_RESOURCE_NAME, PATIENT_RESOURCE_NAME));
|
||||
|
||||
IResourceChangeListenerCache firstcache = entries.iterator().next();
|
||||
// We made a copy
|
||||
assertTrue(ourMap != firstcache.getSearchParameterMap());
|
||||
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(listener1);
|
||||
assertEquals(1, myResourceChangeListenerRegistry.size());
|
||||
ResourceChangeListenerCache cache = myResourceChangeListenerRegistry.iterator().next();
|
||||
assertEquals(PATIENT_RESOURCE_NAME, cache.getResourceName());
|
||||
assertEquals(listener2, cache.getResourceChangeListener());
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(listener2);
|
||||
assertEquals(0, myResourceChangeListenerRegistry.size());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ca.uhn.fhir.jpa.cache;
|
||||
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class ResourceChangeListenerRegistryInterceptorTest {
|
||||
@Autowired
|
||||
ResourceChangeListenerRegistryInterceptor myResourceChangeListenerRegistryInterceptor;
|
||||
|
||||
@MockBean
|
||||
private IInterceptorService myInterceptorBroadcaster;
|
||||
@MockBean
|
||||
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
|
||||
|
||||
@Configuration
|
||||
static class SpringContext {
|
||||
@Bean
|
||||
public ResourceChangeListenerRegistryInterceptor resourceChangeListenerRegistryInterceptor() {
|
||||
return new ResourceChangeListenerRegistryInterceptor();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshCalled() {
|
||||
Patient patient = new Patient();
|
||||
myResourceChangeListenerRegistryInterceptor.created(patient);
|
||||
verify(myResourceChangeListenerRegistry).requestRefreshIfWatching(patient);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package ca.uhn.fhir.jpa.cache.config;
|
||||
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCache;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
@Configuration
|
||||
public class RegisteredResourceListenerFactoryConfig {
|
||||
@Bean
|
||||
ResourceChangeListenerCacheFactory resourceChangeListenerCacheFactory() {
|
||||
return new ResourceChangeListenerCacheFactory();
|
||||
}
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
ResourceChangeListenerCache resourceChangeListenerCache(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
|
||||
return new ResourceChangeListenerCache(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
|||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||
import ca.uhn.fhir.context.support.IValidationSupport;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
|
@ -19,6 +20,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
|
|||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.util.StringUtil;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
|
@ -245,13 +247,13 @@ public class SearchParamExtractorDstu3Test {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshCacheIfNecessary() {
|
||||
public ResourceChangeResult refreshCacheIfNecessary() {
|
||||
// nothing
|
||||
return false;
|
||||
return new ResourceChangeResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
|
||||
public ReadOnlySearchParamCache getActiveSearchParams() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,14 +18,17 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
|||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
|
||||
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseEnumeration;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -40,6 +43,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
// TODO JA Please fix this test. Expanding FhirContext.getResourceTypes() to cover all resource types broke this test.
|
||||
@Disabled
|
||||
public class SearchParamExtractorMegaTest {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamExtractorMegaTest.class);
|
||||
|
@ -254,13 +259,13 @@ public class SearchParamExtractorMegaTest {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshCacheIfNecessary() {
|
||||
public ResourceChangeResult refreshCacheIfNecessary() {
|
||||
// nothing
|
||||
return false;
|
||||
return new ResourceChangeResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
|
||||
public ReadOnlySearchParamCache getActiveSearchParams() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,63 +2,151 @@ package ca.uhn.fhir.jpa.searchparam.registry;
|
|||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceVersionMap;
|
||||
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class SearchParamRegistryImplTest {
|
||||
private static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
private static final ReadOnlySearchParamCache ourBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(ourFhirContext);
|
||||
|
||||
public static final int TEST_SEARCH_PARAMS = 3;
|
||||
private static List<ResourceTable> ourEntities;
|
||||
private static ResourceVersionMap ourResourceVersionMap;
|
||||
private static int ourLastId;
|
||||
private static int ourBuiltinPatientSearchParamCount;
|
||||
|
||||
static {
|
||||
ourEntities = new ArrayList<>();
|
||||
for (ourLastId = 0; ourLastId < TEST_SEARCH_PARAMS; ++ourLastId) {
|
||||
ourEntities.add(createEntity(ourLastId, 1));
|
||||
}
|
||||
ourResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(ourEntities);
|
||||
ourBuiltinPatientSearchParamCount = ReadOnlySearchParamCache.fromFhirContext(ourFhirContext).getSearchParamMap("Patient").size();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
SearchParamRegistryImpl mySearchParamRegistry;
|
||||
@Autowired
|
||||
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
|
||||
@Autowired
|
||||
private ResourceChangeListenerCacheRefresherImpl myChangeListenerCacheRefresher;
|
||||
|
||||
@MockBean
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
@MockBean
|
||||
private ISchedulerService mySchedulerService;
|
||||
@MockBean
|
||||
private ISearchParamProvider mySearchParamProvider;
|
||||
@MockBean
|
||||
private ModelConfig myModelConfig;
|
||||
@MockBean
|
||||
private IInterceptorService myInterceptorBroadcaster;
|
||||
@MockBean
|
||||
private SearchParamMatcher mySearchParamMatcher;
|
||||
@MockBean
|
||||
private MatchUrlService myMatchUrlService;
|
||||
|
||||
@Configuration
|
||||
@Import(RegisteredResourceListenerFactoryConfig.class)
|
||||
static class SpringConfig {
|
||||
@Bean
|
||||
FhirContext fhirContext() { return FhirContext.forR4(); }
|
||||
FhirContext fhirContext() {
|
||||
return ourFhirContext;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ISearchParamRegistry searchParamRegistry() { return new SearchParamRegistryImpl(); }
|
||||
ModelConfig modelConfig() {
|
||||
ModelConfig modelConfig = new ModelConfig();
|
||||
modelConfig.setDefaultSearchParamsCanBeOverridden(true);
|
||||
return modelConfig;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ISearchParamRegistry searchParamRegistry() {
|
||||
return new SearchParamRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SearchParameterCanonicalizer searchParameterCanonicalizer(FhirContext theFhirContext) {
|
||||
return new SearchParameterCanonicalizer(theFhirContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
|
||||
return new ResourceChangeListenerRegistryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ResourceChangeListenerCacheRefresherImpl resourceChangeListenerCacheRefresher() {
|
||||
return new ResourceChangeListenerCacheRefresherImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
InMemoryResourceMatcher inMemoryResourceMatcher() {
|
||||
InMemoryResourceMatcher retval = mock(InMemoryResourceMatcher.class);
|
||||
when(retval.canBeEvaluatedInMemory(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
|
||||
return retval;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static ResourceTable createEntity(long theId, int theVersion) {
|
||||
ResourceTable searchParamEntity = new ResourceTable();
|
||||
searchParamEntity.setResourceType("SearchParameter");
|
||||
searchParamEntity.setId(theId);
|
||||
searchParamEntity.setVersion(theVersion);
|
||||
return searchParamEntity;
|
||||
}
|
||||
|
||||
private int myAnswerCount = 0;
|
||||
|
@ -66,81 +154,152 @@ public class SearchParamRegistryImplTest {
|
|||
@BeforeEach
|
||||
public void before() {
|
||||
myAnswerCount = 0;
|
||||
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(ourResourceVersionMap);
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
|
||||
|
||||
// Our first refresh adds our test searchparams to the registry
|
||||
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), TEST_SEARCH_PARAMS, 0, 0);
|
||||
assertEquals(TEST_SEARCH_PARAMS, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbCalled();
|
||||
assertEquals(ourBuiltInSearchParams.size(), mySearchParamRegistry.getActiveSearchParams().size());
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
myResourceChangeListenerRegistry.clearCachesForUnitTest();
|
||||
// Empty out the searchparam registry
|
||||
mySearchParamRegistry.resetForUnitTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshAfterExpiry() {
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
|
||||
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertEquals(146, mySearchParamRegistry.doRefresh(100000));
|
||||
|
||||
// Second time we don't need to run because we ran recently
|
||||
assertEquals(0, mySearchParamRegistry.doRefresh(100000));
|
||||
|
||||
assertEquals(146, mySearchParamRegistry.getActiveSearchParams().size());
|
||||
assertEmptyResult(mySearchParamRegistry.refreshCacheIfNecessary());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshCacheIfNecessary() {
|
||||
// Second refresh does not call the database
|
||||
assertEmptyResult(mySearchParamRegistry.refreshCacheIfNecessary());
|
||||
assertEquals(TEST_SEARCH_PARAMS, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbNotCalled();
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
|
||||
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
|
||||
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
|
||||
mySearchParamRegistry.doRefresh(t.getArgument(1, Long.class));
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Requesting a refresh calls the database and adds nothing
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertEmptyResult(mySearchParamRegistry.refreshCacheIfNecessary());
|
||||
assertEquals(TEST_SEARCH_PARAMS, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbCalled();
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
|
||||
|
||||
assertTrue(mySearchParamRegistry.refreshCacheIfNecessary());
|
||||
assertFalse(mySearchParamRegistry.refreshCacheIfNecessary());
|
||||
|
||||
// Requesting a refresh after adding a new search parameter calls the database and adds one
|
||||
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertTrue(mySearchParamRegistry.refreshCacheIfNecessary());
|
||||
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 0);
|
||||
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbCalled();
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
|
||||
|
||||
// Requesting a refresh after adding a new search parameter calls the database and
|
||||
// removes the one added above and adds this new one
|
||||
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 1);
|
||||
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbCalled();
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
|
||||
|
||||
// Requesting a refresh after adding a new search parameter calls the database,
|
||||
// removes the ACTIVE one and adds the new one because this is a mock test
|
||||
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.DRAFT);
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 1);
|
||||
assertDbCalled();
|
||||
// the new one does not appear in our patient search params because it's DRAFT
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchParamUpdate() {
|
||||
// Requesting a refresh after adding a new search parameter calls the database and adds one
|
||||
List<ResourceTable> newEntities = resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 0);
|
||||
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbCalled();
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
|
||||
|
||||
// Update the resource without changing anything that would affect our cache
|
||||
ResourceTable lastEntity = newEntities.get(newEntities.size() - 1);
|
||||
lastEntity.setVersion(2);
|
||||
resetMock(Enumerations.PublicationStatus.ACTIVE, newEntities);
|
||||
mySearchParamRegistry.requestRefresh();
|
||||
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 0, 1, 0);
|
||||
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||
assertDbCalled();
|
||||
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
|
||||
}
|
||||
|
||||
private void assertPatientSearchParamSize(int theExpectedSize) {
|
||||
assertEquals(theExpectedSize, mySearchParamRegistry.getActiveSearchParams("Patient").size());
|
||||
}
|
||||
|
||||
private void assertResult(ResourceChangeResult theResult, long theExpectedAdded, long theExpectedUpdated, long theExpectedRemoved) {
|
||||
assertEquals(theExpectedAdded, theResult.created, "added results");
|
||||
assertEquals(theExpectedUpdated, theResult.updated, "updated results");
|
||||
assertEquals(theExpectedRemoved, theResult.deleted, "removed results");
|
||||
}
|
||||
|
||||
private void assertEmptyResult(ResourceChangeResult theResult) {
|
||||
assertResult(theResult, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void assertDbCalled() {
|
||||
verify(myResourceVersionSvc, times(1)).getVersionMap(anyString(), any());
|
||||
reset(myResourceVersionSvc);
|
||||
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(ourResourceVersionMap);
|
||||
}
|
||||
|
||||
private void assertDbNotCalled() {
|
||||
verify(myResourceVersionSvc, never()).getVersionMap(anyString(), any());
|
||||
reset(myResourceVersionSvc);
|
||||
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(ourResourceVersionMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetActiveUniqueSearchParams_Empty() {
|
||||
assertThat(mySearchParamRegistry.getActiveUniqueSearchParams("Patient"), Matchers.empty());
|
||||
assertThat(mySearchParamRegistry.getActiveUniqueSearchParams("Patient"), is(empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetActiveSearchParams() {
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
|
||||
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
|
||||
public void testGetActiveSearchParamsRetries() {
|
||||
AtomicBoolean retried = new AtomicBoolean(false);
|
||||
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenAnswer(t -> {
|
||||
if (myAnswerCount == 0) {
|
||||
myAnswerCount++;
|
||||
retried.set(true);
|
||||
throw new InternalErrorException("this is an error!");
|
||||
}
|
||||
|
||||
mySearchParamRegistry.doRefresh(0);
|
||||
|
||||
return 0;
|
||||
return ourResourceVersionMap;
|
||||
});
|
||||
|
||||
Map<String, RuntimeSearchParam> outcome = mySearchParamRegistry.getActiveSearchParams("Patient");
|
||||
assertFalse(retried.get());
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
Map<String, RuntimeSearchParam> activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Patient");
|
||||
assertTrue(retried.get());
|
||||
assertEquals(ourBuiltInSearchParams.getSearchParamMap("Patient").size(), activeSearchParams.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractExtensions() {
|
||||
SearchParameter searchParameter = new SearchParameter();
|
||||
searchParameter.setCode("foo");
|
||||
searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
searchParameter.setType(Enumerations.SearchParamType.TOKEN);
|
||||
searchParameter.setExpression("Patient.name");
|
||||
searchParameter.addBase("Patient");
|
||||
searchParameter.addExtension("http://foo", new StringType("FOO"));
|
||||
searchParameter.addExtension("http://bar", new StringType("BAR"));
|
||||
public void testAddActiveSearchparam() {
|
||||
// Initialize the registry
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
// Invalid entries
|
||||
searchParameter.addExtension("http://bar", null);
|
||||
searchParameter.addExtension(null, new StringType("BAR"));
|
||||
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider(searchParameter));
|
||||
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
|
||||
mySearchParamRegistry.doRefresh(0);
|
||||
return 0;
|
||||
});
|
||||
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
Map<String, RuntimeSearchParam> outcome = mySearchParamRegistry.getActiveSearchParams("Patient");
|
||||
|
@ -151,7 +310,39 @@ public class SearchParamRegistryImplTest {
|
|||
assertEquals(1, converted.getExtensions("http://foo").size());
|
||||
IPrimitiveType<?> value = (IPrimitiveType<?>) converted.getExtensions("http://foo").get(0).getValue();
|
||||
assertEquals("FOO", value.getValueAsString());
|
||||
}
|
||||
|
||||
private List<ResourceTable> resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus theStatus) {
|
||||
// Add a new search parameter entity
|
||||
List<ResourceTable> newEntities = new ArrayList(ourEntities);
|
||||
newEntities.add(createEntity(++ourLastId, 1));
|
||||
resetMock(theStatus, newEntities);
|
||||
return newEntities;
|
||||
}
|
||||
|
||||
private void resetMock(Enumerations.PublicationStatus theStatus, List<ResourceTable> theNewEntities) {
|
||||
ResourceVersionMap resourceVersionMap = ResourceVersionMap.fromResourceTableEntities(theNewEntities);
|
||||
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(resourceVersionMap);
|
||||
|
||||
// When we ask for the new entity, return our foo search parameter
|
||||
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider(buildSearchParameter(theStatus)));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private SearchParameter buildSearchParameter(Enumerations.PublicationStatus theStatus) {
|
||||
SearchParameter searchParameter = new SearchParameter();
|
||||
searchParameter.setCode("foo");
|
||||
searchParameter.setStatus(theStatus);
|
||||
searchParameter.setType(Enumerations.SearchParamType.TOKEN);
|
||||
searchParameter.setExpression("Patient.name");
|
||||
searchParameter.addBase("Patient");
|
||||
searchParameter.addExtension("http://foo", new StringType("FOO"));
|
||||
searchParameter.addExtension("http://bar", new StringType("BAR"));
|
||||
|
||||
// Invalid entries
|
||||
searchParameter.addExtension("http://bar", null);
|
||||
searchParameter.addExtension(null, new StringType("BAR"));
|
||||
return searchParameter;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ public class SubscriptionStrategyEvaluator {
|
|||
}
|
||||
|
||||
public SubscriptionMatchingStrategy determineStrategy(String theCriteria) {
|
||||
InMemoryMatchResult result = myInMemoryResourceMatcher.match(theCriteria, null, null);
|
||||
InMemoryMatchResult result = myInMemoryResourceMatcher.canBeEvaluatedInMemory(theCriteria);
|
||||
if (result.supported()) {
|
||||
return SubscriptionMatchingStrategy.IN_MEMORY;
|
||||
}
|
||||
|
|
|
@ -21,9 +21,12 @@ package ca.uhn.fhir.jpa.subscription.match.registry;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.model.sched.HapiJob;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
|
||||
|
@ -34,22 +37,28 @@ import ca.uhn.fhir.rest.param.TokenParam;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Subscription;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class SubscriptionLoader {
|
||||
public class SubscriptionLoader implements IResourceChangeListener {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionLoader.class);
|
||||
private static final int MAX_RETRIES = 60; // 60 * 5 seconds = 5 minutes
|
||||
private static long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE;
|
||||
|
||||
private final Object mySyncSubscriptionsLock = new Object();
|
||||
@Autowired
|
||||
private SubscriptionRegistry mySubscriptionRegistry;
|
||||
|
@ -62,6 +71,10 @@ public class SubscriptionLoader {
|
|||
private SubscriptionActivatingSubscriber mySubscriptionActivatingInterceptor;
|
||||
@Autowired
|
||||
private ISearchParamRegistry mySearchParamRegistry;
|
||||
@Autowired
|
||||
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
|
||||
|
||||
private SearchParameterMap mySearchParameterMap;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -70,11 +83,27 @@ public class SubscriptionLoader {
|
|||
super();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void registerListener() {
|
||||
mySearchParameterMap = getSearchParameterMap();
|
||||
IResourceChangeListenerCache subscriptionCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("Subscription", mySearchParameterMap, this, REFRESH_INTERVAL);
|
||||
subscriptionCache.forceRefresh();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void unregisterListener() {
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
|
||||
}
|
||||
|
||||
private boolean subscriptionsDaoExists() {
|
||||
return myDaoRegistry != null && myDaoRegistry.isResourceTypeSupported("Subscription");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the existing subscriptions from the database
|
||||
*/
|
||||
public void syncSubscriptions() {
|
||||
if (myDaoRegistry != null && !myDaoRegistry.isResourceTypeSupported("Subscription")) {
|
||||
if (!subscriptionsDaoExists()) {
|
||||
return;
|
||||
}
|
||||
if (!mySyncSubscriptionsSemaphore.tryAcquire()) {
|
||||
|
@ -87,16 +116,6 @@ public class SubscriptionLoader {
|
|||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void scheduleJob() {
|
||||
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
|
||||
jobDetail.setId(getClass().getName());
|
||||
jobDetail.setJobClass(Job.class);
|
||||
mySchedulerService.scheduleLocalJob(DateUtils.MILLIS_PER_MINUTE, jobDetail);
|
||||
|
||||
syncSubscriptions();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void acquireSemaphoreForUnitTest() throws InterruptedException {
|
||||
mySyncSubscriptionsSemaphore.acquire();
|
||||
|
@ -122,16 +141,8 @@ public class SubscriptionLoader {
|
|||
|
||||
synchronized (mySyncSubscriptionsLock) {
|
||||
ourLog.debug("Starting sync subscriptions");
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
|
||||
if (mySearchParamRegistry.getActiveSearchParam("Subscription", "status") != null) {
|
||||
map.add(Subscription.SP_STATUS, new TokenOrListParam()
|
||||
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
|
||||
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
|
||||
}
|
||||
map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
|
||||
|
||||
IBundleProvider subscriptionBundleList = myDaoRegistry.getSubscriptionDao().search(map);
|
||||
IBundleProvider subscriptionBundleList = getSubscriptionDao().search(mySearchParameterMap);
|
||||
|
||||
Integer subscriptionCount = subscriptionBundleList.size();
|
||||
assert subscriptionCount != null;
|
||||
|
@ -141,41 +152,68 @@ public class SubscriptionLoader {
|
|||
|
||||
List<IBaseResource> resourceList = subscriptionBundleList.getResources(0, subscriptionCount);
|
||||
|
||||
Set<String> allIds = new HashSet<>();
|
||||
int activatedCount = 0;
|
||||
int registeredCount = 0;
|
||||
return updateSubscriptionRegistry(resourceList);
|
||||
}
|
||||
}
|
||||
|
||||
for (IBaseResource resource : resourceList) {
|
||||
String nextId = resource.getIdElement().getIdPart();
|
||||
allIds.add(nextId);
|
||||
private IFhirResourceDao<?> getSubscriptionDao() {
|
||||
return myDaoRegistry.getSubscriptionDao();
|
||||
}
|
||||
|
||||
boolean activated = mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(resource);
|
||||
if (activated) {
|
||||
activatedCount++;
|
||||
}
|
||||
@Nonnull
|
||||
private SearchParameterMap getSearchParameterMap() {
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
|
||||
boolean registered = mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource);
|
||||
if (registered) {
|
||||
registeredCount++;
|
||||
}
|
||||
if (mySearchParamRegistry.getActiveSearchParam("Subscription", "status") != null) {
|
||||
map.add(Subscription.SP_STATUS, new TokenOrListParam()
|
||||
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
|
||||
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
|
||||
}
|
||||
map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
|
||||
return map;
|
||||
}
|
||||
|
||||
private int updateSubscriptionRegistry(List<IBaseResource> theResourceList) {
|
||||
Set<String> allIds = new HashSet<>();
|
||||
int activatedCount = 0;
|
||||
int registeredCount = 0;
|
||||
|
||||
for (IBaseResource resource : theResourceList) {
|
||||
String nextId = resource.getIdElement().getIdPart();
|
||||
allIds.add(nextId);
|
||||
|
||||
boolean activated = mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(resource);
|
||||
if (activated) {
|
||||
activatedCount++;
|
||||
}
|
||||
|
||||
mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds);
|
||||
ourLog.debug("Finished sync subscriptions - activated {} and registered {}", resourceList.size(), registeredCount);
|
||||
|
||||
return activatedCount;
|
||||
boolean registered = mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource);
|
||||
if (registered) {
|
||||
registeredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds);
|
||||
ourLog.debug("Finished sync subscriptions - activated {} and registered {}", theResourceList.size(), registeredCount);
|
||||
return activatedCount;
|
||||
}
|
||||
|
||||
public static class Job implements HapiJob {
|
||||
@Autowired
|
||||
private SubscriptionLoader myTarget;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext theContext) {
|
||||
myTarget.syncSubscriptions();
|
||||
@Override
|
||||
public void handleInit(Collection<IIdType> theResourceIds) {
|
||||
if (!subscriptionsDaoExists()) {
|
||||
ourLog.warn("Subsriptions are enabled on this server, but there is no Subscription DAO configured.");
|
||||
return;
|
||||
}
|
||||
IFhirResourceDao<?> subscriptionDao = getSubscriptionDao();
|
||||
List<IBaseResource> resourceList = theResourceIds.stream().map(subscriptionDao::read).collect(Collectors.toList());
|
||||
updateSubscriptionRegistry(resourceList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
|
||||
// For now ignore the contents of theResourceChangeEvent. In the future, consider updating the registry based on
|
||||
// known subscriptions that have been created, updated & deleted
|
||||
syncSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
|
|||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
|
@ -24,6 +25,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
|||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = {
|
||||
|
@ -76,6 +79,11 @@ public class DaoSubscriptionMatcherTest {
|
|||
return FhirContext.forR4();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IResourceVersionSvc resourceVersionSvc() {
|
||||
return mock(IResourceVersionSvc.class, RETURNS_DEEP_STUBS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,15 +4,16 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
|||
import ca.uhn.fhir.interceptor.executor.InterceptorService;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannelFactory;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.subscription.IChannelNamer;
|
||||
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
|
||||
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSearchParamProvider;
|
||||
import ca.uhn.fhir.jpa.subscription.module.config.TestSubscriptionConfig;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -20,32 +21,42 @@ import org.springframework.context.annotation.Configuration;
|
|||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = {
|
||||
SearchParamConfig.class,
|
||||
SubscriptionProcessorConfig.class,
|
||||
TestSubscriptionConfig.class,
|
||||
BaseSubscriptionTest.MyConfig.class
|
||||
})
|
||||
public abstract class BaseSubscriptionTest {
|
||||
|
||||
static {
|
||||
System.setProperty("unit_test_mode", "true");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
protected IInterceptorService myInterceptorRegistry;
|
||||
|
||||
@Autowired
|
||||
ISearchParamRegistry mySearchParamRegistry;
|
||||
SearchParamRegistryImpl mySearchParamRegistry;
|
||||
|
||||
@Autowired
|
||||
MockFhirClientSearchParamProvider myMockFhirClientSearchParamProvider;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
mySearchParamRegistry.handleInit(Collections.emptyList());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void afterClearAnonymousLambdas() {
|
||||
myInterceptorRegistry.unregisterAllInterceptors();
|
||||
}
|
||||
|
||||
public void initSearchParamRegistry(IBundleProvider theBundleProvider) {
|
||||
myMockFhirClientSearchParamProvider.setBundleProvider(theBundleProvider);
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
public void initSearchParamRegistry(IBaseResource theReadResource) {
|
||||
myMockFhirClientSearchParamProvider.setReadResource(theReadResource);
|
||||
mySearchParamRegistry.handleInit(Collections.singletonList(new IdDt()));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
|
|
|
@ -4,6 +4,8 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
|||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public class MockFhirClientSearchParamProvider implements ISearchParamProvider {
|
||||
|
@ -18,6 +20,8 @@ public class MockFhirClientSearchParamProvider implements ISearchParamProvider {
|
|||
|
||||
public void setBundleProvider(IBundleProvider theBundleProvider) { myMockProvider.setBundleProvider(theBundleProvider); }
|
||||
|
||||
public void setReadResource(IBaseResource theReadResource) { myMockProvider.setReadResource(theReadResource);}
|
||||
|
||||
public void setFailCount(int theFailCount) { myMockProvider.setFailCount(theFailCount); }
|
||||
|
||||
public int getFailCount() { return myMockProvider.getFailCount(); }
|
||||
|
@ -26,8 +30,7 @@ public class MockFhirClientSearchParamProvider implements ISearchParamProvider {
|
|||
public IBundleProvider search(SearchParameterMap theParams) { return myMockProvider.search(theParams); }
|
||||
|
||||
@Override
|
||||
public int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval) {
|
||||
mySearchParamRegistry.doRefresh(0);
|
||||
return 0;
|
||||
public IBaseResource read(IIdType theId) {
|
||||
return myMockProvider.read(theId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,20 @@ package ca.uhn.fhir.jpa.subscription.module.config;
|
|||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
public class MockProvider {
|
||||
private IBundleProvider myBundleProvider = new SimpleBundleProvider();
|
||||
private int myFailCount = 0;
|
||||
private IBaseResource myReadResource;
|
||||
|
||||
public void setBundleProvider(IBundleProvider theBundleProvider) {
|
||||
myBundleProvider = theBundleProvider;
|
||||
}
|
||||
public void setReadResource(IBaseResource theReadResource) {
|
||||
myReadResource = theReadResource;
|
||||
}
|
||||
|
||||
public IBundleProvider search(SearchParameterMap theParams) {
|
||||
if (myFailCount > 0) {
|
||||
|
@ -28,4 +34,7 @@ public class MockProvider {
|
|||
return myFailCount;
|
||||
}
|
||||
|
||||
public IBaseResource read(IIdType theId) {
|
||||
return myReadResource;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
package ca.uhn.fhir.jpa.subscription.module.config;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.cache.ResourceVersionMap;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.InMemorySubscriptionMatcher;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Configuration
|
||||
@TestPropertySource(properties = {
|
||||
"scheduling_disabled=true"
|
||||
|
@ -27,13 +31,19 @@ public class TestSubscriptionConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public IGenericClient fhirClient(FhirContext theFhirContext) {
|
||||
return Mockito.mock(IGenericClient.class);
|
||||
};
|
||||
public IGenericClient fhirClient() {
|
||||
return mock(IGenericClient.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InMemorySubscriptionMatcher inMemorySubscriptionMatcher() {
|
||||
return new InMemorySubscriptionMatcher();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IResourceVersionSvc resourceVersionSvc() {
|
||||
IResourceVersionSvc retval = mock(IResourceVersionSvc.class);
|
||||
when(retval.getVersionMap(any(), any())).thenReturn(ResourceVersionMap.empty());
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import ca.uhn.fhir.context.support.IValidationSupport;
|
|||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -15,15 +17,18 @@ import static org.mockito.Mockito.mock;
|
|||
@Configuration
|
||||
@Import(TestSubscriptionConfig.class)
|
||||
public class TestSubscriptionDstu3Config {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(TestSubscriptionDstu3Config.class);
|
||||
|
||||
private static final FhirContext ourFhirContext = FhirContext.forDstu3();
|
||||
|
||||
@Bean
|
||||
public FhirContext fhirContext() {
|
||||
return FhirContext.forDstu3();
|
||||
return ourFhirContext;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IValidationSupport validationSupport() {
|
||||
return FhirContext.forDstu3().getValidationSupport();
|
||||
public IValidationSupport validationSupport(FhirContext theFhirContext) {
|
||||
return theFhirContext.getValidationSupport();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package ca.uhn.fhir.jpa.subscription.module.matcher;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
|
||||
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
|
||||
import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import org.hl7.fhir.dstu3.model.BodySite;
|
||||
import org.hl7.fhir.dstu3.model.CodeableConcept;
|
||||
|
@ -37,7 +34,6 @@ import org.junit.jupiter.api.AfterEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -52,8 +48,6 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
|
|||
SearchParamMatcher mySearchParamMatcher;
|
||||
@Autowired
|
||||
ModelConfig myModelConfig;
|
||||
@Autowired
|
||||
FhirContext myFhirContext;
|
||||
|
||||
private void assertUnsupported(IBaseResource resource, String criteria) {
|
||||
assertFalse(mySearchParamMatcher.match(criteria, resource, null).supported());
|
||||
|
@ -372,8 +366,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
|
|||
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
|
||||
IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid");
|
||||
initSearchParamRegistry(bundle);
|
||||
initSearchParamRegistry(sp);
|
||||
|
||||
{
|
||||
Provenance prov = new Provenance();
|
||||
|
@ -404,8 +397,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
|
|||
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
|
||||
IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid");
|
||||
initSearchParamRegistry(bundle);
|
||||
initSearchParamRegistry(sp);
|
||||
|
||||
{
|
||||
BodySite bodySite = new BodySite();
|
||||
|
@ -496,8 +488,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
|
|||
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
|
||||
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
|
||||
|
||||
IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid");
|
||||
initSearchParamRegistry(bundle);
|
||||
initSearchParamRegistry(sp);
|
||||
|
||||
{
|
||||
ProcedureRequest pr = new ProcedureRequest();
|
||||
|
|
|
@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
|
@ -35,6 +36,7 @@ import org.springframework.transaction.PlatformTransactionManager;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -69,6 +71,8 @@ public class WebsocketConnectionValidatorTest {
|
|||
|
||||
@Autowired
|
||||
WebsocketConnectionValidator myWebsocketConnectionValidator;
|
||||
@Autowired
|
||||
IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
|
@ -141,6 +145,10 @@ public class WebsocketConnectionValidatorTest {
|
|||
return new WebsocketConnectionValidator();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
|
||||
return mock(IResourceChangeListenerRegistry.class, RETURNS_DEEP_STUBS);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
|
|||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
|
@ -50,6 +51,8 @@ public class SubscriptionSubmitInterceptorLoaderTest {
|
|||
private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader;
|
||||
@Autowired
|
||||
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
|
||||
@MockBean
|
||||
private IResourceVersionSvc myResourceVersionSvc;
|
||||
|
||||
/**
|
||||
* It should be possible to run only the {@link SubscriptionSubmitterConfig} without the
|
||||
|
|
Loading…
Reference in New Issue