Correct Validation Performance Regression (#1895)

* Validation performance regression

* Optimize validation

* More cleanup

* Add changelog

* Test fixes

* Build fixes
This commit is contained in:
James Agnew 2020-06-10 05:26:21 -04:00 committed by GitHub
parent 926afd01f2
commit 0d3ad622b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 292 additions and 112 deletions

View File

@ -20,6 +20,9 @@ package ca.uhn.fhir.context.support;
* #L%
*/
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class ConceptValidationOptions {
public boolean isInferSystem() {
@ -33,4 +36,10 @@ public class ConceptValidationOptions {
private boolean myInferSystem;
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("inferSystem", myInferSystem)
.toString();
}
}

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1895
title: "HAPI FHIR 5.0.0 introduced a regressin in JPA validator performance, where a number of unnecessary database lookups
were introduced. This has been corrected."

View File

@ -114,6 +114,7 @@ import java.util.Date;
public abstract class BaseConfig {
public static final String JPA_VALIDATION_SUPPORT_CHAIN = "myJpaValidationSupportChain";
public static final String JPA_VALIDATION_SUPPORT = "myJpaValidationSupport";
public static final String TASK_EXECUTOR_NAME = "hapiJpaTaskExecutor";
public static final String GRAPHQL_PROVIDER_NAME = "myGraphQLProvider";
public static final String PERSISTED_JPA_BUNDLE_PROVIDER = "PersistedJpaBundleProvider";

View File

@ -65,16 +65,16 @@ public abstract class BaseConfigDstu3Plus extends BaseConfig {
public abstract ITermVersionAdapterSvc terminologyVersionAdapterSvc();
@Bean(name = "myDefaultProfileValidationSupport")
public IValidationSupport defaultProfileValidationSupport() {
public DefaultProfileValidationSupport defaultProfileValidationSupport() {
return new DefaultProfileValidationSupport(fhirContext());
}
@Bean(name = JPA_VALIDATION_SUPPORT_CHAIN)
public ValidationSupportChain jpaValidationSupportChain() {
public JpaValidationSupportChain jpaValidationSupportChain() {
return new JpaValidationSupportChain(fhirContext());
}
@Bean(name = "myJpaValidationSupport")
@Bean(name = JPA_VALIDATION_SUPPORT)
public IValidationSupport jpaValidationSupport() {
return new JpaPersistedResourceValidationSupport(fhirContext());
}

View File

@ -86,28 +86,30 @@ public class BaseDstu2Config extends BaseConfig {
@Bean(name = "myInstanceValidator")
@Lazy
public IInstanceValidatorModule instanceValidator() {
ValidationSupportChain validationSupportChain = validationSupportChain();
CachingValidationSupport cachingValidationSupport = new CachingValidationSupport(new HapiToHl7OrgDstu2ValidatingSupportWrapper(validationSupportChain));
public IInstanceValidatorModule instanceValidator(ValidationSupportChain theValidationSupportChain) {
CachingValidationSupport cachingValidationSupport = new CachingValidationSupport(new HapiToHl7OrgDstu2ValidatingSupportWrapper(theValidationSupportChain));
FhirInstanceValidator retVal = new FhirInstanceValidator(cachingValidationSupport);
retVal.setBestPracticeWarningLevel(IResourceValidator.BestPracticeWarningLevel.Warning);
return retVal;
}
@Bean(name = "myDefaultProfileValidationSupport")
public DefaultProfileValidationSupport defaultProfileValidationSupport() {
return new DefaultProfileValidationSupport(fhirContext());
}
@Bean(name = JPA_VALIDATION_SUPPORT_CHAIN)
public ValidationSupportChain validationSupportChain() {
DefaultProfileValidationSupport defaultProfileValidationSupport = new DefaultProfileValidationSupport(fhirContext());
public ValidationSupportChain validationSupportChain(DefaultProfileValidationSupport theDefaultProfileValidationSupport) {
InMemoryTerminologyServerValidationSupport inMemoryTerminologyServer = new InMemoryTerminologyServerValidationSupport(fhirContextDstu2());
IValidationSupport jpaValidationSupport = jpaValidationSupportDstu2();
CommonCodeSystemsTerminologyService commonCodeSystemsTermSvc = new CommonCodeSystemsTerminologyService(fhirContext());
return new ValidationSupportChain(defaultProfileValidationSupport, jpaValidationSupport, inMemoryTerminologyServer, commonCodeSystemsTermSvc);
return new ValidationSupportChain(theDefaultProfileValidationSupport, jpaValidationSupport, inMemoryTerminologyServer, commonCodeSystemsTermSvc);
}
@Primary
@Bean
@Bean(name = JPA_VALIDATION_SUPPORT)
public IValidationSupport jpaValidationSupportDstu2() {
JpaPersistedResourceValidationSupport retVal = new JpaPersistedResourceValidationSupport(fhirContextDstu2());
return retVal;
return new JpaPersistedResourceValidationSupport(fhirContextDstu2());
}
@Bean(name = "myResourceCountsCache")

View File

@ -28,6 +28,8 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.UriParam;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -43,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
import java.util.concurrent.TimeUnit;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -57,6 +60,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
private static final Logger ourLog = LoggerFactory.getLogger(JpaPersistedResourceValidationSupport.class);
private final FhirContext myFhirContext;
private final IBaseResource myNoMatch;
@Autowired
private DaoRegistry myDaoRegistry;
@ -65,6 +69,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
private Class<? extends IBaseResource> myValueSetType;
private Class<? extends IBaseResource> myQuestionnaireType;
private Class<? extends IBaseResource> myImplementationGuideType;
private Cache<String, IBaseResource> myLoadCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
/**
* Constructor
@ -73,6 +78,8 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
super();
Validate.notNull(theFhirContext);
myFhirContext = theFhirContext;
myNoMatch = myFhirContext.getResourceDefinition("Basic").newInstance();
}
@ -99,77 +106,86 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
return null;
}
IdType id = new IdType(theUri);
boolean localReference = false;
if (id.hasBaseUrl() == false && id.hasIdPart() == true) {
localReference = true;
}
String key = theClass.getSimpleName() + " " + theUri;
IBaseResource fetched = myLoadCache.get(key, t -> {
IdType id = new IdType(theUri);
boolean localReference = false;
if (id.hasBaseUrl() == false && id.hasIdPart() == true) {
localReference = true;
}
String resourceName = myFhirContext.getResourceType(theClass);
IBundleProvider search;
if ("ValueSet".equals(resourceName)) {
if (localReference) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(IAnyResource.SP_RES_ID, new StringParam(theUri));
search = myDaoRegistry.getResourceDao("ValueSet").search(params);
if (search.size() == 0) {
params = new SearchParameterMap();
String resourceName = myFhirContext.getResourceType(theClass);
IBundleProvider search;
if ("ValueSet".equals(resourceName)) {
if (localReference) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(IAnyResource.SP_RES_ID, new StringParam(theUri));
search = myDaoRegistry.getResourceDao("ValueSet").search(params);
if (search.size() == 0) {
params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(ValueSet.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("ValueSet").search(params);
}
} else {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(ValueSet.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("ValueSet").search(params);
}
} else {
} else if ("StructureDefinition".equals(resourceName)) {
// Don't allow the core FHIR definitions to be overwritten
if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
if (myFhirContext.getElementDefinition(typeName) != null) {
return myNoMatch;
}
}
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(ValueSet.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("ValueSet").search(params);
}
} else if ("StructureDefinition".equals(resourceName)) {
// Don't allow the core FHIR definitions to be overwritten
if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
if (myFhirContext.getElementDefinition(typeName) != null) {
return null;
params.add(StructureDefinition.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("StructureDefinition").search(params);
} else if ("Questionnaire".equals(resourceName)) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) {
params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart()));
} else {
params.add(Questionnaire.SP_URL, new UriParam(id.getValue()));
}
}
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(StructureDefinition.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("StructureDefinition").search(params);
} else if ("Questionnaire".equals(resourceName)) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) {
params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart()));
search = myDaoRegistry.getResourceDao("Questionnaire").search(params);
} else if ("CodeSystem".equals(resourceName)) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(CodeSystem.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao(resourceName).search(params);
} else if ("ImplementationGuide".equals(resourceName)) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(ImplementationGuide.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("ImplementationGuide").search(params);
} else {
params.add(Questionnaire.SP_URL, new UriParam(id.getValue()));
throw new IllegalArgumentException("Can't fetch resource type: " + resourceName);
}
search = myDaoRegistry.getResourceDao("Questionnaire").search(params);
} else if ("CodeSystem".equals(resourceName)) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(CodeSystem.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao(resourceName).search(params);
} else if ("ImplementationGuide".equals(resourceName)) {
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(1);
params.add(ImplementationGuide.SP_URL, new UriParam(theUri));
search = myDaoRegistry.getResourceDao("ImplementationGuide").search(params);
} else {
throw new IllegalArgumentException("Can't fetch resource type: " + resourceName);
}
Integer size = search.size();
if (size == null || size == 0) {
Integer size = search.size();
if (size == null || size == 0) {
return myNoMatch;
}
if (size > 1) {
ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri);
}
return search.getResources(0, 1).get(0);
});
if (fetched == myNoMatch) {
return null;
}
if (size > 1) {
ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri);
}
return (T) search.getResources(0, 1).get(0);
return (T) fetched;
}
@Override
@ -192,4 +208,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
}
public void clearCaches() {
myLoadCache.invalidateAll();
}
}

View File

@ -27,7 +27,19 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import javax.persistence.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ForeignKey;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
@ -127,6 +139,11 @@ public class TermCodeSystemVersion implements Serializable {
return this;
}
public TermCodeSystemVersion setId(Long theId) {
myId = theId;
return this;
}
@Override
public boolean equals(Object theO) {
if (this == theO) {

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.term;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
@ -173,8 +174,11 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
public static final int DEFAULT_FETCH_SIZE = 250;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseTermReadSvcImpl.class);
private static final ValueSetExpansionOptions DEFAULT_EXPANSION_OPTIONS = new ValueSetExpansionOptions();
private static final TermCodeSystemVersion NO_CURRENT_VERSION = new TermCodeSystemVersion().setId(-1L);
private static boolean ourLastResultsFromTranslationCache; // For testing.
private static boolean ourLastResultsFromTranslationWithReverseCache; // For testing.
private final int myFetchSize = DEFAULT_FETCH_SIZE;
private final Cache<String, TermCodeSystemVersion> myCodeSystemCurrentVersionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
@Autowired
protected DaoRegistry myDaoRegistry;
@Autowired
@ -209,7 +213,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
private DaoConfig myDaoConfig;
private Cache<TranslationQuery, List<TermConceptMapGroupElementTarget>> myTranslationCache;
private Cache<TranslationQuery, List<TermConceptMapGroupElement>> myTranslationWithReverseCache;
private final int myFetchSize = DEFAULT_FETCH_SIZE;
private TransactionTemplate myTxTemplate;
@Autowired
private PlatformTransactionManager myTransactionManager;
@ -227,12 +230,15 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
private ITermCodeSystemStorageSvc myConceptStorageSvc;
@Autowired
private ApplicationContext myApplicationContext;
@Autowired
private DefaultProfileValidationSupport myDefaultProfileValidationSupport;
private volatile IValidationSupport myJpaValidationSupport;
private volatile IValidationSupport myValidationSupport;
@Override
public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
return supportsSystem(theSystem);
TermCodeSystemVersion cs = getCurrentCodeSystemVersion(theSystem);
return cs != null;
}
private void addCodeIfNotAlreadyAdded(IValueSetConceptAccumulator theValueSetCodeAccumulator, Set<String> theAddedCodes, TermConcept theConcept, boolean theAdd, AtomicInteger theCodeCounter) {
@ -283,16 +289,10 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting
public void clearTranslationCache() {
public void clearCaches() {
myTranslationCache.invalidateAll();
}
/**
* This method is present only for unit tests, do not call from client code
*/
@VisibleForTesting()
public void clearTranslationWithReverseCache() {
myTranslationWithReverseCache.invalidateAll();
myCodeSystemCurrentVersionCache.invalidateAll();
}
public void deleteConceptMap(ResourceTable theResourceTable) {
@ -1289,15 +1289,35 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
return txTemplate.execute(t -> {
TermCodeSystemVersion csv = null;
TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(theCodeSystem);
if (cs != null && cs.getCurrentVersion() != null) {
csv = cs.getCurrentVersion();
TermCodeSystemVersion csv = getCurrentCodeSystemVersion(theCodeSystem);
if (csv == null) {
return null;
}
return myConceptDao.findByCodeSystemAndCode(csv, theCode);
});
}
@Nullable
private TermCodeSystemVersion getCurrentCodeSystemVersion(String theUri) {
TermCodeSystemVersion retVal = myCodeSystemCurrentVersionCache.get(theUri, uri -> {
TermCodeSystemVersion csv = null;
TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(uri);
if (cs != null && cs.getCurrentVersion() != null) {
csv = cs.getCurrentVersion();
}
if (csv != null) {
return csv;
} else {
return NO_CURRENT_VERSION;
}
});
if (retVal == NO_CURRENT_VERSION) {
return null;
}
return retVal;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Set<TermConcept> findCodesAbove(Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
@ -1715,12 +1735,6 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
return null;
}
@Override
public boolean supportsSystem(String theSystem) {
TermCodeSystem cs = getCodeSystem(theSystem);
return cs != null;
}
private ArrayList<VersionIndependentConcept> toVersionIndependentConcepts(String theSystem, Set<TermConcept> codes) {
ArrayList<VersionIndependentConcept> retVal = new ArrayList<>(codes.size());
for (TermConcept next : codes) {

View File

@ -94,8 +94,6 @@ public interface ITermReadSvc extends IValidationSupport {
void storeTermValueSet(ResourceTable theResourceTable, ValueSet theValueSet);
boolean supportsSystem(String theCodeSystem);
List<TermConceptMapGroupElementTarget> translate(TranslationRequest theTranslationRequest);
List<TermConceptMapGroupElement> translateWithReverse(TranslationRequest theTranslationRequest);

View File

@ -63,12 +63,12 @@ public class JpaValidationSupportChain extends ValidationSupportChain {
@PostConstruct
public void postConstruct() {
addValidationSupport((IValidationSupport) new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(new CommonCodeSystemsTerminologyService(myFhirContext));
addValidationSupport(myDefaultProfileValidationSupport);
addValidationSupport(myJpaValidationSupport);
addValidationSupport((IValidationSupport) myTerminologyService);
addValidationSupport((IValidationSupport) new SnapshotGeneratingValidationSupport(myFhirContext));
addValidationSupport((IValidationSupport) new InMemoryTerminologyServerValidationSupport(myFhirContext));
addValidationSupport(myTerminologyService);
addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext));
addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext));
}
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.IInstanceValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
@ -152,12 +153,12 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
*/
@Bean
@Lazy
public RequestValidatingInterceptor requestValidatingInterceptor() {
public RequestValidatingInterceptor requestValidatingInterceptor(IInstanceValidatorModule theInstanceValidator) {
RequestValidatingInterceptor requestValidator = new RequestValidatingInterceptor();
requestValidator.setFailOnSeverity(ResultSeverityEnum.ERROR);
requestValidator.setAddResponseHeaderOnSeverity(null);
requestValidator.setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum.INFORMATION);
requestValidator.addValidatorModule(instanceValidator());
requestValidator.addValidatorModule(theInstanceValidator);
return requestValidator;
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.dao;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
@ -9,6 +10,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
@ -30,6 +32,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.BaseTest;
import ca.uhn.fhir.test.utilities.LoggingRule;
import ca.uhn.fhir.test.utilities.ProxyUtil;
import ca.uhn.fhir.test.utilities.UnregisterScheduledProcessor;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.StopWatch;
@ -54,6 +57,8 @@ import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.PlatformTransactionManager;
@ -125,6 +130,10 @@ public abstract class BaseJpaTest extends BaseTest {
private IdHelperService myIdHelperService;
@Autowired
private MemoryCacheService myMemoryCacheService;
@Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT)
@Autowired
private IValidationSupport myJpaPersistedValidationSupport;
@After
public void afterPerformCleanup() {
@ -138,6 +147,11 @@ public abstract class BaseJpaTest extends BaseTest {
if (myMemoryCacheService != null) {
myMemoryCacheService.invalidateAllCaches();
}
if (myJpaPersistedValidationSupport != null) {
ProxyUtil.getSingletonTarget(myJpaPersistedValidationSupport, JpaPersistedResourceValidationSupport.class).clearCaches();
}
}
@After

View File

@ -353,8 +353,7 @@ public abstract class BaseJpaDstu3Test extends BaseJpaTest {
@After
public void afterClearTerminologyCaches() {
BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc);
baseHapiTerminologySvc.clearTranslationCache();
baseHapiTerminologySvc.clearTranslationWithReverseCache();
baseHapiTerminologySvc.clearCaches();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl deferredSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc);

View File

@ -1,5 +1,8 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
@ -7,6 +10,7 @@ import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.test.utilities.ProxyUtil;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.validation.IValidatorModule;
import org.apache.commons.io.IOUtils;
@ -23,6 +27,7 @@ import org.junit.AfterClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.util.AopTestUtils;
import java.io.IOException;
@ -40,6 +45,9 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
private CachingValidationSupport myValidationSupport;
@Autowired
private FhirInstanceValidator myFhirInstanceValidator;
@Autowired
@Qualifier(BaseConfig.JPA_VALIDATION_SUPPORT)
private IValidationSupport myPersistedResourceValidationSupport;
@Test
public void testValidateChangedQuestionnaire() {
@ -78,6 +86,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test {
ourLog.info("Clearing cache");
myValidationSupport.invalidateCaches();
myFhirInstanceValidator.invalidateCaches();
ProxyUtil.getSingletonTarget(myPersistedResourceValidationSupport, JpaPersistedResourceValidationSupport.class).clearCaches();
try {
myQuestionnaireResponseDao.validate(qr, null, null, null, null, null, null);

View File

@ -482,8 +482,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBui
@After
public void afterClearTerminologyCaches() {
BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc);
baseHapiTerminologySvc.clearTranslationCache();
baseHapiTerminologySvc.clearTranslationWithReverseCache();
baseHapiTerminologySvc.clearCaches();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTerminologyDeferredStorageSvc);

View File

@ -9,11 +9,16 @@ import ca.uhn.fhir.rest.param.ReferenceParam;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Narrative;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.StringType;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@ -25,7 +30,6 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.contains;
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
@ -114,6 +118,56 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
@Test
public void testValidate() {
CodeSystem cs = new CodeSystem();
cs.setUrl("http://foo/cs");
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
cs.addConcept().setCode("bar-1").setDisplay("Bar 1");
cs.addConcept().setCode("bar-2").setDisplay("Bar 2");
myCodeSystemDao.create(cs);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(cs));
Observation obs = new Observation();
// obs.getMeta().addProfile("http://example.com/fhir/StructureDefinition/vitalsigns-2");
obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED).setDivAsString("<div>Hello</div>");
obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs");
obs.setSubject(new Reference("Patient/123"));
obs.addPerformer(new Reference("Practitioner/123"));
obs.setEffective(DateTimeType.now());
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setValue(new StringType("This is the value"));
obs.getCode().addCoding().setSystem("http://foo/cs").setCode("bar-1");
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(obs));
// Validate once
myCaptureQueriesListener.clear();
myObservationDao.validate(obs, null, null, null, null, null, null);
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(8, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
myCaptureQueriesListener.logDeleteQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
// Validate again (should rely only on caches)
myCaptureQueriesListener.clear();
myObservationDao.validate(obs, null, null, null, null, null, null);
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
myCaptureQueriesListener.logUpdateQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
myCaptureQueriesListener.logInsertQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
myCaptureQueriesListener.logDeleteQueriesForCurrentThread();
assertEquals(0, myCaptureQueriesListener.getDeleteQueriesForCurrentThread().size());
}
@Test
public void testVRead() {
IIdType id = runInTransaction(() -> {
@ -488,12 +542,10 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
}
@Test
public void testTransactionWithMultipleReferences() {
Bundle input = new Bundle();
Patient patient = new Patient();
patient.setId(IdType.newRandomUuid());
patient.setActive(true);

View File

@ -136,6 +136,27 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
oo = validateAndReturnOutcome(obs);
assertEquals(encode(oo), "Unknown code 'http://terminology.hl7.org/CodeSystem/observation-category#FOO'", oo.getIssueFirstRep().getDiagnostics());
// Make sure we're caching the validations as opposed to hitting the DB every time
myCaptureQueriesListener.clear();
obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED);
obs.getCode().getCoding().clear();
obs.getCategory().clear();
obs.getCategoryFirstRep().addCoding().setSystem("http://terminology.hl7.org/CodeSystem/observation-category").setCode("vital-signs");
obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE4").setDisplay("Display 3");
oo = validateAndReturnOutcome(obs);
assertEquals(encode(oo), "No issues detected during validation", oo.getIssueFirstRep().getDiagnostics());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
myCaptureQueriesListener.clear();
obs.getText().setStatus(Narrative.NarrativeStatus.GENERATED);
obs.getCode().getCodingFirstRep().setSystem("http://loinc.org").setCode("CODE4").setDisplay("Display 3");
oo = validateAndReturnOutcome(obs);
assertEquals(encode(oo), "No issues detected during validation", oo.getIssueFirstRep().getDiagnostics());
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
}
/**

View File

@ -429,8 +429,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest {
@After
public void afterClearTerminologyCaches() {
BaseTermReadSvcImpl baseHapiTerminologySvc = AopTestUtils.getTargetObject(myTermSvc);
baseHapiTerminologySvc.clearTranslationCache();
baseHapiTerminologySvc.clearTranslationWithReverseCache();
baseHapiTerminologySvc.clearCaches();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationCache();
BaseTermReadSvcImpl.clearOurLastResultsFromTranslationWithReverseCache();
TermDeferredStorageSvcImpl deferredStorageSvc = AopTestUtils.getTargetObject(myTermDeferredStorageSvc);

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor;
import org.apache.commons.dbcp2.BasicDataSource;
@ -133,15 +134,16 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
/**
* Bean which validates incoming requests
* @param theInstanceValidator
*/
@Bean
@Lazy
public RequestValidatingInterceptor requestValidatingInterceptor() {
public RequestValidatingInterceptor requestValidatingInterceptor(IValidatorModule theInstanceValidator) {
RequestValidatingInterceptor requestValidator = new RequestValidatingInterceptor();
requestValidator.setFailOnSeverity(null);
requestValidator.setAddResponseHeaderOnSeverity(null);
requestValidator.setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum.INFORMATION);
requestValidator.addValidatorModule(instanceValidator());
requestValidator.addValidatorModule(theInstanceValidator);
requestValidator.setIgnoreValidatorExceptions(true);
return requestValidator;

View File

@ -18,7 +18,7 @@ spring:
enabled: true
hapi:
fhir:
version: dstu3
version: DSTU3
server:
path: /fhir/*
rest:

View File

@ -1,21 +1,27 @@
package org.hl7.fhir.common.hapi.validation.support;
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@SuppressWarnings("unchecked")
public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport {
@ -96,6 +102,19 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
}
@Override
public IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
BaseRuntimeChildDefinition urlChild = myCtx.getResourceDefinition(theValueSet).getChildByName("url");
Optional<String> valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream().map(t -> ((IPrimitiveType<?>) t).getValueAsString()).filter(t->isNotBlank(t)).findFirst();
if (valueSetUrl.isPresent()) {
String key = "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem, "(null)") + " " + defaultString(theCode, "(null)") + " " + defaultString(theDisplay, "(null)") + " " + valueSetUrl.get();
return loadFromCache(myValidateCodeCache, key, t-> super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet));
}
return super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
}
@Override
public void invalidateCaches() {
myLookupCodeCache.invalidateAll();

View File

@ -894,7 +894,7 @@
<dependency>
<groupId>org.hl7.fhir.testcases</groupId>
<artifactId>fhir-test-cases</artifactId>
<version>1.1.14-SNAPSHOT</version>
<version>1.1.14</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>