diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1760-support-chained-parameters-in-has.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1760-support-chained-parameters-in-has.yaml new file mode 100644 index 00000000000..8d0b57eed10 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1760-support-chained-parameters-in-has.yaml @@ -0,0 +1,8 @@ +--- +type: add +issue: 1760 +title: "Adds support for chained parameters in a _has query. For example + `GET /Patient?_has:Observation:subject:device.identifier=1234-5`. Adds a performance warning on any queries that use + an unqualified resource in a chain which ends up resolving to 2 or more candidate target types." + + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java index 2d545f14884..d0357aebbdf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java @@ -81,7 +81,6 @@ public class MatchResourceUrlService { .add(StorageProcessingMessage.class, message); JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params); } - return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index bdd70b0c53a..891296fba86 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -157,7 +157,7 @@ public class SearchBuilder implements ISearchBuilder { @Autowired private PredicateBuilderFactory myPredicateBuilderFactory; private List myAlsoIncludePids; - private CriteriaBuilder myBuilder; + private CriteriaBuilder myCriteriaBuilder; private IDao myCallingDao; private SearchParameterMap myParams; private String mySearchUuid; @@ -195,15 +195,8 @@ public class SearchBuilder implements ISearchBuilder { Dstu3DistanceHelper.setNearDistance(myResourceType, theParams); } - /* - * Check if there is a unique key associated with the set - * of parameters passed in - */ - boolean couldBeEligibleForCompositeUniqueSpProcessing = - myDaoConfig.isUniqueIndexesEnabled() && - myParams.getEverythingMode() == null && - myParams.isAllParametersHaveNoModifier(); - if (couldBeEligibleForCompositeUniqueSpProcessing) { + // Attempt to lookup via composite unique key. + if (isCompositeUniqueSpCandidate()) { attemptCompositeUniqueSpProcessing(theParams, theRequest); } @@ -213,7 +206,16 @@ public class SearchBuilder implements ISearchBuilder { List> andOrParams = nextParamEntry.getValue(); searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams, theRequest); } + } + /** + * A search is a candidate for Composite Unique SP if unique indexes are enabled, there is no EverythingMode, and the + * parameters all have no modifiers. + */ + private boolean isCompositeUniqueSpCandidate() { + return myDaoConfig.isUniqueIndexesEnabled() && + myParams.getEverythingMode() == null && + myParams.isAllParametersHaveNoModifier(); } @Override @@ -243,10 +245,10 @@ public class SearchBuilder implements ISearchBuilder { return new QueryIterator(theSearchRuntimeDetails, theRequest); } - private void init(SearchParameterMap theParams, String theTheSearchUuid) { + private void init(SearchParameterMap theParams, String theSearchUuid) { myParams = theParams; - myBuilder = myEntityManager.getCriteriaBuilder(); - mySearchUuid = theTheSearchUuid; + myCriteriaBuilder = myEntityManager.getCriteriaBuilder(); + mySearchUuid = theSearchUuid; myPredicateBuilder = new PredicateBuilder(this, myPredicateBuilderFactory); } @@ -261,50 +263,49 @@ public class SearchBuilder implements ISearchBuilder { if (sort != null) { assert !theCount; - outerQuery = myBuilder.createQuery(Long.class); + outerQuery = myCriteriaBuilder.createQuery(Long.class); myQueryRoot.push(outerQuery); if (theCount) { - outerQuery.multiselect(myBuilder.countDistinct(myQueryRoot.getRoot())); + outerQuery.multiselect(myCriteriaBuilder.countDistinct(myQueryRoot.getRoot())); } else { outerQuery.multiselect(myQueryRoot.get("myId").as(Long.class)); } List orders = Lists.newArrayList(); - createSort(myBuilder, myQueryRoot, sort, orders); + createSort(myCriteriaBuilder, myQueryRoot, sort, orders); if (orders.size() > 0) { outerQuery.orderBy(orders); } } else { - outerQuery = myBuilder.createQuery(Long.class); + outerQuery = myCriteriaBuilder.createQuery(Long.class); myQueryRoot.push(outerQuery); if (theCount) { - outerQuery.multiselect(myBuilder.countDistinct(myQueryRoot.getRoot())); + outerQuery.multiselect(myCriteriaBuilder.countDistinct(myQueryRoot.getRoot())); } else { outerQuery.multiselect(myQueryRoot.get("myId").as(Long.class)); // KHS This distinct call is causing performance issues in large installations // outerQuery.distinct(true); } - } if (myParams.getEverythingMode() != null) { Join join = myQueryRoot.join("myResourceLinks", JoinType.LEFT); if (myParams.get(IAnyResource.SP_RES_ID) != null) { - StringParam idParm = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0); - ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(myResourceName, idParm.getValue()); + StringParam idParam = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0); + ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(myResourceName, idParam.getValue()); if (myAlsoIncludePids == null) { myAlsoIncludePids = new ArrayList<>(1); } myAlsoIncludePids.add(pid); - myQueryRoot.addPredicate(myBuilder.equal(join.get("myTargetResourcePid").as(Long.class), pid.getIdAsLong())); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(join.get("myTargetResourcePid").as(Long.class), pid.getIdAsLong())); } else { - Predicate targetTypePredicate = myBuilder.equal(join.get("myTargetResourceType").as(String.class), myResourceName); - Predicate sourceTypePredicate = myBuilder.equal(myQueryRoot.get("myResourceType").as(String.class), myResourceName); - myQueryRoot.addPredicate(myBuilder.or(sourceTypePredicate, targetTypePredicate)); + Predicate targetTypePredicate = myCriteriaBuilder.equal(join.get("myTargetResourceType").as(String.class), myResourceName); + Predicate sourceTypePredicate = myCriteriaBuilder.equal(myQueryRoot.get("myResourceType").as(String.class), myResourceName); + myQueryRoot.addPredicate(myCriteriaBuilder.or(sourceTypePredicate, targetTypePredicate)); } } else { @@ -348,17 +349,17 @@ public class SearchBuilder implements ISearchBuilder { boolean haveNoIndexSearchParams = myParams.size() == 0 || myParams.keySet().stream().allMatch(t -> t.startsWith("_")); if (haveNoIndexSearchParams) { if (myParams.getEverythingMode() == null) { - myQueryRoot.addPredicate(myBuilder.equal(myQueryRoot.get("myResourceType"), myResourceName)); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myResourceType"), myResourceName)); } - myQueryRoot.addPredicate(myBuilder.isNull(myQueryRoot.get("myDeleted"))); + myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted"))); } // Last updated DateRangeParam lu = myParams.getLastUpdated(); - List lastUpdatedPredicates = createLastUpdatedPredicates(lu, myBuilder, myQueryRoot.getRoot()); + List lastUpdatedPredicates = createLastUpdatedPredicates(lu, myCriteriaBuilder, myQueryRoot.getRoot()); myQueryRoot.addPredicates(lastUpdatedPredicates); - myQueryRoot.where(myBuilder.and(myQueryRoot.getPredicateArray())); + myQueryRoot.where(myCriteriaBuilder.and(myQueryRoot.getPredicateArray())); /* * Now perform the search @@ -880,7 +881,7 @@ public class SearchBuilder implements ISearchBuilder { private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexedString) { myQueryRoot.setHasIndexJoins(true); Join join = myQueryRoot.join("myParamsCompositeStringUnique", JoinType.LEFT); - Predicate predicate = myBuilder.equal(join.get("myIndexString"), theIndexedString); + Predicate predicate = myCriteriaBuilder.equal(join.get("myIndexString"), theIndexedString); myQueryRoot.addPredicate(predicate); // Remove any empty parameters remaining after this @@ -907,7 +908,7 @@ public class SearchBuilder implements ISearchBuilder { } public CriteriaBuilder getBuilder() { - return myBuilder; + return myCriteriaBuilder; } public QueryRoot getQueryRoot() { @@ -984,7 +985,7 @@ public class SearchBuilder implements ISearchBuilder { private final SearchRuntimeDetails mySearchRuntimeDetails; private final RequestDetails myRequest; private final boolean myHaveRawSqlHooks; - private final boolean myHavePerftraceFoundIdHook; + private final boolean myHavePerfTraceFoundIdHook; private boolean myFirst = true; private IncludesIterator myIncludesIterator; private ResourcePersistentId myNext; @@ -1005,7 +1006,7 @@ public class SearchBuilder implements ISearchBuilder { myStillNeedToFetchIncludes = true; } - myHavePerftraceFoundIdHook = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest); + myHavePerfTraceFoundIdHook = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest); myHaveRawSqlHooks = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest); } @@ -1047,7 +1048,7 @@ public class SearchBuilder implements ISearchBuilder { if (myNext == null) { while (myResultsIterator.hasNext()) { Long nextLong = myResultsIterator.next(); - if (myHavePerftraceFoundIdHook) { + if (myHavePerfTraceFoundIdHook) { HookParams params = new HookParams() .add(Integer.class, System.identityHashCode(this)) .add(Object.class, nextLong); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index 5d4ffea9dd7..d11d5e4e820 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -109,10 +109,8 @@ public class IdHelperService { } /** - * Given a resource type and ID, looks up the resource and returns a {@link IResourceLookup}. This - * object contains the internal PID for the resource and the resource deletion status, making it sufficient - * for persisting resource links between resources without adding any further database calls after the - * single one performed by this call. + * Given a forced ID, convert it to it's Long value. Since you are allowed to use string IDs for resources, we need to + * convert those to the underlying Long values that are stored, for lookup and comparison purposes. * * @throws ResourceNotFoundException If the ID can not be found */ @@ -167,10 +165,10 @@ public class IdHelperService { if (myDaoConfig.getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) { theIds .stream() - .filter(t -> isValidPid(t)) - .map(t -> t.getIdPartAsLong()) - .map(t -> new ResourcePersistentId(t)) - .forEach(t -> retVal.add(t)); + .filter(IdHelperService::isValidPid) + .map(IIdType::getIdPartAsLong) + .map(ResourcePersistentId::new) + .forEach(retVal::add); } ListMultimap typeToIds = organizeIdsByResourceType(theIds); @@ -274,15 +272,10 @@ public class IdHelperService { .map(t -> t.getIdPartAsLong()) .collect(Collectors.toList()); if (!pids.isEmpty()) { - - Collection lookups = myResourceTableDao.findLookupFieldsByResourcePid(pids); - for (Object[] next : lookups) { - String resourceType = (String) next[0]; - Long resourcePid = (Long) next[1]; - Date deletedAt = (Date) next[2]; - retVal.add(new ResourceLookup(resourceType, resourcePid, deletedAt)); - } - + myResourceTableDao.findLookupFieldsByResourcePid(pids) + .stream() + .map(lookup -> new ResourceLookup((String)lookup[0], (Long)lookup[1], (Date)lookup[2])) + .forEach(retVal::add); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java index db189334bc7..e37dab1e1ac 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java @@ -52,7 +52,7 @@ abstract class BasePredicateBuilder { boolean myDontUseHashesForSearch; final IDao myCallingDao; - final CriteriaBuilder myBuilder; + final CriteriaBuilder myCriteriaBuilder; final QueryRoot myQueryRoot; final Class myResourceType; final String myResourceName; @@ -60,7 +60,7 @@ abstract class BasePredicateBuilder { BasePredicateBuilder(SearchBuilder theSearchBuilder) { myCallingDao = theSearchBuilder.getCallingDao(); - myBuilder = theSearchBuilder.getBuilder(); + myCriteriaBuilder = theSearchBuilder.getBuilder(); myQueryRoot = theSearchBuilder.getQueryRoot(); myResourceType = theSearchBuilder.getResourceType(); myResourceName = theSearchBuilder.getResourceName(); @@ -122,27 +122,27 @@ abstract class BasePredicateBuilder { Expression hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing); - myQueryRoot.addPredicate(myBuilder.equal(hashPresence, hash)); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(hashPresence, hash)); } void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join theJoin) { - myQueryRoot.addPredicate(myBuilder.equal(theJoin.get("myResourceType"), theResourceName)); - myQueryRoot.addPredicate(myBuilder.equal(theJoin.get("myParamName"), theParamName)); - myQueryRoot.addPredicate(myBuilder.equal(theJoin.get("myMissing"), theMissing)); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myResourceType"), theResourceName)); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myParamName"), theParamName)); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myMissing"), theMissing)); } Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From theFrom, Predicate thePredicate) { if (myDontUseHashesForSearch) { - Predicate resourceTypePredicate = myBuilder.equal(theFrom.get("myResourceType"), theResourceName); - Predicate paramNamePredicate = myBuilder.equal(theFrom.get("myParamName"), theParamName); - Predicate outerPredicate = myBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate); + Predicate resourceTypePredicate = myCriteriaBuilder.equal(theFrom.get("myResourceType"), theResourceName); + Predicate paramNamePredicate = myCriteriaBuilder.equal(theFrom.get("myParamName"), theParamName); + Predicate outerPredicate = myCriteriaBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate); return outerPredicate; } long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); - Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); - return myBuilder.and(hashIdentityPredicate, thePredicate); + Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); + return myCriteriaBuilder.and(hashIdentityPredicate, thePredicate); } Predicate createPredicateNumeric(String theResourceName, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java index 2cf5ac68ddc..933b7335e80 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java @@ -161,13 +161,13 @@ public class PredicateBuilderCoords extends BasePredicateBuilder implements IPre Predicate singleCode = createPredicateCoords(nextOr, theResourceName, theParamName, - myBuilder, + myCriteriaBuilder, join ); codePredicates.add(singleCode); } - Predicate retVal = myBuilder.or(toArray(codePredicates)); + Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(retVal); return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java index 37274a1d07a..466182b138f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java @@ -85,13 +85,13 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi Predicate p = createPredicateDate(params, theResourceName, theParamName, - myBuilder, + myCriteriaBuilder, join, operation); codePredicates.add(p); } - Predicate orPredicates = myBuilder.or(toArray(codePredicates)); + Predicate orPredicates = myCriteriaBuilder.or(toArray(codePredicates)); if (newJoin) { Predicate identityAndValuePredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, orPredicates); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java index d989546d3f3..21f46c0635c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java @@ -94,7 +94,7 @@ class PredicateBuilderNumber extends BasePredicateBuilder implements IPredicateB String invalidMessageName = "invalidNumberPrefix"; - Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, nextOr, prefix, value, fromObj, invalidMessageName); + Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myCriteriaBuilder, nextOr, prefix, value, fromObj, invalidMessageName); Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric); codePredicates.add(predicateOuter); @@ -104,7 +104,7 @@ class PredicateBuilderNumber extends BasePredicateBuilder implements IPredicateB } - Predicate predicate = myBuilder.or(toArray(codePredicates)); + Predicate predicate = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(predicate); return predicate; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java index 330059e1d86..e9737ee56df 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java @@ -66,13 +66,13 @@ class PredicateBuilderQuantity extends BasePredicateBuilder implements IPredicat Predicate singleCode = createPredicateQuantity(nextOr, theResourceName, theParamName, - myBuilder, + myCriteriaBuilder, join, operation); codePredicates.add(singleCode); } - Predicate retVal = myBuilder.or(toArray(codePredicates)); + Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(retVal); return retVal; } @@ -171,13 +171,13 @@ class PredicateBuilderQuantity extends BasePredicateBuilder implements IPredicat Predicate hashPredicate; if (!isBlank(systemValue) && !isBlank(unitsValue)) { long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue); - hashPredicate = myBuilder.equal(theFrom.get("myHashIdentitySystemAndUnits"), hash); + hashPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentitySystemAndUnits"), hash); } else if (!isBlank(unitsValue)) { long hash = ResourceIndexedSearchParamQuantity.calculateHashUnits(theResourceName, theParamName, unitsValue); - hashPredicate = myBuilder.equal(theFrom.get("myHashIdentityAndUnits"), hash); + hashPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentityAndUnits"), hash); } else { long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); - hashPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hash); + hashPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hash); } cmpValue = defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java index 8d78fcf5059..6306487cabf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderReference.java @@ -21,14 +21,19 @@ package ca.uhn.fhir.jpa.dao.predicate; */ import ca.uhn.fhir.context.*; +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.*; +import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.util.SourceParam; +import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster; import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -41,7 +46,9 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -53,6 +60,7 @@ import org.springframework.stereotype.Component; import javax.persistence.criteria.*; import java.util.*; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.*; @@ -69,6 +77,8 @@ class PredicateBuilderReference extends BasePredicateBuilder { MatchUrlService myMatchUrlService; @Autowired DaoRegistry myDaoRegistry; + @Autowired + private IInterceptorBroadcaster myInterceptorBroadcaster; private final PredicateBuilder myPredicateBuilder; @@ -87,6 +97,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { SearchFilterParser.CompareOperation operation, RequestDetails theRequest) { + //Is this just to ensure the chain has been split correctly??? assert theParamName.contains(".") == false; if ((operation != null) && @@ -164,7 +175,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { } else { pidPredicate = join.get("myTargetResourcePid").in(ResourcePersistentId.toLongList(targetPids)).not(); } - codePredicates.add(myBuilder.and(pathPredicate, pidPredicate)); + codePredicates.add(myCriteriaBuilder.and(pathPredicate, pidPredicate)); } // Resources by fully qualified URL @@ -182,11 +193,11 @@ class PredicateBuilderReference extends BasePredicateBuilder { } else { pidPredicate = join.get("myTargetResourceUrl").in(targetQualifiedUrls).not(); } - codePredicates.add(myBuilder.and(pathPredicate, pidPredicate)); + codePredicates.add(myCriteriaBuilder.and(pathPredicate, pidPredicate)); } if (codePredicates.size() > 0) { - Predicate predicate = myBuilder.or(toArray(codePredicates)); + Predicate predicate = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(predicate); return predicate; } else { @@ -198,9 +209,13 @@ class PredicateBuilderReference extends BasePredicateBuilder { } } - private Predicate addPredicateReferenceWithChain(String theResourceName, String theParamName, List theList, Join theJoin, List theCodePredicates, ReferenceParam theRef, RequestDetails theRequest) { + /** + * This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain + * on the device. + */ + private Predicate addPredicateReferenceWithChain(String theResourceName, String theParamName, List theList, Join theJoin, List theCodePredicates, ReferenceParam theReferenceParam, RequestDetails theRequest) { final List> resourceTypes; - if (!theRef.hasResourceType()) { + if (!theReferenceParam.hasResourceType()) { RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); resourceTypes = new ArrayList<>(); @@ -259,19 +274,19 @@ class PredicateBuilderReference extends BasePredicateBuilder { } else { try { - RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theRef.getResourceType()); + RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theReferenceParam.getResourceType()); resourceTypes = new ArrayList<>(1); resourceTypes.add(resDef.getImplementingClass()); } catch (DataFormatException e) { - throw new InvalidRequestException("Invalid resource type: " + theRef.getResourceType()); + throw new InvalidRequestException("Invalid resource type: " + theReferenceParam.getResourceType()); } } boolean foundChainMatch = false; - + List> candidateTargetTypes = new ArrayList<>(); for (Class nextType : resourceTypes) { - String chain = theRef.getChain(); + String chain = theReferenceParam.getChain(); String remainingChain = null; int chainDotIndex = chain.indexOf('.'); if (chainDotIndex != -1) { @@ -317,24 +332,46 @@ class PredicateBuilderReference extends BasePredicateBuilder { orValues.add(chainValue); } + Subquery subQ = createLinkSubquery(foundChainMatch, chain, subResourceName, orValues, theRequest); Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin); Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ); - Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate); + Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, pidPredicate); theCodePredicates.add(andPredicate); - + candidateTargetTypes.add(nextType); } if (!foundChainMatch) { - throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theRef.getChain())); + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "invalidParameterChain", theParamName + '.' + theReferenceParam.getChain())); } - Predicate predicate = myBuilder.or(toArray(theCodePredicates)); + if (candidateTargetTypes.size() > 1) { + warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes); + } + + Predicate predicate = myCriteriaBuilder.or(toArray(theCodePredicates)); myQueryRoot.addPredicate(predicate); return predicate; } + private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, List> theCandidateTargetTypes) { + String message = new StringBuilder() + .append("This search uses an unqualified resource(a parameter in a chain without a resource type). ") + .append("This is less efficient than using a qualified type. ") + .append("[" + theParamName + "] resolves to ["+ theCandidateTargetTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(",")) +"].") + .append("If you know what you're looking for, try qualifying it like this: ") + .append(theCandidateTargetTypes.stream().map(cls -> "[" +cls.getSimpleName() +":"+theParamName+"]").collect(Collectors.joining(" or "))) + .toString(); + StorageProcessingMessage msg = new StorageProcessingMessage() + .setMessage(message); + HookParams params = new HookParams() + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest) + .add(StorageProcessingMessage.class, msg); + JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params); + } + Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From from) { return createResourceLinkPathPredicate(myContext, theParamName, from, theResourceName); } @@ -398,8 +435,8 @@ class PredicateBuilderReference extends BasePredicateBuilder { andOrParams.add(theOrValues); // Create the subquery predicates - myQueryRoot.addPredicate(myBuilder.equal(myQueryRoot.get("myResourceType"), theSubResourceName)); - myQueryRoot.addPredicate(myBuilder.isNull(myQueryRoot.get("myDeleted"))); + myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myResourceType"), theSubResourceName)); + myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted"))); if (theFoundChainMatch) { searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams, theRequest); @@ -557,9 +594,9 @@ class PredicateBuilderReference extends BasePredicateBuilder { theResourceName, theRequest); if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.and) { - return myBuilder.and(xPredicate, yPredicate); + return myCriteriaBuilder.and(xPredicate, yPredicate); } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.or) { - return myBuilder.or(xPredicate, yPredicate); + return myCriteriaBuilder.or(xPredicate, yPredicate); } } else if (theFilter instanceof SearchFilterParser.FilterParameterGroup) { return processFilter(((SearchFilterParser.FilterParameterGroup) theFilter).getContained(), @@ -744,10 +781,10 @@ class PredicateBuilderReference extends BasePredicateBuilder { SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myContext)); String sourceUri = sourceParameter.getSourceUri(); String requestId = sourceParameter.getRequestId(); - Predicate sourceUriPredicate = myBuilder.equal(join.get("mySourceUri"), sourceUri); - Predicate requestIdPredicate = myBuilder.equal(join.get("myRequestId"), requestId); + Predicate sourceUriPredicate = myCriteriaBuilder.equal(join.get("mySourceUri"), sourceUri); + Predicate requestIdPredicate = myCriteriaBuilder.equal(join.get("myRequestId"), requestId); if (isNotBlank(sourceUri) && isNotBlank(requestId)) { - codePredicates.add(myBuilder.and(sourceUriPredicate, requestIdPredicate)); + codePredicates.add(myCriteriaBuilder.and(sourceUriPredicate, requestIdPredicate)); } else if (isNotBlank(sourceUri)) { codePredicates.add(sourceUriPredicate); } else if (isNotBlank(requestId)) { @@ -755,7 +792,7 @@ class PredicateBuilderReference extends BasePredicateBuilder { } } - Predicate retVal = myBuilder.or(toArray(codePredicates)); + Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(retVal); return retVal; } @@ -791,11 +828,17 @@ class PredicateBuilderReference extends BasePredicateBuilder { } assert parameterName != null; + + //Ensure that the name of the search param + // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val) + // exists on the target resource type. RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); if (owningParameterDef == null) { throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName); } + //Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in Patient?_has:Observation:subject:code=sys|val) + //exists on the target resource. owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramReference); if (owningParameterDef == null) { throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + paramReference); @@ -810,18 +853,30 @@ class PredicateBuilderReference extends BasePredicateBuilder { for (IQueryParameterOr next : parsedParam.getValuesAsQueryTokens()) { orValues.addAll(next.getValuesAsQueryTokens()); } + //Handle internal chain inside the has. + if (parameterName.contains(".")) { + String chainedPartOfParameter = getChainedPart(parameterName); + orValues.stream() + .filter(qp -> qp instanceof ReferenceParam) + .map(qp -> (ReferenceParam)qp) + .forEach(rp -> rp.setChain(getChainedPart(chainedPartOfParameter))); + } - Subquery subQ = myPredicateBuilder.createLinkSubquery(parameterName, targetResourceType, orValues, theRequest); - + Subquery subQ = myPredicateBuilder.createLinkSubquery(paramName, targetResourceType, orValues, theRequest); Join join = myQueryRoot.join("myResourceLinksAsTarget", JoinType.LEFT); + Predicate pathPredicate = myPredicateBuilder.createResourceLinkPathPredicate(targetResourceType, paramReference, join); - Predicate sourceTypePredicate = myBuilder.equal(join.get("myTargetResourceType"), theResourceType); + Predicate sourceTypePredicate = myCriteriaBuilder.equal(join.get("myTargetResourceType"), theResourceType); Predicate sourcePidPredicate = join.get("mySourceResourcePid").in(subQ); - Predicate andPredicate = myBuilder.and(pathPredicate, sourcePidPredicate, sourceTypePredicate); + Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, sourcePidPredicate, sourceTypePredicate); myQueryRoot.addPredicate(andPredicate); } } + private String getChainedPart(String parameter) { + return parameter.substring(parameter.indexOf(".") + 1); + } + private void addPredicateComposite(String theResourceName, RuntimeSearchParam theParamDef, List theNextAnd) { // TODO: fail if missing is set for a composite query @@ -846,24 +901,24 @@ class PredicateBuilderReference extends BasePredicateBuilder { switch (theParam.getParamType()) { case STRING: { From stringJoin = theRoot.join("myParamsString", JoinType.INNER); - retVal = myPredicateBuilder.createPredicateString(leftValue, theResourceName, theParam.getName(), myBuilder, stringJoin); + retVal = myPredicateBuilder.createPredicateString(leftValue, theResourceName, theParam.getName(), myCriteriaBuilder, stringJoin); break; } case TOKEN: { From tokenJoin = theRoot.join("myParamsToken", JoinType.INNER); List tokens = Collections.singletonList(leftValue); - Collection tokenPredicates = myPredicateBuilder.createPredicateToken(tokens, theResourceName, theParam.getName(), myBuilder, tokenJoin); - retVal = myBuilder.and(tokenPredicates.toArray(new Predicate[0])); + Collection tokenPredicates = myPredicateBuilder.createPredicateToken(tokens, theResourceName, theParam.getName(), myCriteriaBuilder, tokenJoin); + retVal = myCriteriaBuilder.and(tokenPredicates.toArray(new Predicate[0])); break; } case DATE: { From dateJoin = theRoot.join("myParamsDate", JoinType.INNER); - retVal = myPredicateBuilder.createPredicateDate(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); + retVal = myPredicateBuilder.createPredicateDate(leftValue, theResourceName, theParam.getName(), myCriteriaBuilder, dateJoin); break; } case QUANTITY: { From dateJoin = theRoot.join("myParamsQuantity", JoinType.INNER); - retVal = myPredicateBuilder.createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); + retVal = myPredicateBuilder.createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myCriteriaBuilder, dateJoin); break; } case COMPOSITE: diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java index 80c0ff920ac..137b86cfd18 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderResourceId.java @@ -110,7 +110,7 @@ class PredicateBuilderResourceId extends BasePredicateBuilder { if (allOrPids != null && allOrPids.isEmpty()) { // This will never match - nextPredicate = myBuilder.equal(theRoot.get("myId").as(Long.class), -1); + nextPredicate = myCriteriaBuilder.equal(theRoot.get("myId").as(Long.class), -1); } else if (allOrPids != null) { @@ -121,11 +121,11 @@ class PredicateBuilderResourceId extends BasePredicateBuilder { default: case eq: codePredicates.add(theRoot.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(allOrPids))); - nextPredicate = myBuilder.and(toArray(codePredicates)); + nextPredicate = myCriteriaBuilder.and(toArray(codePredicates)); break; case ne: codePredicates.add(theRoot.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(allOrPids)).not()); - nextPredicate = myBuilder.and(toArray(codePredicates)); + nextPredicate = myCriteriaBuilder.and(toArray(codePredicates)); break; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java index 3f770fbdfef..221db21ae11 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java @@ -71,13 +71,13 @@ class PredicateBuilderString extends BasePredicateBuilder implements IPredicateB Predicate singleCode = createPredicateString(theParameter, theResourceName, theParamName, - myBuilder, + myCriteriaBuilder, join, operation); codePredicates.add(singleCode); } - Predicate retVal = myBuilder.or(toArray(codePredicates)); + Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(retVal); return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java index a52237682f6..f5f4e0a4700 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java @@ -134,8 +134,8 @@ class PredicateBuilderTag extends BasePredicateBuilder { subQ.select(subQfrom.get("myResourceId").as(Long.class)); myQueryRoot.addPredicate( - myBuilder.not( - myBuilder.in( + myCriteriaBuilder.not( + myCriteriaBuilder.in( myQueryRoot.get("myId") ).value(subQ) ) @@ -147,7 +147,7 @@ class PredicateBuilderTag extends BasePredicateBuilder { subQ.where(subQfrom.get("myTagId").as(Long.class).in(defJoin)); - Predicate tagListPredicate = createPredicateTagList(defJoinFrom, myBuilder, tagType, tokens); + Predicate tagListPredicate = createPredicateTagList(defJoinFrom, myCriteriaBuilder, tagType, tokens); defJoin.where(tagListPredicate); continue; @@ -156,7 +156,7 @@ class PredicateBuilderTag extends BasePredicateBuilder { Join tagJoin = myQueryRoot.join("myTags", JoinType.LEFT); From defJoin = tagJoin.join("myTag"); - Predicate tagListPredicate = createPredicateTagList(defJoin, myBuilder, tagType, tokens); + Predicate tagListPredicate = createPredicateTagList(defJoin, myCriteriaBuilder, tagType, tokens); myQueryRoot.addPredicate(tagListPredicate); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java index 0b3fcf6be24..5ba6374c4ae 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java @@ -98,11 +98,11 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu } Join join = createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); - Collection singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join, operation); + Collection singleCode = createPredicateToken(tokens, theResourceName, theParamName, myCriteriaBuilder, join, operation); assert singleCode != null; codePredicates.addAll(singleCode); - Predicate spPredicate = myBuilder.or(toArray(codePredicates)); + Predicate spPredicate = myCriteriaBuilder.or(toArray(codePredicates)); myQueryRoot.addPredicate(spPredicate); return spPredicate; } @@ -345,8 +345,8 @@ class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBu private Expression toEqualOrIsNullPredicate(Path theExpression, T theCode) { if (theCode == null) { - return myBuilder.isNull(theExpression); + return myCriteriaBuilder.isNull(theExpression); } - return myBuilder.equal(theExpression, theCode); + return myCriteriaBuilder.equal(theExpression, theCode); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java index 1011759e1bb..cb94fa7fe2e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java @@ -109,37 +109,37 @@ class PredicateBuilderUri extends BasePredicateBuilder implements IPredicateBuil } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { - Predicate uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); + Predicate uriPredicate = myCriteriaBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate); codePredicates.add(hashAndUriPredicate); } else { if (myDontUseHashesForSearch) { - Predicate predicate = myBuilder.equal(join.get("myUri").as(String.class), value); + Predicate predicate = myCriteriaBuilder.equal(join.get("myUri").as(String.class), value); codePredicates.add(predicate); } else { Predicate uriPredicate = null; if (operation == null || operation == SearchFilterParser.CompareOperation.eq) { long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value); - Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri); + Predicate hashPredicate = myCriteriaBuilder.equal(join.get("myHashUri"), hashUri); codePredicates.add(hashPredicate); } else if (operation == SearchFilterParser.CompareOperation.ne) { - uriPredicate = myBuilder.notEqual(join.get("myUri").as(String.class), value); + uriPredicate = myCriteriaBuilder.notEqual(join.get("myUri").as(String.class), value); } else if (operation == SearchFilterParser.CompareOperation.co) { - uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createLeftAndRightMatchLikeExpression(value)); + uriPredicate = myCriteriaBuilder.like(join.get("myUri").as(String.class), createLeftAndRightMatchLikeExpression(value)); } else if (operation == SearchFilterParser.CompareOperation.gt) { - uriPredicate = myBuilder.greaterThan(join.get("myUri").as(String.class), value); + uriPredicate = myCriteriaBuilder.greaterThan(join.get("myUri").as(String.class), value); } else if (operation == SearchFilterParser.CompareOperation.lt) { - uriPredicate = myBuilder.lessThan(join.get("myUri").as(String.class), value); + uriPredicate = myCriteriaBuilder.lessThan(join.get("myUri").as(String.class), value); } else if (operation == SearchFilterParser.CompareOperation.ge) { - uriPredicate = myBuilder.greaterThanOrEqualTo(join.get("myUri").as(String.class), value); + uriPredicate = myCriteriaBuilder.greaterThanOrEqualTo(join.get("myUri").as(String.class), value); } else if (operation == SearchFilterParser.CompareOperation.le) { - uriPredicate = myBuilder.lessThanOrEqualTo(join.get("myUri").as(String.class), value); + uriPredicate = myCriteriaBuilder.lessThanOrEqualTo(join.get("myUri").as(String.class), value); } else if (operation == SearchFilterParser.CompareOperation.sw) { - uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); + uriPredicate = myCriteriaBuilder.like(join.get("myUri").as(String.class), createLeftMatchLikeExpression(value)); } else if (operation == SearchFilterParser.CompareOperation.ew) { - uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createRightMatchLikeExpression(value)); + uriPredicate = myCriteriaBuilder.like(join.get("myUri").as(String.class), createRightMatchLikeExpression(value)); } else { throw new IllegalArgumentException(String.format("Unsupported operator specified in _filter clause, %s", operation.toString())); @@ -147,8 +147,8 @@ class PredicateBuilderUri extends BasePredicateBuilder implements IPredicateBuil if (uriPredicate != null) { long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); - Predicate hashIdentityPredicate = myBuilder.equal(join.get("myHashIdentity"), hashIdentity); - codePredicates.add(myBuilder.and(hashIdentityPredicate, uriPredicate)); + Predicate hashIdentityPredicate = myCriteriaBuilder.equal(join.get("myHashIdentity"), hashIdentity); + codePredicates.add(myCriteriaBuilder.and(hashIdentityPredicate, uriPredicate)); } } } @@ -164,12 +164,12 @@ class PredicateBuilderUri extends BasePredicateBuilder implements IPredicateBuil * just add a predicate that can never match */ if (codePredicates.isEmpty()) { - Predicate predicate = myBuilder.isNull(join.get("myMissing").as(String.class)); + Predicate predicate = myCriteriaBuilder.isNull(join.get("myMissing").as(String.class)); myQueryRoot.addPredicate(predicate); return null; } - Predicate orPredicate = myBuilder.or(toArray(codePredicates)); + Predicate orPredicate = myCriteriaBuilder.or(toArray(codePredicates)); Predicate outerPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java index c9916d62c22..43f2d598f17 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchCustomSearchParamTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.DaoMethodOutcome; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.api.server.IBundleProvider; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 8435136b16f..6167350e701 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum; +import ca.uhn.fhir.jpa.util.SqlQuery; import ca.uhn.fhir.jpa.util.TestUtil; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; @@ -15,6 +16,7 @@ import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.method.SearchParameter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu3.model.*; @@ -47,6 +49,7 @@ import java.util.Date; import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -362,16 +365,16 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test { obs.addIdentifier().setSystem("urn:system").setValue("FOO"); obs.setDevice(new Reference(devId)); obs.setSubject(new Reference(pid0)); - myObservationDao.create(obs, mySrd).getId(); + obs.setCode(new CodeableConcept(new Coding("sys", "val", "disp"))); + myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); } - SearchParameterMap params; + SearchParameterMap params = new SearchParameterMap(); - // Not currently working -// params = new SearchParameterMap(); -// params.setLoadSynchronous(true); -// params.add("_has", new HasParam("Observation", "subject", "device.identifier", "urn:system|DEVICEID")); -// assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(pid0.getValue())); + // Target exists and is linked + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "device.identifier", "urn:system|DEVICEID")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(pid0.getValue())); // No targets exist params = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java index 9d1bbe51f52..a19a466c9c0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoHashesTest.java @@ -497,12 +497,10 @@ public class FhirResourceDaoR4SearchNoHashesTest extends BaseJpaR4Test { SearchParameterMap params; -// KHS JA When we switched _has from two queries to a nested subquery, we broke support for chains within _has -// We have decided for now to prefer the performance optimization of the subquery over the slower full capability -// params = new SearchParameterMap(); -// params.setLoadSynchronous(true); -// params.add("_has", new HasParam("Observation", "subject", "device.identifier", "urn:system|DEVICEID")); -// assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(pid0.getValue())); + params = new SearchParameterMap(); + params.setLoadSynchronous(true); + params.add("_has", new HasParam("Observation", "subject", "device.identifier", "urn:system|DEVICEID")); + assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(params)), contains(pid0.getValue())); // No targets exist params = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java index a3a384936d4..1cdb6f4ca37 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/SearchParameterMap.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.builder.ToStringStyle; import java.io.Serializable; import java.util.*; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -455,45 +456,33 @@ public class SearchParameterMap implements Serializable { return b.toString(); } + public void clean() { for (Map.Entry>> nextParamEntry : this.entrySet()) { String nextParamName = nextParamEntry.getKey(); List> andOrParams = nextParamEntry.getValue(); - clean(nextParamName, andOrParams); + cleanParameter(nextParamName, andOrParams); } } /* - * Filter out + * Given a particular named parameter, e.g. `name`, iterate over AndOrParams and remove any which are empty. */ - private void clean(String theParamName, List> theAndOrParams) { - for (int andListIdx = 0; andListIdx < theAndOrParams.size(); andListIdx++) { - List nextOrList = theAndOrParams.get(andListIdx); + private void cleanParameter(String theParamName, List> theAndOrParams) { + theAndOrParams + .forEach( + orList -> { + List emptyParameters = orList.stream() + .filter(nextOr -> nextOr.getMissing() == null) + .filter(nextOr -> nextOr instanceof QuantityParam) + .filter(nextOr -> isBlank(((QuantityParam) nextOr).getValueAsString())) + .collect(Collectors.toList()); - for (int orListIdx = 0; orListIdx < nextOrList.size(); orListIdx++) { - IQueryParameterType nextOr = nextOrList.get(orListIdx); - boolean hasNoValue = false; - if (nextOr.getMissing() != null) { - continue; - } - if (nextOr instanceof QuantityParam) { - if (isBlank(((QuantityParam) nextOr).getValueAsString())) { - hasNoValue = true; - } - } - - if (hasNoValue) { ourLog.debug("Ignoring empty parameter: {}", theParamName); - nextOrList.remove(orListIdx); - orListIdx--; + orList.removeAll(emptyParameters); } - } - - if (nextOrList.isEmpty()) { - theAndOrParams.remove(andListIdx); - andListIdx--; - } - } + ); + theAndOrParams.removeIf(List::isEmpty); } public void setNearDistanceParam(QuantityParam theQuantityParam) {