This commit is contained in:
Anthony Sute 2019-11-08 11:18:08 -05:00
commit 208d13ec5e
16 changed files with 392 additions and 89 deletions

View File

@ -48,4 +48,8 @@ public interface IFluentPath {
<T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> theReturnType); <T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> theReturnType);
/**
* Parses the expression and throws an exception if it can not parse correctly
*/
void parse(String theExpression) throws Exception;
} }

View File

@ -1599,6 +1599,33 @@ public enum Pointcut {
), ),
/**
* THIS IS AN EXPERIMENTAL HOOK AND MAY BE REMOVED OR CHANGED WITHOUT WARNING.
*
* Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost.
* <p>
* This hook is invoked when a search has found an individual ID.
* </p>
* Hooks may accept the following parameters:
* <ul>
* <li>
* java.lang.Integer - The query ID
* </li>
* <li>
* java.lang.Object - The ID
* </li>
* </ul>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
JPA_PERFTRACE_SEARCH_FOUND_ID(void.class,
"java.lang.Integer",
"java.lang.Object"
),
/** /**
* Note that this is a performance tracing hook. Use with caution in production * Note that this is a performance tracing hook. Use with caution in production
* systems, since calling it may (or may not) carry a cost. * systems, since calling it may (or may not) carry a cost.

View File

@ -109,6 +109,8 @@ ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPa
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1} ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1}
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1} ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1}
ca.uhn.fhir.jpa.dao.SearchBuilder.sourceParamDisabled=The _source parameter is disabled on this server ca.uhn.fhir.jpa.dao.SearchBuilder.sourceParamDisabled=The _source parameter is disabled on this server
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidCodeMissingSystem=Invalid token specified for parameter {0} - No system specified: {1}|{2}
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidCodeMissingCode=Invalid token specified for parameter {0} - No code specified: {1}|{2}
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.matchesFound=Matches found! ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.matchesFound=Matches found!
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.noMatchesFound=No matches found! ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.noMatchesFound=No matches found!

View File

@ -1857,9 +1857,11 @@ public class SearchBuilder implements ISearchBuilder {
codes.addAll(myTerminologySvc.expandValueSet(code)); codes.addAll(myTerminologySvc.expandValueSet(code));
} else if (modifier == TokenParamModifier.ABOVE) { } else if (modifier == TokenParamModifier.ABOVE) {
system = determineSystemIfMissing(theParamName, code, system); system = determineSystemIfMissing(theParamName, code, system);
validateHaveSystemAndCodeForToken(theParamName, code, system);
codes.addAll(myTerminologySvc.findCodesAbove(system, code)); codes.addAll(myTerminologySvc.findCodesAbove(system, code));
} else if (modifier == TokenParamModifier.BELOW) { } else if (modifier == TokenParamModifier.BELOW) {
system = determineSystemIfMissing(theParamName, code, system); system = determineSystemIfMissing(theParamName, code, system);
validateHaveSystemAndCodeForToken(theParamName, code, system);
codes.addAll(myTerminologySvc.findCodesBelow(system, code)); codes.addAll(myTerminologySvc.findCodesBelow(system, code));
} else { } else {
codes.add(new VersionIndependentConcept(system, code)); codes.add(new VersionIndependentConcept(system, code));
@ -1902,6 +1904,19 @@ public class SearchBuilder implements ISearchBuilder {
return retVal; return retVal;
} }
private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) {
String systemDesc = defaultIfBlank(theSystem, "(missing)");
String codeDesc = defaultIfBlank(theCode, "(missing)");
if (isBlank(theCode)) {
String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc);
throw new InvalidRequestException(msg);
}
if (isBlank(theSystem)) {
String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc);
throw new InvalidRequestException(msg);
}
}
private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<VersionIndependentConcept> theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) { private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<VersionIndependentConcept> theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) {
if (myDontUseHashesForSearch) { if (myDontUseHashesForSearch) {
final Path<String> systemExpression = theFrom.get("mySystem"); final Path<String> systemExpression = theFrom.get("mySystem");
@ -2056,6 +2071,7 @@ public class SearchBuilder implements ISearchBuilder {
outerQuery.multiselect(myBuilder.countDistinct(myResourceTableRoot)); outerQuery.multiselect(myBuilder.countDistinct(myResourceTableRoot));
} else { } else {
outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class)); outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class));
outerQuery.distinct(true);
} }
} }
@ -3125,6 +3141,8 @@ public class SearchBuilder implements ISearchBuilder {
private final SearchRuntimeDetails mySearchRuntimeDetails; private final SearchRuntimeDetails mySearchRuntimeDetails;
private final RequestDetails myRequest; private final RequestDetails myRequest;
private final boolean myHaveRawSqlHooks;
private final boolean myHavePerftraceFoundIdHook;
private boolean myFirst = true; private boolean myFirst = true;
private IncludesIterator myIncludesIterator; private IncludesIterator myIncludesIterator;
private Long myNext; private Long myNext;
@ -3143,13 +3161,16 @@ public class SearchBuilder implements ISearchBuilder {
if (myParams.getEverythingMode() != null) { if (myParams.getEverythingMode() != null) {
myStillNeedToFetchIncludes = true; myStillNeedToFetchIncludes = true;
} }
myHavePerftraceFoundIdHook =JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest);
myHaveRawSqlHooks = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
} }
private void fetchNext() { private void fetchNext() {
boolean haveRawSqlHooks = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
try { try {
if (haveRawSqlHooks) { if (myHaveRawSqlHooks) {
CurrentThreadCaptureQueriesListener.startCapturing(); CurrentThreadCaptureQueriesListener.startCapturing();
} }
@ -3190,6 +3211,13 @@ public class SearchBuilder implements ISearchBuilder {
if (myNext == null) { if (myNext == null) {
while (myResultsIterator.hasNext()) { while (myResultsIterator.hasNext()) {
Long next = myResultsIterator.next(); Long next = myResultsIterator.next();
if (myHavePerftraceFoundIdHook) {
HookParams params = new HookParams()
.add(Integer.class, System.identityHashCode(this))
.add(Object.class, next);
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params);
}
if (next != null) { if (next != null) {
if (myPidSet.add(next)) { if (myPidSet.add(next)) {
myNext = next; myNext = next;
@ -3228,7 +3256,7 @@ public class SearchBuilder implements ISearchBuilder {
mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size()); mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
} finally { } finally {
if (haveRawSqlHooks) { if (myHaveRawSqlHooks) {
SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing(); SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
HookParams params = new HookParams() HookParams params = new HookParams()
.add(RequestDetails.class, myRequest) .add(RequestDetails.class, myRequest)

View File

@ -145,10 +145,9 @@ public class FhirResourceDaoSearchParameterR4 extends BaseHapiFhirResourceDao<Se
} else { } else {
FHIRPathEngine fhirPathEngine = new FHIRPathEngine(new HapiWorkerContext(theContext, VALIDATION_SUPPORT));
try { try {
fhirPathEngine.parse(theExpression); theContext.newFluentPath().parse(theExpression);
} catch (FHIRLexer.FHIRLexerException e) { } catch (Exception e) {
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + theExpression + "\": " + e.getMessage()); throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + theExpression + "\": " + e.getMessage());
} }

View File

@ -1,31 +0,0 @@
package ca.uhn.fhir.jpa.provider;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2019 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
/**
* @deprecated Use ca.uhn.fhir.jpa.binstore.BinaryAccessProvider instead
*/
@Deprecated
public class BinaryAccessProvider extends ca.uhn.fhir.jpa.binstore.BinaryAccessProvider {
// FIXME: JA delete before 4.0.0
}

