Use a select count query for summary count searches in JPA server
This commit is contained in:
parent
9f7e21fec6
commit
62d6771814
|
@ -29,9 +29,9 @@ import java.util.Map;
|
|||
public enum SummaryEnum {
|
||||
|
||||
/**
|
||||
* Return only those elements marked as 'summary' in the base definition of the resource(s)
|
||||
* Search only: just return a count of the matching resources, without returning the actual matches
|
||||
*/
|
||||
TRUE("true"),
|
||||
COUNT("count"),
|
||||
|
||||
/**
|
||||
* Return only the 'text' element, and any mandatory elements
|
||||
|
@ -44,9 +44,9 @@ public enum SummaryEnum {
|
|||
DATA("data"),
|
||||
|
||||
/**
|
||||
* Search only: just return a count of the matching resources, without returning the actual matches
|
||||
* Return only those elements marked as 'summary' in the base definition of the resource(s)
|
||||
*/
|
||||
COUNT("count"),
|
||||
TRUE("true"),
|
||||
|
||||
/**
|
||||
* Return all parts of the resource(s)
|
||||
|
|
|
@ -89,6 +89,6 @@ public interface IClientExecutable<T extends IClientExecutable<?,Y>, Y> {
|
|||
/**
|
||||
* Request that the server modify the response using the <code>_summary</code> param
|
||||
*/
|
||||
T summaryMode(SummaryEnum theSummary);
|
||||
T summaryMode(SummaryEnum... theSummary);
|
||||
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import java.util.LinkedHashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
||||
import ca.uhn.fhir.util.XmlDetectionUtil;
|
||||
|
@ -204,7 +205,7 @@ public abstract class BaseClient implements IRestfulClient {
|
|||
}
|
||||
|
||||
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint,
|
||||
boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective) {
|
||||
boolean theLogRequestAndResponse, List<SummaryEnum> theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective) {
|
||||
|
||||
if (!myDontValidateConformance) {
|
||||
myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this);
|
||||
|
@ -226,7 +227,8 @@ public abstract class BaseClient implements IRestfulClient {
|
|||
}
|
||||
|
||||
if (theSummaryMode != null) {
|
||||
params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode()));
|
||||
List<String> summaryModeStrings = theSummaryMode.stream().map(SummaryEnum::getCode).collect(Collectors.toList());;
|
||||
params.put(Constants.PARAM_SUMMARY, summaryModeStrings);
|
||||
} else if (mySummary != null) {
|
||||
params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode()));
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
|
|||
}
|
||||
|
||||
private <T extends IBaseResource> T doReadOrVRead(final Class<T> theType, IIdType theId, boolean theVRead, ICallable<T> theNotModifiedHandler, String theIfVersionMatches, Boolean thePrettyPrint,
|
||||
SummaryEnum theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements) {
|
||||
List<SummaryEnum> theSummary, EncodingEnum theEncoding, Set<String> theSubsetElements) {
|
||||
String resName = toResourceName(theType);
|
||||
IIdType id = theId;
|
||||
if (!id.hasBaseUrl()) {
|
||||
|
@ -127,7 +127,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
|
|||
invocation.addHeader(Constants.HEADER_IF_NONE_MATCH, '"' + theIfVersionMatches + '"');
|
||||
}
|
||||
|
||||
boolean allowHtmlResponse = (theSummary == SummaryEnum.TEXT) || (theSummary == null && getSummary() == SummaryEnum.TEXT);
|
||||
boolean allowHtmlResponse = (theSummary != null && theSummary.contains(SummaryEnum.TEXT)) || (theSummary == null && getSummary() == SummaryEnum.TEXT);
|
||||
ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theType, (Class<? extends IBaseResource>) null, id, allowHtmlResponse);
|
||||
|
||||
if (theNotModifiedHandler == null) {
|
||||
|
@ -368,7 +368,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
|
|||
|
||||
protected EncodingEnum myParamEncoding;
|
||||
protected Boolean myPrettyPrint;
|
||||
protected SummaryEnum mySummaryMode;
|
||||
protected List<SummaryEnum> mySummaryMode;
|
||||
protected CacheControlDirective myCacheControlDirective;
|
||||
private List<Class<? extends IBaseResource>> myPreferResponseTypes;
|
||||
private boolean myQueryLogRequestAndResponse;
|
||||
|
@ -483,8 +483,15 @@ public class GenericClient extends BaseClient implements IGenericClient {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T summaryMode(SummaryEnum theSummary) {
|
||||
mySummaryMode = theSummary;
|
||||
public T summaryMode(SummaryEnum... theSummary) {
|
||||
mySummaryMode = null;
|
||||
if (theSummary != null) {
|
||||
if (theSummary.length == 1) {
|
||||
mySummaryMode = Collections.singletonList(theSummary[0]);
|
||||
} else {
|
||||
mySummaryMode = Arrays.asList(theSummary);
|
||||
}
|
||||
}
|
||||
return ((T) this);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.param.ParameterUtil;
|
||||
import ca.uhn.fhir.rest.param.QualifierDetails;
|
||||
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||
import ca.uhn.fhir.rest.server.exceptions.*;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
|
||||
|
|
|
@ -35,6 +35,8 @@ public interface ISearchBuilder {
|
|||
|
||||
Iterator<Long> createQuery(SearchParameterMap theParams, String theSearchUuid);
|
||||
|
||||
Iterator createCountQuery(SearchParameterMap theParams, String theSearchUuid);
|
||||
|
||||
void loadResourcesByPid(Collection<Long> theIncludePids, List<IBaseResource> theResourceListToPopulate, Set<Long> theRevIncludedPids, boolean theForHistoryOperation, EntityManager theEntityManager,
|
||||
FhirContext theContext, IDao theDao);
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
|
|||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
@ -84,6 +84,7 @@ import static org.apache.commons.lang3.StringUtils.*;
|
|||
* The SearchBuilder is responsible for actually forming the SQL query that handles
|
||||
* searches for resources
|
||||
*/
|
||||
@SuppressWarnings("JpaQlInspection")
|
||||
public class SearchBuilder implements ISearchBuilder {
|
||||
|
||||
private static final List<Long> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<Long>());
|
||||
|
@ -93,6 +94,8 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
private static SearchParameterMap ourLastHandlerParamsForUnitTest;
|
||||
private static String ourLastHandlerThreadForUnitTest;
|
||||
private static boolean ourTrackHandlersForUnitTest;
|
||||
protected IResourceTagDao myResourceTagDao;
|
||||
protected IResourceSearchViewDao myResourceSearchViewDao;
|
||||
private List<Long> myAlsoIncludePids;
|
||||
private CriteriaBuilder myBuilder;
|
||||
private BaseHapiFhirDao<?> myCallingDao;
|
||||
|
@ -113,17 +116,14 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
private IHapiTerminologySvc myTerminologySvc;
|
||||
private int myFetchSize;
|
||||
|
||||
protected IResourceTagDao myResourceTagDao;
|
||||
protected IResourceSearchViewDao myResourceSearchViewDao;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public SearchBuilder(FhirContext theFhirContext, EntityManager theEntityManager,
|
||||
IFulltextSearchSvc theFulltextSearchSvc, BaseHapiFhirDao<?> theDao,
|
||||
IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao,
|
||||
IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry,
|
||||
IResourceTagDao theResourceTagDao, IResourceSearchViewDao theResourceViewDao) {
|
||||
IFulltextSearchSvc theFulltextSearchSvc, BaseHapiFhirDao<?> theDao,
|
||||
IResourceIndexedSearchParamUriDao theResourceIndexedSearchParamUriDao, IForcedIdDao theForcedIdDao,
|
||||
IHapiTerminologySvc theTerminologySvc, ISearchParamRegistry theSearchParamRegistry,
|
||||
IResourceTagDao theResourceTagDao, IResourceSearchViewDao theResourceViewDao) {
|
||||
myContext = theFhirContext;
|
||||
myEntityManager = theEntityManager;
|
||||
myFulltextSearchSvc = theFulltextSearchSvc;
|
||||
|
@ -286,7 +286,7 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
String invalidMessageName = "invalidNumberPrefix";
|
||||
|
||||
Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, params, prefix, value, fromObj, invalidMessageName);
|
||||
Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric );
|
||||
Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric);
|
||||
codePredicates.add(predicateOuter);
|
||||
|
||||
} else {
|
||||
|
@ -1266,6 +1266,16 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
return predicate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Long> createCountQuery(SearchParameterMap theParams, String theSearchUuid) {
|
||||
myParams = theParams;
|
||||
myBuilder = myEntityManager.getCriteriaBuilder();
|
||||
mySearchUuid = theSearchUuid;
|
||||
|
||||
TypedQuery<Long> query = createQuery(null, null, true);
|
||||
return new CountQueryIterator(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Long> createQuery(SearchParameterMap theParams, String theSearchUuid) {
|
||||
myParams = theParams;
|
||||
|
@ -1325,7 +1335,7 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
return new QueryIterator();
|
||||
}
|
||||
|
||||
private TypedQuery<Long> createQuery(SortSpec sort, Integer theMaximumResults) {
|
||||
private TypedQuery<Long> createQuery(SortSpec sort, Integer theMaximumResults, boolean theCount) {
|
||||
CriteriaQuery<Long> outerQuery;
|
||||
/*
|
||||
* Sort
|
||||
|
@ -1334,6 +1344,7 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
* finds the appropriate resources) in an outer search which is then sorted
|
||||
*/
|
||||
if (sort != null) {
|
||||
assert !theCount;
|
||||
|
||||
outerQuery = myBuilder.createQuery(Long.class);
|
||||
Root<ResourceTable> outerQueryFrom = outerQuery.from(ResourceTable.class);
|
||||
|
@ -1365,7 +1376,11 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
outerQuery = myBuilder.createQuery(Long.class);
|
||||
myResourceTableQuery = outerQuery;
|
||||
myResourceTableRoot = myResourceTableQuery.from(ResourceTable.class);
|
||||
outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class));
|
||||
if (theCount) {
|
||||
outerQuery.multiselect(myBuilder.count(myResourceTableRoot.get("myId").as(Long.class)));
|
||||
} else {
|
||||
outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1501,37 +1516,37 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
switch (param.getParamType()) {
|
||||
case STRING:
|
||||
joinAttrName = "myParamsString";
|
||||
sortAttrName = new String[] {"myValueExact"};
|
||||
sortAttrName = new String[]{"myValueExact"};
|
||||
joinType = JoinEnum.STRING;
|
||||
break;
|
||||
case DATE:
|
||||
joinAttrName = "myParamsDate";
|
||||
sortAttrName = new String[] {"myValueLow"};
|
||||
sortAttrName = new String[]{"myValueLow"};
|
||||
joinType = JoinEnum.DATE;
|
||||
break;
|
||||
case REFERENCE:
|
||||
joinAttrName = "myResourceLinks";
|
||||
sortAttrName = new String[] {"myTargetResourcePid"};
|
||||
sortAttrName = new String[]{"myTargetResourcePid"};
|
||||
joinType = JoinEnum.REFERENCE;
|
||||
break;
|
||||
case TOKEN:
|
||||
joinAttrName = "myParamsToken";
|
||||
sortAttrName = new String[] {"mySystem", "myValue"};
|
||||
sortAttrName = new String[]{"mySystem", "myValue"};
|
||||
joinType = JoinEnum.TOKEN;
|
||||
break;
|
||||
case NUMBER:
|
||||
joinAttrName = "myParamsNumber";
|
||||
sortAttrName = new String[] {"myValue"};
|
||||
sortAttrName = new String[]{"myValue"};
|
||||
joinType = JoinEnum.NUMBER;
|
||||
break;
|
||||
case URI:
|
||||
joinAttrName = "myParamsUri";
|
||||
sortAttrName = new String[] {"myUri"};
|
||||
sortAttrName = new String[]{"myUri"};
|
||||
joinType = JoinEnum.URI;
|
||||
break;
|
||||
case QUANTITY:
|
||||
joinAttrName = "myParamsQuantity";
|
||||
sortAttrName = new String[] {"myValue"};
|
||||
sortAttrName = new String[]{"myValue"};
|
||||
joinType = JoinEnum.QUANTITY;
|
||||
break;
|
||||
case COMPOSITE:
|
||||
|
@ -1653,7 +1668,7 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
List<Long> idList = new ArrayList<Long>(theResourceSearchViewList.size());
|
||||
|
||||
//-- find all resource has tags
|
||||
for (ResourceSearchView resource: theResourceSearchViewList) {
|
||||
for (ResourceSearchView resource : theResourceSearchViewList) {
|
||||
if (resource.isHasTags())
|
||||
idList.add(resource.getId());
|
||||
}
|
||||
|
@ -2033,116 +2048,6 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
return qp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out the tolerance for a search. For example, if the user is searching for <code>4.00</code>, this method
|
||||
* returns <code>0.005</code> because we shold actually match values which are
|
||||
* <code>4 (+/-) 0.005</code> according to the FHIR specs.
|
||||
*/
|
||||
static BigDecimal calculateFuzzAmount(ParamPrefixEnum cmpValue, BigDecimal theValue) {
|
||||
if (cmpValue == ParamPrefixEnum.APPROXIMATE) {
|
||||
return theValue.multiply(new BigDecimal(0.1));
|
||||
} else {
|
||||
String plainString = theValue.toPlainString();
|
||||
int dotIdx = plainString.indexOf('.');
|
||||
if (dotIdx == -1) {
|
||||
return new BigDecimal(0.5);
|
||||
}
|
||||
|
||||
int precision = plainString.length() - (dotIdx);
|
||||
double mul = Math.pow(10, -precision);
|
||||
double val = mul * 5.0d;
|
||||
return new BigDecimal(val);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
|
||||
List<Predicate> lastUpdatedPredicates = new ArrayList<>();
|
||||
if (theLastUpdated != null) {
|
||||
if (theLastUpdated.getLowerBoundAsInstant() != null) {
|
||||
ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
|
||||
Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
|
||||
lastUpdatedPredicates.add(predicateLower);
|
||||
}
|
||||
if (theLastUpdated.getUpperBoundAsInstant() != null) {
|
||||
Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
|
||||
lastUpdatedPredicates.add(predicateUpper);
|
||||
}
|
||||
}
|
||||
return lastUpdatedPredicates;
|
||||
}
|
||||
|
||||
private static String createLeftAndRightMatchLikeExpression(String likeExpression) {
|
||||
return "%" + likeExpression.replace("%", "[%]") + "%";
|
||||
}
|
||||
|
||||
private static String createLeftMatchLikeExpression(String likeExpression) {
|
||||
return likeExpression.replace("%", "[%]") + "%";
|
||||
}
|
||||
|
||||
private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
|
||||
String theResourceType) {
|
||||
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
|
||||
RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName);
|
||||
List<String> path = param.getPathsSplit();
|
||||
|
||||
/*
|
||||
* SearchParameters can declare paths on multiple resources
|
||||
* types. Here we only want the ones that actually apply.
|
||||
*/
|
||||
path = new ArrayList<>(path);
|
||||
|
||||
for (int i = 0; i < path.size(); i++) {
|
||||
String nextPath = trim(path.get(i));
|
||||
if (!nextPath.contains(theResourceType + ".")) {
|
||||
path.remove(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return theFrom.get("mySourcePath").in(path);
|
||||
}
|
||||
|
||||
private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) {
|
||||
if (thePids.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
CriteriaBuilder builder = theEntityManager.getCriteriaBuilder();
|
||||
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
|
||||
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
||||
cq.select(from.get("myId").as(Long.class));
|
||||
|
||||
List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
|
||||
lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(thePids));
|
||||
|
||||
cq.where(SearchBuilder.toArray(lastUpdatedPredicates));
|
||||
TypedQuery<Long> query = theEntityManager.createQuery(cq);
|
||||
|
||||
List<Long> resultList = query.getResultList();
|
||||
return resultList;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static HandlerTypeEnum getLastHandlerMechanismForUnitTest() {
|
||||
return ourLastHandlerMechanismForUnitTest;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static String getLastHandlerParamsForUnitTest() {
|
||||
return ourLastHandlerParamsForUnitTest.toString() + " on thread [" + ourLastHandlerThreadForUnitTest +"]";
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void resetLastHandlerMechanismForUnitTest() {
|
||||
ourLastHandlerMechanismForUnitTest = null;
|
||||
ourLastHandlerParamsForUnitTest = null;
|
||||
ourLastHandlerThreadForUnitTest = null;
|
||||
ourTrackHandlersForUnitTest = true;
|
||||
}
|
||||
|
||||
static Predicate[] toArray(List<Predicate> thePredicates) {
|
||||
return thePredicates.toArray(new Predicate[thePredicates.size()]);
|
||||
}
|
||||
|
||||
public enum HandlerTypeEnum {
|
||||
UNIQUE_INDEX, STANDARD_QUERY
|
||||
}
|
||||
|
@ -2247,7 +2152,7 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
if (myResultsIterator == null) {
|
||||
Integer maximumResults = myCallingDao.getConfig().getFetchSizeDefaultMaximum();
|
||||
|
||||
final TypedQuery<Long> query = createQuery(mySort, maximumResults);
|
||||
final TypedQuery<Long> query = createQuery(mySort, maximumResults, false);
|
||||
|
||||
Query<Long> hibernateQuery = (Query<Long>) query;
|
||||
hibernateQuery.setFetchSize(myFetchSize);
|
||||
|
@ -2370,6 +2275,38 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
private static class CountQueryIterator implements Iterator<Long> {
|
||||
private final TypedQuery<Long> myQuery;
|
||||
private boolean myCountLoaded;
|
||||
private Long myCount;
|
||||
|
||||
public CountQueryIterator(TypedQuery<Long> theQuery) {
|
||||
myQuery = theQuery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
boolean retVal = myCount != null;
|
||||
if (!retVal) {
|
||||
if (myCountLoaded == false) {
|
||||
myCount = myQuery.getSingleResult();
|
||||
retVal = true;
|
||||
myCountLoaded = true;
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long next() {
|
||||
Validate.isTrue(hasNext());
|
||||
Validate.isTrue(myCount != null);
|
||||
Long retVal = myCount;
|
||||
myCount = null;
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
|
||||
private static class JoinKey {
|
||||
private final JoinEnum myJoinType;
|
||||
private final String myParamName;
|
||||
|
@ -2398,4 +2335,114 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out the tolerance for a search. For example, if the user is searching for <code>4.00</code>, this method
|
||||
* returns <code>0.005</code> because we shold actually match values which are
|
||||
* <code>4 (+/-) 0.005</code> according to the FHIR specs.
|
||||
*/
|
||||
static BigDecimal calculateFuzzAmount(ParamPrefixEnum cmpValue, BigDecimal theValue) {
|
||||
if (cmpValue == ParamPrefixEnum.APPROXIMATE) {
|
||||
return theValue.multiply(new BigDecimal(0.1));
|
||||
} else {
|
||||
String plainString = theValue.toPlainString();
|
||||
int dotIdx = plainString.indexOf('.');
|
||||
if (dotIdx == -1) {
|
||||
return new BigDecimal(0.5);
|
||||
}
|
||||
|
||||
int precision = plainString.length() - (dotIdx);
|
||||
double mul = Math.pow(10, -precision);
|
||||
double val = mul * 5.0d;
|
||||
return new BigDecimal(val);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
|
||||
List<Predicate> lastUpdatedPredicates = new ArrayList<>();
|
||||
if (theLastUpdated != null) {
|
||||
if (theLastUpdated.getLowerBoundAsInstant() != null) {
|
||||
ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
|
||||
Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
|
||||
lastUpdatedPredicates.add(predicateLower);
|
||||
}
|
||||
if (theLastUpdated.getUpperBoundAsInstant() != null) {
|
||||
Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
|
||||
lastUpdatedPredicates.add(predicateUpper);
|
||||
}
|
||||
}
|
||||
return lastUpdatedPredicates;
|
||||
}
|
||||
|
||||
private static String createLeftAndRightMatchLikeExpression(String likeExpression) {
|
||||
return "%" + likeExpression.replace("%", "[%]") + "%";
|
||||
}
|
||||
|
||||
private static String createLeftMatchLikeExpression(String likeExpression) {
|
||||
return likeExpression.replace("%", "[%]") + "%";
|
||||
}
|
||||
|
||||
private static Predicate createResourceLinkPathPredicate(IDao theCallingDao, FhirContext theContext, String theParamName, From<?, ? extends ResourceLink> theFrom,
|
||||
String theResourceType) {
|
||||
RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType);
|
||||
RuntimeSearchParam param = theCallingDao.getSearchParamByName(resourceDef, theParamName);
|
||||
List<String> path = param.getPathsSplit();
|
||||
|
||||
/*
|
||||
* SearchParameters can declare paths on multiple resources
|
||||
* types. Here we only want the ones that actually apply.
|
||||
*/
|
||||
path = new ArrayList<>(path);
|
||||
|
||||
for (int i = 0; i < path.size(); i++) {
|
||||
String nextPath = trim(path.get(i));
|
||||
if (!nextPath.contains(theResourceType + ".")) {
|
||||
path.remove(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return theFrom.get("mySourcePath").in(path);
|
||||
}
|
||||
|
||||
private static List<Long> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<Long> thePids) {
|
||||
if (thePids.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
CriteriaBuilder builder = theEntityManager.getCriteriaBuilder();
|
||||
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
|
||||
Root<ResourceTable> from = cq.from(ResourceTable.class);
|
||||
cq.select(from.get("myId").as(Long.class));
|
||||
|
||||
List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
|
||||
lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(thePids));
|
||||
|
||||
cq.where(SearchBuilder.toArray(lastUpdatedPredicates));
|
||||
TypedQuery<Long> query = theEntityManager.createQuery(cq);
|
||||
|
||||
List<Long> resultList = query.getResultList();
|
||||
return resultList;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static HandlerTypeEnum getLastHandlerMechanismForUnitTest() {
|
||||
return ourLastHandlerMechanismForUnitTest;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static String getLastHandlerParamsForUnitTest() {
|
||||
return ourLastHandlerParamsForUnitTest.toString() + " on thread [" + ourLastHandlerThreadForUnitTest + "]";
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void resetLastHandlerMechanismForUnitTest() {
|
||||
ourLastHandlerMechanismForUnitTest = null;
|
||||
ourLastHandlerParamsForUnitTest = null;
|
||||
ourLastHandlerThreadForUnitTest = null;
|
||||
ourTrackHandlersForUnitTest = true;
|
||||
}
|
||||
|
||||
static Predicate[] toArray(List<Predicate> thePredicates) {
|
||||
return thePredicates.toArray(new Predicate[thePredicates.size()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ca.uhn.fhir.model.api.Include;
|
|||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.SortOrderEnum;
|
||||
import ca.uhn.fhir.rest.api.SortSpec;
|
||||
import ca.uhn.fhir.rest.api.SummaryEnum;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.util.ObjectUtil;
|
||||
|
@ -30,9 +31,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
* 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.
|
||||
|
@ -53,6 +54,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
private Integer myLoadSynchronousUpTo;
|
||||
private Set<Include> myRevIncludes;
|
||||
private SortSpec mySort;
|
||||
private Set<SummaryEnum> mySummaryMode;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -68,6 +70,20 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
add(theName, theParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An unmodifiable set
|
||||
*/
|
||||
public Set<SummaryEnum> getSummaryMode() {
|
||||
if (mySummaryMode == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return Collections.unmodifiableSet(mySummaryMode);
|
||||
}
|
||||
|
||||
public void setSummaryMode(Set<SummaryEnum> theSummaryMode) {
|
||||
mySummaryMode = theSummaryMode;
|
||||
}
|
||||
|
||||
public SearchParameterMap add(String theName, DateParam theDateParam) {
|
||||
add(theName, (IQueryParameterOr<?>) theDateParam);
|
||||
return this;
|
||||
|
@ -78,7 +94,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
return;
|
||||
}
|
||||
if (!containsKey(theName)) {
|
||||
put(theName, new ArrayList<List<? extends IQueryParameterType>>());
|
||||
put(theName, new ArrayList<>());
|
||||
}
|
||||
|
||||
for (IQueryParameterOr<?> next : theAnd.getValuesAsQueryTokens()) {
|
||||
|
@ -94,7 +110,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
return;
|
||||
}
|
||||
if (!containsKey(theName)) {
|
||||
put(theName, new ArrayList<List<? extends IQueryParameterType>>());
|
||||
put(theName, new ArrayList<>());
|
||||
}
|
||||
|
||||
get(theName).add(theOr.getValuesAsQueryTokens());
|
||||
|
@ -107,9 +123,9 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
return this;
|
||||
}
|
||||
if (!containsKey(theName)) {
|
||||
put(theName, new ArrayList<List<? extends IQueryParameterType>>());
|
||||
put(theName, new ArrayList<>());
|
||||
}
|
||||
ArrayList<IQueryParameterType> list = new ArrayList<IQueryParameterType>();
|
||||
ArrayList<IQueryParameterType> list = new ArrayList<>();
|
||||
list.add(theParam);
|
||||
get(theName).add(list);
|
||||
|
||||
|
@ -134,9 +150,9 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
}
|
||||
|
||||
private void addUrlIncludeParams(StringBuilder b, String paramName, Set<Include> theList) {
|
||||
ArrayList<Include> list = new ArrayList<Include>(theList);
|
||||
ArrayList<Include> list = new ArrayList<>(theList);
|
||||
|
||||
Collections.sort(list, new IncludeComparator());
|
||||
list.sort(new IncludeComparator());
|
||||
for (Include nextInclude : list) {
|
||||
addUrlParamSeparator(b);
|
||||
b.append(paramName);
|
||||
|
@ -177,7 +193,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
|
||||
public Set<Include> getIncludes() {
|
||||
if (myIncludes == null) {
|
||||
myIncludes = new HashSet<Include>();
|
||||
myIncludes = new HashSet<>();
|
||||
}
|
||||
return myIncludes;
|
||||
}
|
||||
|
@ -202,16 +218,6 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
myLastUpdated = theLastUpdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if there is no last updated value, and removes the lastupdated
|
||||
* value from this map
|
||||
*/
|
||||
public DateRangeParam getLastUpdatedAndRemove() {
|
||||
DateRangeParam retVal = getLastUpdated();
|
||||
myLastUpdated = null;
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, tells the server to load these results synchronously, and not to load
|
||||
* more than X results
|
||||
|
@ -288,23 +294,23 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
public String toNormalizedQueryString(FhirContext theCtx) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
|
||||
ArrayList<String> keys = new ArrayList<String>(keySet());
|
||||
ArrayList<String> keys = new ArrayList<>(keySet());
|
||||
Collections.sort(keys);
|
||||
for (String nextKey : keys) {
|
||||
|
||||
List<List<? extends IQueryParameterType>> nextValuesAndsIn = get(nextKey);
|
||||
List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<List<IQueryParameterType>>();
|
||||
List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<>();
|
||||
|
||||
for (List<? extends IQueryParameterType> nextValuesAndIn : nextValuesAndsIn) {
|
||||
|
||||
List<IQueryParameterType> nextValuesOrsOut = new ArrayList<IQueryParameterType>();
|
||||
List<IQueryParameterType> nextValuesOrsOut = new ArrayList<>();
|
||||
for (IQueryParameterType nextValueOrIn : nextValuesAndIn) {
|
||||
if (nextValueOrIn.getMissing() != null || isNotBlank(nextValueOrIn.getValueAsQueryToken(theCtx))) {
|
||||
nextValuesOrsOut.add(nextValueOrIn);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(nextValuesOrsOut, new QueryParameterTypeComparator(theCtx));
|
||||
nextValuesOrsOut.sort(new QueryParameterTypeComparator(theCtx));
|
||||
|
||||
if (nextValuesOrsOut.size() > 0) {
|
||||
nextValuesAndsOut.add(nextValuesOrsOut);
|
||||
|
@ -312,7 +318,7 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
|
||||
} // for AND
|
||||
|
||||
Collections.sort(nextValuesAndsOut, new QueryParameterOrComparator(theCtx));
|
||||
nextValuesAndsOut.sort(new QueryParameterOrComparator(theCtx));
|
||||
|
||||
for (List<IQueryParameterType> nextValuesAnd : nextValuesAndsOut) {
|
||||
addUrlParamSeparator(b);
|
||||
|
@ -389,6 +395,16 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
b.append(getCount());
|
||||
}
|
||||
|
||||
// Summary
|
||||
if (getSummaryMode() != null) {
|
||||
for (SummaryEnum next : getSummaryMode()) {
|
||||
addUrlParamSeparator(b);
|
||||
b.append(Constants.PARAM_SUMMARY);
|
||||
b.append('=');
|
||||
b.append(next.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (b.length() == 0) {
|
||||
b.append('?');
|
||||
}
|
||||
|
@ -408,38 +424,6 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
return b.toString();
|
||||
}
|
||||
|
||||
static int compare(FhirContext theCtx, IQueryParameterType theO1, IQueryParameterType theO2) {
|
||||
int retVal;
|
||||
if (theO1.getMissing() == null && theO2.getMissing() == null) {
|
||||
retVal = 0;
|
||||
} else if (theO1.getMissing() == null) {
|
||||
retVal = -1;
|
||||
} else if (theO2.getMissing() == null) {
|
||||
retVal = 1;
|
||||
} else if (ObjectUtil.equals(theO1.getMissing(), theO2.getMissing())) {
|
||||
retVal = 0;
|
||||
} else {
|
||||
if (theO1.getMissing().booleanValue()) {
|
||||
retVal = 1;
|
||||
} else {
|
||||
retVal = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (retVal == 0) {
|
||||
String q1 = theO1.getQueryParameterQualifier();
|
||||
String q2 = theO2.getQueryParameterQualifier();
|
||||
retVal = StringUtils.compare(q1, q2);
|
||||
}
|
||||
|
||||
if (retVal == 0) {
|
||||
String v1 = theO1.getValueAsQueryToken(theCtx);
|
||||
String v2 = theO2.getValueAsQueryToken(theCtx);
|
||||
retVal = StringUtils.compare(v1, v2);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public enum EverythingModeEnum {
|
||||
/*
|
||||
* Don't reorder! We rely on the ordinals
|
||||
|
@ -521,4 +505,36 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
|
|||
|
||||
}
|
||||
|
||||
static int compare(FhirContext theCtx, IQueryParameterType theO1, IQueryParameterType theO2) {
|
||||
int retVal;
|
||||
if (theO1.getMissing() == null && theO2.getMissing() == null) {
|
||||
retVal = 0;
|
||||
} else if (theO1.getMissing() == null) {
|
||||
retVal = -1;
|
||||
} else if (theO2.getMissing() == null) {
|
||||
retVal = 1;
|
||||
} else if (ObjectUtil.equals(theO1.getMissing(), theO2.getMissing())) {
|
||||
retVal = 0;
|
||||
} else {
|
||||
if (theO1.getMissing()) {
|
||||
retVal = 1;
|
||||
} else {
|
||||
retVal = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (retVal == 0) {
|
||||
String q1 = theO1.getQueryParameterQualifier();
|
||||
String q2 = theO2.getQueryParameterQualifier();
|
||||
retVal = StringUtils.compare(q1, q2);
|
||||
}
|
||||
|
||||
if (retVal == 0) {
|
||||
String v1 = theO1.getValueAsQueryToken(theCtx);
|
||||
String v2 = theO2.getValueAsQueryToken(theCtx);
|
||||
retVal = StringUtils.compare(v1, v2);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.search;
|
|||
* 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.
|
||||
|
@ -29,10 +29,10 @@ import ca.uhn.fhir.jpa.dao.data.ISearchDao;
|
|||
import ca.uhn.fhir.jpa.dao.data.ISearchIncludeDao;
|
||||
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
|
||||
import ca.uhn.fhir.jpa.entity.*;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.SummaryEnum;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
|
@ -40,6 +40,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||
import ca.uhn.fhir.rest.server.method.PageMethodBinding;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
@ -253,7 +254,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
* since we're returning a static bundle with all the results
|
||||
* pre-loaded. This is ok because syncronous requests are not
|
||||
* expected to be paged
|
||||
*
|
||||
*
|
||||
* On the other hand for async queries we load includes/revincludes
|
||||
* individually for pages as we return them to clients
|
||||
*/
|
||||
|
@ -321,27 +322,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
}
|
||||
|
||||
Search search = new Search();
|
||||
search.setDeleted(false);
|
||||
search.setUuid(searchUuid);
|
||||
search.setCreated(new Date());
|
||||
search.setSearchLastReturned(new Date());
|
||||
search.setTotalCount(null);
|
||||
search.setNumFound(0);
|
||||
search.setPreferredPageSize(theParams.getCount());
|
||||
search.setSearchType(theParams.getEverythingMode() != null ? SearchTypeEnum.EVERYTHING : SearchTypeEnum.SEARCH);
|
||||
search.setLastUpdated(theParams.getLastUpdated());
|
||||
search.setResourceType(theResourceType);
|
||||
search.setStatus(SearchStatusEnum.LOADING);
|
||||
|
||||
search.setSearchQueryString(queryString);
|
||||
search.setSearchQueryStringHash(queryString.hashCode());
|
||||
|
||||
for (Include next : theParams.getIncludes()) {
|
||||
search.addInclude(new SearchInclude(search, next.getValue(), false, next.isRecurse()));
|
||||
}
|
||||
for (Include next : theParams.getRevIncludes()) {
|
||||
search.addInclude(new SearchInclude(search, next.getValue(), true, next.isRecurse()));
|
||||
}
|
||||
populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search);
|
||||
|
||||
SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, searchUuid);
|
||||
myIdToSearchTask.put(search.getUuid(), task);
|
||||
|
@ -410,46 +391,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
myManagedTxManager = theTxManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Pageable} using a start and end index
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static @Nullable Pageable toPage(final int theFromIndex, int theToIndex) {
|
||||
int pageSize = theToIndex - theFromIndex;
|
||||
if (pageSize < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int pageIndex = theFromIndex / pageSize;
|
||||
|
||||
Pageable page = new PageRequest(pageIndex, pageSize) {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public long getOffset() {
|
||||
return theFromIndex;
|
||||
}
|
||||
};
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
static void verifySearchHasntFailedOrThrowInternalErrorException(Search theSearch) {
|
||||
if (theSearch.getStatus() == SearchStatusEnum.FAILED) {
|
||||
Integer status = theSearch.getFailureCode();
|
||||
status = ObjectUtils.defaultIfNull(status, 500);
|
||||
|
||||
String message = theSearch.getFailureMessage();
|
||||
throw BaseServerResponseException.newInstance(status, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A search task is a Callable task that runs in
|
||||
* a thread pool to handle an individual search. One instance
|
||||
* is created for any requested search and runs from the
|
||||
* beginning to the end of the search.
|
||||
*
|
||||
* <p>
|
||||
* Understand:
|
||||
* This class executes in its own thread separate from the
|
||||
* web server client thread that made the request. We do that
|
||||
|
@ -587,9 +534,32 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
}
|
||||
|
||||
private void doSearch() {
|
||||
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(myResourceType).getImplementingClass();
|
||||
ISearchBuilder sb = myCallingDao.newSearchBuilder();
|
||||
sb.setType(resourceTypeClass, myResourceType);
|
||||
|
||||
boolean wantCount = myParams.getSummaryMode().contains(SummaryEnum.COUNT);
|
||||
boolean wantOnlyCount = wantCount && myParams.getSummaryMode().size() == 1;
|
||||
if (wantCount) {
|
||||
ISearchBuilder sb = newSearchBuilder();
|
||||
Iterator<Long> countIterator = sb.createCountQuery(myParams, mySearchUuid);
|
||||
Long count = countIterator.next();
|
||||
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
|
||||
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
|
||||
txTemplate.execute(new TransactionCallbackWithoutResult() {
|
||||
@Override
|
||||
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
|
||||
mySearch.setTotalCount(count.intValue());
|
||||
if (wantOnlyCount) {
|
||||
mySearch.setStatus(SearchStatusEnum.FINISHED);
|
||||
}
|
||||
doSaveSearch();
|
||||
}
|
||||
});
|
||||
if (wantOnlyCount) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ISearchBuilder sb = newSearchBuilder();
|
||||
Iterator<Long> theResultIterator = sb.createQuery(myParams, mySearchUuid);
|
||||
|
||||
while (theResultIterator.hasNext()) {
|
||||
|
@ -630,6 +600,13 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
saveUnsynced(theResultIterator);
|
||||
}
|
||||
|
||||
private ISearchBuilder newSearchBuilder() {
|
||||
Class<? extends IBaseResource> resourceTypeClass = myContext.getResourceDefinition(myResourceType).getImplementingClass();
|
||||
ISearchBuilder sb = myCallingDao.newSearchBuilder();
|
||||
sb.setType(resourceTypeClass, myResourceType);
|
||||
return sb;
|
||||
}
|
||||
|
||||
public CountDownLatch getCompletionLatch() {
|
||||
return myCompletionLatch;
|
||||
}
|
||||
|
@ -744,4 +721,63 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
|||
|
||||
}
|
||||
|
||||
public static void populateSearchEntity(SearchParameterMap theParams, String theResourceType, String theSearchUuid, String theQueryString, Search theSearch) {
|
||||
theSearch.setDeleted(false);
|
||||
theSearch.setUuid(theSearchUuid);
|
||||
theSearch.setCreated(new Date());
|
||||
theSearch.setSearchLastReturned(new Date());
|
||||
theSearch.setTotalCount(null);
|
||||
theSearch.setNumFound(0);
|
||||
theSearch.setPreferredPageSize(theParams.getCount());
|
||||
theSearch.setSearchType(theParams.getEverythingMode() != null ? SearchTypeEnum.EVERYTHING : SearchTypeEnum.SEARCH);
|
||||
theSearch.setLastUpdated(theParams.getLastUpdated());
|
||||
theSearch.setResourceType(theResourceType);
|
||||
theSearch.setStatus(SearchStatusEnum.LOADING);
|
||||
|
||||
theSearch.setSearchQueryString(theQueryString);
|
||||
theSearch.setSearchQueryStringHash(theQueryString.hashCode());
|
||||
|
||||
for (Include next : theParams.getIncludes()) {
|
||||
theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), false, next.isRecurse()));
|
||||
}
|
||||
for (Include next : theParams.getRevIncludes()) {
|
||||
theSearch.addInclude(new SearchInclude(theSearch, next.getValue(), true, next.isRecurse()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Pageable} using a start and end index
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static @Nullable
|
||||
Pageable toPage(final int theFromIndex, int theToIndex) {
|
||||
int pageSize = theToIndex - theFromIndex;
|
||||
if (pageSize < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int pageIndex = theFromIndex / pageSize;
|
||||
|
||||
Pageable page = new PageRequest(pageIndex, pageSize) {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public long getOffset() {
|
||||
return theFromIndex;
|
||||
}
|
||||
};
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
static void verifySearchHasntFailedOrThrowInternalErrorException(Search theSearch) {
|
||||
if (theSearch.getStatus() == SearchStatusEnum.FAILED) {
|
||||
Integer status = theSearch.getFailureCode();
|
||||
status = ObjectUtils.defaultIfNull(status, 500);
|
||||
|
||||
String message = theSearch.getFailureMessage();
|
||||
throw BaseServerResponseException.newInstance(status, message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import org.junit.AfterClass;
|
|||
import org.junit.Test;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@TestPropertySource(properties = {
|
||||
"scheduling_disabled=true"
|
||||
|
@ -71,7 +71,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
|
|||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Test
|
||||
public void testOneRowPerUpdate() {
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.dao.r4;
|
|||
|
||||
import ca.uhn.fhir.jpa.dao.*;
|
||||
import ca.uhn.fhir.jpa.entity.*;
|
||||
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
|
||||
|
@ -55,7 +56,7 @@ import static org.junit.Assert.*;
|
|||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@SuppressWarnings({"unchecked", "deprecation"})
|
||||
@SuppressWarnings({"unchecked", "deprecation", "Duplicates"})
|
||||
public class FhirResourceDaoR4Test extends BaseJpaR4Test {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4Test.class);
|
||||
|
@ -159,11 +160,11 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
|
|||
pt1.addName().setFamily("FAM");
|
||||
IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless();
|
||||
|
||||
runInTransaction(()->{
|
||||
runInTransaction(() -> {
|
||||
assertThat(myResourceIndexedSearchParamTokenDao.countForResourceId(id1.getIdPartAsLong()), greaterThan(0));
|
||||
});
|
||||
|
||||
runInTransaction(()->{
|
||||
runInTransaction(() -> {
|
||||
Optional<ResourceTable> tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong());
|
||||
assertTrue(tableOpt.isPresent());
|
||||
ResourceTable table = tableOpt.get();
|
||||
|
@ -174,7 +175,7 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
|
|||
mySystemDao.performReindexingPass(1000);
|
||||
mySystemDao.performReindexingPass(1000);
|
||||
|
||||
runInTransaction(()->{
|
||||
runInTransaction(() -> {
|
||||
Optional<ResourceTable> tableOpt = myResourceTableDao.findById(id1.getIdPartAsLong());
|
||||
assertTrue(tableOpt.isPresent());
|
||||
assertEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXED, tableOpt.get().getIndexStatus().longValue());
|
||||
|
@ -3796,6 +3797,27 @@ public class FhirResourceDaoR4Test extends BaseJpaR4Test {
|
|||
myStructureDefinitionDao.update(ext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDontReuseErrorSearches() {
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add("subject", new ReferenceParam("Patient/123"));
|
||||
String normalized = map.toNormalizedQueryString(myFhirCtx);
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
|
||||
runInTransaction(() -> {
|
||||
Search search = new Search();
|
||||
SearchCoordinatorSvcImpl.populateSearchEntity(map, "Encounter", uuid, normalized, search);
|
||||
search.setStatus(SearchStatusEnum.FAILED);
|
||||
search.setFailureCode(500);
|
||||
search.setFailureMessage("FOO");
|
||||
mySearchEntityDao.save(search);
|
||||
});
|
||||
|
||||
IBundleProvider results = myEncounterDao.search(map);
|
||||
assertEquals(0, results.size().intValue());
|
||||
assertNotEquals(uuid, results.getUuid());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
|
|
|
@ -259,7 +259,7 @@ public class ResourceProviderExpungeR4Test extends BaseResourceProviderR4Test {
|
|||
.execute();
|
||||
|
||||
assertEquals("count", output.getParameter().get(0).getName());
|
||||
assertEquals(2, ((IntegerType) output.getParameter().get(0).getValue()).getValue().intValue());
|
||||
assertEquals(3, ((IntegerType) output.getParameter().get(0).getValue()).getValue().intValue());
|
||||
|
||||
// Only deleted and prior patients
|
||||
assertStillThere(myOneVersionPatientId);
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package ca.uhn.fhir.jpa.provider.r4;
|
||||
|
||||
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
|
||||
import ca.uhn.fhir.rest.api.SummaryEnum;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Narrative;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.test.util.AopTestUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Test {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderSummaryModeR4Test.class);
|
||||
private SearchCoordinatorSvcImpl mySearchCoordinatorSvcRaw;
|
||||
|
||||
@Override
|
||||
@After
|
||||
public void after() throws Exception {
|
||||
super.after();
|
||||
myDaoConfig.setCountSearchResultsUpTo(null);
|
||||
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
|
||||
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void before() throws Exception {
|
||||
super.before();
|
||||
myDaoConfig.setCountSearchResultsUpTo(5);
|
||||
|
||||
mySearchCoordinatorSvcRaw = AopTestUtils.getTargetObject(mySearchCoordinatorSvc);
|
||||
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(250);
|
||||
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(5);
|
||||
|
||||
runInTransaction(() -> {
|
||||
for (int i = 0; i < 104; i++) {
|
||||
Patient p = new Patient();
|
||||
p.getText().setStatus(Narrative.NarrativeStatus.GENERATED);
|
||||
p.getText().getDiv().setValue("<div>i am a div</div>");
|
||||
p.addName().setFamily("FAM" + i);
|
||||
p.setActive(true);
|
||||
myPatientDao.create(p);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Count only - Should include count but no results
|
||||
*/
|
||||
@Test
|
||||
public void testSearchWithCount() {
|
||||
Bundle outcome = ourClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.ACTIVE.exactly().code("true"))
|
||||
.summaryMode(SummaryEnum.COUNT)
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
|
||||
assertEquals(new Integer(104), outcome.getTotalElement().getValue());
|
||||
assertEquals(0, outcome.getEntry().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Count and data - Should include both a count and the data portions of results
|
||||
*/
|
||||
@Test
|
||||
public void testSearchWithCountAndData() {
|
||||
Bundle outcome = ourClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.ACTIVE.exactly().code("true"))
|
||||
.summaryMode(SummaryEnum.COUNT, SummaryEnum.DATA)
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
|
||||
assertEquals(new Integer(104), outcome.getTotalElement().getValue());
|
||||
assertEquals(10, outcome.getEntry().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* No summary mode - Should return the first page of results but not
|
||||
* have the total available yet
|
||||
*/
|
||||
@Test
|
||||
public void testSearchWithNoSummaryMode() {
|
||||
Bundle outcome = ourClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.ACTIVE.exactly().code("true"))
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
|
||||
assertEquals(null, outcome.getTotalElement().getValue());
|
||||
assertEquals(10, outcome.getEntry().size());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClassClearContext() {
|
||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||
}
|
||||
|
||||
}
|
|
@ -78,9 +78,9 @@ public class RestfulServerUtils {
|
|||
}
|
||||
|
||||
if (summaryMode != null) {
|
||||
if (summaryMode.contains(SummaryEnum.COUNT)) {
|
||||
if (summaryMode.contains(SummaryEnum.COUNT) && summaryMode.size() == 1) {
|
||||
parser.setEncodeElements(Collections.singleton("Bundle.total"));
|
||||
} else if (summaryMode.contains(SummaryEnum.TEXT)) {
|
||||
} else if (summaryMode.contains(SummaryEnum.TEXT) && summaryMode.size() == 1) {
|
||||
parser.setEncodeElements(TEXT_ENCODE_ELEMENTS);
|
||||
} else {
|
||||
parser.setSuppressNarratives(summaryMode.contains(SummaryEnum.DATA));
|
||||
|
@ -688,7 +688,7 @@ public class RestfulServerUtils {
|
|||
responseEncoding = new ResponseEncoding(theServer.getFhirContext(), theServer.getDefaultResponseEncoding(), null);
|
||||
}
|
||||
|
||||
boolean encodingDomainResourceAsText = theSummaryMode.contains(SummaryEnum.TEXT);
|
||||
boolean encodingDomainResourceAsText = theSummaryMode.size() == 1 && theSummaryMode.contains(SummaryEnum.TEXT);
|
||||
if (encodingDomainResourceAsText) {
|
||||
/*
|
||||
* If the user requests "text" for a bundle, only suppress the non text elements in the Element.entry.resource
|
||||
|
|
|
@ -75,7 +75,7 @@ public class SummaryEnumParameter implements IParameter {
|
|||
retVal = toCollectionOrNull(SummaryEnum.fromCode(summary[0].toLowerCase()));
|
||||
}
|
||||
} else {
|
||||
retVal = new HashSet<SummaryEnum>();
|
||||
retVal = new HashSet<>();
|
||||
for (String next : summary) {
|
||||
SummaryEnum value = SummaryEnum.fromCode(next);
|
||||
if (value == null) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import ca.uhn.fhir.model.${version}.resource.*; //
|
|||
import ca.uhn.fhir.rest.annotation.*;
|
||||
import ca.uhn.fhir.rest.param.*;
|
||||
import ca.uhn.fhir.rest.api.SortSpec;
|
||||
import ca.uhn.fhir.rest.api.SummaryEnum;
|
||||
|
||||
public class ${className}ResourceProvider extends
|
||||
## We have specialized base classes for RPs that handle certain resource types. These
|
||||
|
@ -130,8 +131,11 @@ public class ${className}ResourceProvider extends
|
|||
SortSpec theSort,
|
||||
|
||||
@ca.uhn.fhir.rest.annotation.Count
|
||||
Integer theCount
|
||||
) {
|
||||
Integer theCount,
|
||||
|
||||
Set<SummaryEnum> theSummaryMode
|
||||
|
||||
) {
|
||||
startRequest(theServletRequest);
|
||||
try {
|
||||
SearchParameterMap paramMap = new SearchParameterMap();
|
||||
|
@ -151,6 +155,7 @@ public class ${className}ResourceProvider extends
|
|||
paramMap.setIncludes(theIncludes);
|
||||
paramMap.setSort(theSort);
|
||||
paramMap.setCount(theCount);
|
||||
paramMap.setSummaryMode(theSummaryMode);
|
||||
|
||||
getDao().translateRawParameters(theAdditionalRawParams, paramMap);
|
||||
|
||||
|
|
|
@ -381,6 +381,14 @@
|
|||
handled properly by the validator. Thanks to Anthony Sute
|
||||
for the Pull Request!
|
||||
</action>
|
||||
<action type="add">
|
||||
The JPA server now performs a count query instead of a more expensive data query
|
||||
when searches using
|
||||
<![CDATA[<code>_summary=count</code>]]>.
|
||||
This means that a total will always be returned in the Bundle (this isn't always
|
||||
guaranteed otherwise, since the Search Controller can result in data being returned
|
||||
before the total number of results is known).
|
||||
</action>
|
||||
</release>
|
||||
<release version="3.4.0" date="2018-05-28">
|
||||
<action type="add">
|
||||
|
|
Loading…
Reference in New Issue