Use a select count query for summary count searches in JPA server

This commit is contained in:
James Agnew 2018-09-29 21:58:14 -04:00
parent 9f7e21fec6
commit 62d6771814
17 changed files with 530 additions and 275 deletions

View File

@ -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)

View File

@ -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);
}

View File

@ -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()));
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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()]);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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);

View File

@ -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">