View File

@ -233,6 +233,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
ourLog.trace("Going to try to start next search"); ourLog.trace("Going to try to start next search");
Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search); Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search);
if (newSearch.isPresent()) { if (newSearch.isPresent()) {
ourLog.trace("Launching new search");
search = newSearch.get(); search = newSearch.get();
String resourceType = search.getResourceType(); String resourceType = search.getResourceType();
SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search")); SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
@ -1113,7 +1114,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
throw newResourceGoneException(getSearch().getUuid()); throw newResourceGoneException(getSearch().getUuid());
} }
ourLog.debug("Have {} previously added IDs in search: {}", previouslyAddedResourcePids.size(), getSearch().getUuid()); ourLog.trace("Have {} previously added IDs in search: {}", previouslyAddedResourcePids.size(), getSearch().getUuid());
setPreviouslyAddedResourcePids(previouslyAddedResourcePids); setPreviouslyAddedResourcePids(previouslyAddedResourcePids);
return null; return null;
}); });

View File

@ -2,10 +2,15 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
@ -34,7 +39,11 @@ import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.Observation.ObservationStatus;
import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType;
import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus; import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus;
import org.junit.*; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
@ -45,11 +54,29 @@ import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@SuppressWarnings({"unchecked", "Duplicates"}) @SuppressWarnings({"unchecked", "Duplicates"})
@ -1978,6 +2005,52 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
} }
@Test
public void testSearchOnCodesWithBelow() {
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
CodeSystem cs = new CodeSystem();
cs.setUrl("http://foo");
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
cs.addConcept().setCode("111-1")
.addConcept().setCode("111-2");
cs.addConcept().setCode("222-1")
.addConcept().setCode("222-2");
myCodeSystemDao.create(cs);
Observation obs1 = new Observation();
obs1.getCode().addCoding().setSystem("http://foo").setCode("111-1");
String id1 = myObservationDao.create(obs1).getId().toUnqualifiedVersionless().getValue();
Observation obs2 = new Observation();
obs2.getCode().addCoding().setSystem("http://foo").setCode("111-2");
String id2 = myObservationDao.create(obs2).getId().toUnqualifiedVersionless().getValue();
IBundleProvider result;
result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam("http://foo", "111-1")));
assertThat(toUnqualifiedVersionlessIds(result).toString(), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1));
result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam("http://foo", "111-1").setModifier(TokenParamModifier.BELOW)));
assertThat(toUnqualifiedVersionlessIds(result).toString(), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1, id2));
try {
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam(null, "111-1").setModifier(TokenParamModifier.BELOW)));
fail();
} catch (InvalidRequestException e) {
assertEquals("Invalid token specified for parameter code - No code specified: (missing)|111-1", e.getMessage());
}
try {
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam("111-1", null).setModifier(TokenParamModifier.BELOW)));
fail();
} catch (InvalidRequestException e) {
assertEquals("Invalid token specified for parameter code - No system specified: 111-1|(missing)", e.getMessage());
}
}
@Test @Test
public void testSearchParamChangesType() { public void testSearchParamChangesType() {
String name = "testSearchParamChangesType"; String name = "testSearchParamChangesType";

View File

@ -1,19 +1,26 @@
package ca.uhn.fhir.jpa.provider.r4; package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity; import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
import ca.uhn.fhir.rest.gclient.TokenClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent; import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
@ -30,25 +37,32 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.io.IOException; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProviderR4Test { public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProviderR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderCustomSearchParamR4Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderCustomSearchParamR4Test.class);
@Override @Override
@After @After
public void after() throws Exception { public void after() throws Exception {
super.after(); super.after();
myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden()); myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden());
myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches());
} }
@Override @Override
@ -66,7 +80,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
} }
private Map<String, CapabilityStatementRestResourceSearchParamComponent> extractSearchParams(CapabilityStatement conformance, String resType) { private Map<String, CapabilityStatementRestResourceSearchParamComponent> extractSearchParams(CapabilityStatement conformance, String resType) {
Map<String, CapabilityStatementRestResourceSearchParamComponent> map = new HashMap<String, CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent>(); Map<String, CapabilityStatementRestResourceSearchParamComponent> map = new HashMap<>();
for (CapabilityStatementRestComponent nextRest : conformance.getRest()) { for (CapabilityStatementRestComponent nextRest : conformance.getRest()) {
for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) { for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) {
if (!resType.equals(nextResource.getType())) { if (!resType.equals(nextResource.getType())) {
@ -81,7 +95,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
} }
@Test @Test
public void saveCreateSearchParamInvalidWithMissingStatus() throws IOException { public void saveCreateSearchParamInvalidWithMissingStatus() {
SearchParameter sp = new SearchParameter(); SearchParameter sp = new SearchParameter();
sp.setCode("foo"); sp.setCode("foo");
sp.setExpression("Patient.gender"); sp.setExpression("Patient.gender");
@ -431,6 +445,127 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
} }
/**
* See #1300
*/
@Test
public void testCustomParameterMatchingManyValues() {
List<String> found = new ArrayList<>();
class Interceptor {
@Hook(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID)
public void foundId(Integer theSearchId, Object theId) {
found.add(theSearchId + "/" + theId);
}
}
Interceptor interceptor = new Interceptor();
myInterceptorRegistry.registerInterceptor(interceptor);
try {
myDaoConfig.setAllowContainsSearches(true);
// Add a custom search parameter
SearchParameter fooSp = new SearchParameter();
fooSp.addBase("Questionnaire");
fooSp.setCode("item-text");
fooSp.setName("item-text");
fooSp.setType(Enumerations.SearchParamType.STRING);
fooSp.setTitle("FOO SP");
fooSp.setExpression("Questionnaire.item.text | Questionnaire.item.item.text | Questionnaire.item.item.item.text");
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
mySearchParameterDao.create(fooSp, mySrd);
mySearchParamRegistry.forceRefresh();
int textIndex = 0;
List<Long> ids = new ArrayList<>();
for (int i = 0; i < 200; i++) {
//Lots and lots of matches
Questionnaire q = new Questionnaire();
q
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++));
q
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++));
q
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++));
q
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++));
q
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++))
.addItem()
.setText("Section " + (textIndex++));
ids.add(myQuestionnaireDao.create(q).getId().getIdPartAsLong());
}
int foundCount = 0;
Bundle bundle = null;
List<Long> actualIds = new ArrayList<>();
do {
if (bundle == null) {
bundle = ourClient
.search()
.byUrl(ourServerBase + "/Questionnaire?item-text=Section")
.returnBundle(Bundle.class)
.execute();
} else {
bundle = ourClient
.loadPage()
.next(bundle)
.execute();
}
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, bundle);
resources.forEach(t -> actualIds.add(t.getIdElement().getIdPartAsLong()));
foundCount += resources.size();
} while (bundle.getLink("next") != null);
ourLog.info("Found: {}", found);
runInTransaction(() -> {
List<Search> searches = mySearchEntityDao.findAll();
assertEquals(1, searches.size());
Search search = searches.get(0);
String message = "\nWanted: " + (ids) + "\n" +
"Actual: " + (actualIds) + "\n" +
"Found : " + (found) + "\n" +
search.toString();
assertEquals(message, 200, search.getNumFound());
assertEquals(message, 200, search.getTotalCount().intValue());
assertEquals(message, SearchStatusEnum.FINISHED, search.getStatus());
});
assertEquals(200, foundCount);
} finally {
myInterceptorRegistry.unregisterInterceptor(interceptor);
}
}
@AfterClass @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -7,7 +7,13 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc; import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.PrimitiveType;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.Type;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
@ -24,8 +30,13 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.*; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test { public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test {
@ -116,9 +127,7 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test {
myTerminologyDeferredStorageSvc.saveDeferred(); myTerminologyDeferredStorageSvc.saveDeferred();
runInTransaction(() -> { await().until(() -> runInTransaction(() -> myTermConceptMapDao.count()), greaterThan(0L));
await().until(() -> myTermConceptMapDao.count(), greaterThan(0L));
});
} }
@Test @Test

View File

@ -0,0 +1,29 @@
{
"resourceType": "SearchParameter",
"id": "54805",
"meta": {
"versionId": "1",
"lastUpdated": "2019-11-04T18:33:47.918+00:00",
"source": "#pt4ERkOO6LGm5ZoA"
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Search for a questionnaire by item.text nested field - up to 3 levels.</div>"
},
"url": "https://impact-fhir.mitre.org/r4/SearchParameter/QuestionnaireItemText",
"version": "0.0.1",
"name": "QuestionnaireItemText",
"status": "active",
"date": "2019-10-25T15:38:45-04:00",
"description": "Search for a questionnaire by item.text nested field - up to 3 levels.",
"code": "item-text",
"base": [
"Questionnaire"
],
"type": "string",
"expression": "Questionnaire.item.text | Questionnaire.item.item.text | Questionnaire.item.item.item.text",
"xpathUsage": "normal",
"modifier": [
"contains"
]
}

View File

@ -49,4 +49,9 @@ public class FluentPathDstu3 implements IFluentPath {
return evaluate(theInput, thePath, theReturnType).stream().findFirst(); return evaluate(theInput, thePath, theReturnType).stream().findFirst();
} }
@Override
public void parse(String theExpression) {
myEngine.parse(theExpression);
}
} }

View File

@ -49,5 +49,10 @@ public class FluentPathR4 implements IFluentPath {
return evaluate(theInput, thePath, theReturnType).stream().findFirst(); return evaluate(theInput, thePath, theReturnType).stream().findFirst();
} }
@Override
public void parse(String theExpression) {
myEngine.parse(theExpression);
}
} }

View File

@ -49,5 +49,10 @@ public class FhirPathR5 implements IFluentPath {
return evaluate(theInput, thePath, theReturnType).stream().findFirst(); return evaluate(theInput, thePath, theReturnType).stream().findFirst();
} }
@Override
public void parse(String theExpression) {
myEngine.parse(theExpression);
}
} }

View File

@ -501,6 +501,18 @@
A NullPointerException in the XML Parser was fixed when serializing a resource containing an extension A NullPointerException in the XML Parser was fixed when serializing a resource containing an extension
on a primitive datatype that was missing a URL declaration. on a primitive datatype that was missing a URL declaration.
</action> </action>
<action type="fix">
When using the _filter search parameter in the JPA server with an untyped resource ID, the
filter could bring in search results of the wrong type. Thanks to Anthony Sute for the Pull
Request and Jens Villadsen for reporting!
</action>
<action type="fix" issue="1300">
In some cases where where a single search parameter matches the same resource many times with
different distinct values (e.g. a search by Patient:name where there are hundreds of patients having
hundreds of distinct names each) the Search Coordinator would end up in an infinite loop and never
return all of the possible results. Thanks to @imranmoezkhan for reporting, and to
Tim Shaffer for providing a reproducible test case!
</action>
</release> </release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)"> <release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix"> <action type="fix">

View File

@ -17,9 +17,9 @@
<br/> <br/>
<iframe src="https://ghbtns.com/github-btn.html?user=jamesagnew&amp;repo=hapi-fhir&amp;type=fork&amp;count=true&amp;v=2" frameborder="0" scrolling="0" width="100px" height="20px"></iframe> <iframe src="https://ghbtns.com/github-btn.html?user=jamesagnew&amp;repo=hapi-fhir&amp;type=fork&amp;count=true&amp;v=2" frameborder="0" scrolling="0" width="100px" height="20px"></iframe>
<br/> <br/>
<a href="https://travis-ci.org/jamesagnew/hapi-fhir"><img src="https://travis-ci.org/jamesagnew/hapi-fhir.svg?branch=master" alt="Build Status"/></a> <a href="https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master"><img src="https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master" alt="Build Status"/></a>
<br/> <br/>
<a href="https://coveralls.io/r/jamesagnew/hapi-fhir?branch=master"><img src="https://coveralls.io/repos/jamesagnew/hapi-fhir/badge.svg?branch=master" alt="Coverage Status"/></a> <a href="https://codecov.io/gh/jamesagnew/hapi-fhir"><img src="https://codecov.io/gh/jamesagnew/hapi-fhir/branch/master/graph/badge.svg" alt="Coverage Status"/></a>
<br/> <br/>
<a href="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg"><img src="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg" alt="Maven Central"/></a> <a href="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg"><img src="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg" alt="Maven Central"/></a>
<br/> <br/>