diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 8a6491e8560..bc507738736 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -317,7 +317,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } @Override - protected DaoConfig getConfig() { + public DaoConfig getConfig() { return myConfig; } 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 2989ef796f1..60ee5fef4b3 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 @@ -24,6 +24,7 @@ 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.predicate.*; import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao; import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; @@ -34,21 +35,15 @@ import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.jpa.model.entity.*; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; -import ca.uhn.fhir.jpa.model.util.StringNormalizer; import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; import ca.uhn.fhir.jpa.searchparam.util.SourceParam; -import ca.uhn.fhir.jpa.term.VersionIndependentConcept; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.util.*; import ca.uhn.fhir.model.api.*; -import ca.uhn.fhir.model.base.composite.BaseCodingDt; -import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; -import ca.uhn.fhir.model.base.composite.BaseQuantityDt; -import ca.uhn.fhir.model.dstu2.resource.Location; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; @@ -59,7 +54,6 @@ 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.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.StopWatch; @@ -67,37 +61,29 @@ import ca.uhn.fhir.util.UrlUtil; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.tuple.Pair; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.query.Query; -import org.hibernate.query.criteria.internal.CriteriaBuilderImpl; -import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate; -import org.hibernate.search.spatial.impl.Point; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IdType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.PersistenceContextType; import javax.persistence.TypedQuery; import javax.persistence.criteria.*; -import java.math.BigDecimal; -import java.math.MathContext; import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.StringUtils.*; @@ -118,7 +104,7 @@ public class SearchBuilder implements ISearchBuilder { public static final int MAXIMUM_PAGE_SIZE = 800; private static final List EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>()); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchBuilder.class); + private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class); private static ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L); private final boolean myDontUseHashesForSearch; private final DaoConfig myDaoConfig; @@ -144,10 +130,13 @@ public class SearchBuilder implements ISearchBuilder { private ITermReadSvc myTerminologySvc; @Autowired private MatchUrlService myMatchUrlService; + @Autowired + private PredicateBuilderFactory myPredicateBuilderFactory; + private List myAlsoIncludePids; private CriteriaBuilder myBuilder; private BaseHapiFhirDao myCallingDao; - private Map> myIndexJoins = Maps.newHashMap(); + private IndexJoins myIndexJoins = new IndexJoins(); private SearchParameterMap myParams; private ArrayList myPredicates; private String myResourceName; @@ -159,6 +148,14 @@ public class SearchBuilder implements ISearchBuilder { private Integer myMaxResultsToFetch; private Set myPidSet; private boolean myHaveIndexJoins = false; + private PredicateBuilderCoords myPredicateBuilderCoords; + private PredicateBuilderDate myPredicateBuilderDate; + private PredicateBuilderNumber myPredicateBuilderNumber; + private PredicateBuilderQuantity myPredicateBuilderQuantity; + private PredicateBuilderString myPredicateBuilderString; + private PredicateBuilderTag myPredicateBuilderTag; + private PredicateBuilderToken myPredicateBuilderToken; + private PredicateBuilderUri myPredicateBuilderUri; /** * Constructor @@ -169,304 +166,23 @@ public class SearchBuilder implements ISearchBuilder { myDontUseHashesForSearch = myDaoConfig.getDisableHashBasedSearches(); } + @PostConstruct + public void postConstruct() { + myPredicateBuilderCoords = myPredicateBuilderFactory.newPredicateBuilderCoords(this); + myPredicateBuilderDate = myPredicateBuilderFactory.newPredicateBuilderDate(this); + myPredicateBuilderNumber = myPredicateBuilderFactory.newPredicateBuilderNumber(this); + myPredicateBuilderQuantity = myPredicateBuilderFactory.newPredicateBuilderQuantity(this); + myPredicateBuilderString = myPredicateBuilderFactory.newPredicateBuilderString(this); + myPredicateBuilderTag = myPredicateBuilderFactory.newPredicateBuilderTag(this); + myPredicateBuilderToken = myPredicateBuilderFactory.newPredicateBuilderToken(this); + myPredicateBuilderUri = myPredicateBuilderFactory.newPredicateBuilderUri(this); + } + @Override public void setMaxResultsToFetch(Integer theMaxResultsToFetch) { myMaxResultsToFetch = theMaxResultsToFetch; } - private void addPredicateComposite(String theResourceName, RuntimeSearchParam theParamDef, List theNextAnd) { - // TODO: fail if missing is set for a composite query - - IQueryParameterType or = theNextAnd.get(0); - if (!(or instanceof CompositeParam)) { - throw new InvalidRequestException("Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + or.getClass()); - } - CompositeParam cp = (CompositeParam) or; - - RuntimeSearchParam left = theParamDef.getCompositeOf().get(0); - IQueryParameterType leftValue = cp.getLeftValue(); - myPredicates.add(createCompositeParamPart(theResourceName, myResourceTableRoot, left, leftValue)); - - RuntimeSearchParam right = theParamDef.getCompositeOf().get(1); - IQueryParameterType rightValue = cp.getRightValue(); - myPredicates.add(createCompositeParamPart(theResourceName, myResourceTableRoot, right, rightValue)); - - } - - private Predicate addPredicateDate(String theResourceName, - String theParamName, - List theList) { - - return addPredicateDate(theResourceName, - theParamName, - theList, - null); - } - - private Predicate addPredicateDate(String theResourceName, - String theParamName, - List theList, - SearchFilterParser.CompareOperation operation) { - - Join join = createJoin(JoinEnum.DATE, theParamName); - - if (theList.get(0).getMissing() != null) { - Boolean missing = theList.get(0).getMissing(); - addPredicateParamMissing(theResourceName, theParamName, missing, join); - return null; - } - - List codePredicates = new ArrayList<>(); - for (IQueryParameterType nextOr : theList) { - IQueryParameterType params = nextOr; - Predicate p = createPredicateDate(params, - theResourceName, - theParamName, - myBuilder, - join, - operation); - codePredicates.add(p); - } - - Predicate orPredicates = myBuilder.or(toArray(codePredicates)); - myPredicates.add(orPredicates); - return orPredicates; - } - - private void addPredicateHas(List> theHasParameters, RequestDetails theRequest) { - - for (List nextOrList : theHasParameters) { - - String targetResourceType = null; - String paramReference = null; - String parameterName = null; - - String paramName = null; - List parameters = new ArrayList<>(); - for (IQueryParameterType nextParam : nextOrList) { - HasParam next = (HasParam) nextParam; - targetResourceType = next.getTargetResourceType(); - paramReference = next.getReferenceFieldName(); - parameterName = next.getParameterName(); - paramName = parameterName.replaceAll("\\..*", ""); - parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myContext))); - } - - if (paramName == null) { - continue; - } - - RuntimeResourceDefinition targetResourceDefinition; - try { - targetResourceDefinition = myContext.getResourceDefinition(targetResourceType); - } catch (DataFormatException e) { - throw new InvalidRequestException("Invalid resource type: " + targetResourceType); - } - - assert parameterName != null; - RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); - if (owningParameterDef == null) { - throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName); - } - - owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramReference); - if (owningParameterDef == null) { - throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + paramReference); - } - - RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); - - IQueryParameterAnd> parsedParam = (IQueryParameterAnd>) ParameterUtil.parseQueryParams(myContext, paramDef, paramName, parameters); - - ArrayList orValues = Lists.newArrayList(); - - for (IQueryParameterOr next : parsedParam.getValuesAsQueryTokens()) { - orValues.addAll(next.getValuesAsQueryTokens()); - } - - Subquery subQ = createLinkSubquery(true, parameterName, targetResourceType, orValues, theRequest); - - Join join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT); - Predicate pathPredicate = createResourceLinkPathPredicate(targetResourceType, paramReference, join); - Predicate pidPredicate = join.get("mySourceResourcePid").in(subQ); - Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate); - myPredicates.add(andPredicate); - } - } - - private Predicate addPredicateLanguage(List> theList) { - return addPredicateLanguage(theList, - null); - } - - private Predicate addPredicateLanguage(List> theList, - SearchFilterParser.CompareOperation operation) { - for (List nextList : theList) { - - Set values = new HashSet<>(); - for (IQueryParameterType next : nextList) { - if (next instanceof StringParam) { - String nextValue = ((StringParam) next).getValue(); - if (isBlank(nextValue)) { - continue; - } - values.add(nextValue); - } else { - throw new InternalErrorException("Language parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName()); - } - } - - if (values.isEmpty()) { - continue; - } - - Predicate predicate = null; - if ((operation == null) || - (operation == SearchFilterParser.CompareOperation.eq)) { - predicate = myResourceTableRoot.get("myLanguage").as(String.class).in(values); - } else if (operation == SearchFilterParser.CompareOperation.ne) { - predicate = myResourceTableRoot.get("myLanguage").as(String.class).in(values).not(); - } else { - throw new InvalidRequestException("Unsupported operator specified in language query, only \"eq\" and \"ne\" are supported"); - } - myPredicates.add(predicate); - if (operation != null) { - return predicate; - } - } - - return null; - } - - private Predicate addPredicateNumber(String theResourceName, - String theParamName, - List theList) { - return addPredicateNumber(theResourceName, - theParamName, - theList, - null); - } - - private Predicate addPredicateNumber(String theResourceName, - String theParamName, - List theList, - SearchFilterParser.CompareOperation operation) { - - Join join = createJoin(JoinEnum.NUMBER, theParamName); - - if (theList.get(0).getMissing() != null) { - addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); - return null; - } - - List codePredicates = new ArrayList<>(); - for (IQueryParameterType nextOr : theList) { - - if (nextOr instanceof NumberParam) { - NumberParam param = (NumberParam) nextOr; - - BigDecimal value = param.getValue(); - if (value == null) { - continue; - } - - final Expression fromObj = join.get("myValue"); - ParamPrefixEnum prefix = defaultIfNull(param.getPrefix(), ParamPrefixEnum.EQUAL); - if (operation == SearchFilterParser.CompareOperation.ne) { - prefix = ParamPrefixEnum.NOT_EQUAL; - } else if (operation == SearchFilterParser.CompareOperation.lt) { - prefix = ParamPrefixEnum.LESSTHAN; - } else if (operation == SearchFilterParser.CompareOperation.le) { - prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS; - } else if (operation == SearchFilterParser.CompareOperation.gt) { - prefix = ParamPrefixEnum.GREATERTHAN; - } else if (operation == SearchFilterParser.CompareOperation.ge) { - prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS; - } else if (operation == SearchFilterParser.CompareOperation.eq) { - prefix = ParamPrefixEnum.EQUAL; - } else if (operation != null) { - throw new IllegalArgumentException("Invalid operator specified for number type"); - } - - - String invalidMessageName = "invalidNumberPrefix"; - - Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, nextOr, prefix, value, fromObj, invalidMessageName); - Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric); - codePredicates.add(predicateOuter); - - } else { - throw new IllegalArgumentException("Invalid token type: " + nextOr.getClass()); - } - - } - - Predicate predicate = myBuilder.or(toArray(codePredicates)); - myPredicates.add(predicate); - return predicate; - } - - private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) { -// if (myDontUseHashesForSearch) { -// Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); -// Join paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT); -// -// myPredicates.add(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName)); -// myPredicates.add(myBuilder.equal(paramJoin.get("myParamName"), theParamName)); -// myPredicates.add(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing)); -// } - - Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); - - Expression hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); - Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing); - myPredicates.add(myBuilder.equal(hashPresence, hash)); - } - - private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join theJoin) { - - myPredicates.add(myBuilder.equal(theJoin.get("myResourceType"), theResourceName)); - myPredicates.add(myBuilder.equal(theJoin.get("myParamName"), theParamName)); - myPredicates.add(myBuilder.equal(theJoin.get("myMissing"), theMissing)); - } - - private Predicate addPredicateQuantity(String theResourceName, - String theParamName, - List theList) { - return addPredicateQuantity(theResourceName, - theParamName, - theList, - null); - } - - private Predicate addPredicateQuantity(String theResourceName, - String theParamName, - List theList, - SearchFilterParser.CompareOperation operation) { - Join join = createJoin(JoinEnum.QUANTITY, theParamName); - - if (theList.get(0).getMissing() != null) { - addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); - return null; - } - - List codePredicates = new ArrayList(); - for (IQueryParameterType nextOr : theList) { - - Predicate singleCode = createPredicateQuantity(nextOr, - theResourceName, - theParamName, - myBuilder, - join, - operation); - codePredicates.add(singleCode); - } - - Predicate retVal = myBuilder.or(toArray(codePredicates)); - myPredicates.add(retVal); - return retVal; - } - private Predicate addPredicateReference(String theResourceName, String theParamName, List theList, @@ -499,7 +215,7 @@ public class SearchBuilder implements ISearchBuilder { return null; } - Join join = createJoin(JoinEnum.REFERENCE, theParamName); + Join join = createJoin(SearchBuilderJoinEnum.REFERENCE, theParamName); List targetIds = new ArrayList<>(); List targetQualifiedUrls = new ArrayList<>(); @@ -734,6 +450,272 @@ public class SearchBuilder implements ISearchBuilder { return predicate; } + private Subquery createLinkSubquery(boolean theFoundChainMatch, String theChain, String theSubResourceName, List theOrValues, RequestDetails theRequest) { + Subquery subQ = myResourceTableQuery.subquery(Long.class); + Root subQfrom = subQ.from(ResourceTable.class); + subQ.select(subQfrom.get("myId").as(Long.class)); + + List> andOrParams = new ArrayList<>(); + andOrParams.add(theOrValues); + + /* + * We're doing a chain call, so push the current query root + * and predicate list down and put new ones at the top of the + * stack and run a subquery + */ + Root stackRoot = myResourceTableRoot; + ArrayList stackPredicates = myPredicates; + Map> stackIndexJoins = myIndexJoins; + myResourceTableRoot = subQfrom; + myPredicates = Lists.newArrayList(); + myIndexJoins = Maps.newHashMap(); + + // Create the subquery predicates + myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), theSubResourceName)); + myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted"))); + + if (theFoundChainMatch) { + searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams, theRequest); + subQ.where(toArray(myPredicates)); + } + + /* + * Pop the old query root and predicate list back + */ + myResourceTableRoot = stackRoot; + myPredicates = stackPredicates; + myIndexJoins = stackIndexJoins; + return subQ; + } + + private void searchForIdsWithAndOr(String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest) { + + if (theAndOrParams.isEmpty()) { + return; + } + + switch (theParamName) { + case IAnyResource.SP_RES_ID: + addPredicateResourceId(theResourceName, theAndOrParams, theRequest); + break; + + case IAnyResource.SP_RES_LANGUAGE: + addPredicateLanguage(theAndOrParams); + break; + + case Constants.PARAM_HAS: + addPredicateHas(theAndOrParams, theRequest); + break; + + case Constants.PARAM_TAG: + case Constants.PARAM_PROFILE: + case Constants.PARAM_SECURITY: + addPredicateTag(theAndOrParams, theParamName); + break; + + case Constants.PARAM_SOURCE: + addPredicateSource(theAndOrParams, theRequest); + break; + + default: + + RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); + if (nextParamDef != null) { + switch (nextParamDef.getParamType()) { + case DATE: + for (List nextAnd : theAndOrParams) { + addPredicateDate(theResourceName, theParamName, nextAnd); + } + break; + case QUANTITY: + for (List nextAnd : theAndOrParams) { + addPredicateQuantity(theResourceName, theParamName, nextAnd); + } + break; + case REFERENCE: + for (List nextAnd : theAndOrParams) { + addPredicateReference(theResourceName, theParamName, nextAnd, theRequest); + } + break; + case STRING: + for (List nextAnd : theAndOrParams) { + addPredicateString(theResourceName, theParamName, nextAnd); + } + break; + case TOKEN: + for (List nextAnd : theAndOrParams) { + if ("Location.position".equals(nextParamDef.getPath())) { + addPredicateCoords(theResourceName, theParamName, nextAnd); + } else { + addPredicateToken(theResourceName, theParamName, nextAnd); + } + } + break; + case NUMBER: + for (List nextAnd : theAndOrParams) { + addPredicateNumber(theResourceName, theParamName, nextAnd); + } + break; + case COMPOSITE: + for (List nextAnd : theAndOrParams) { + addPredicateComposite(theResourceName, nextParamDef, nextAnd); + } + break; + case URI: + for (List nextAnd : theAndOrParams) { + addPredicateUri(theResourceName, theParamName, nextAnd); + } + break; + case HAS: + case SPECIAL: + for (List nextAnd : theAndOrParams) { + if ("Location.position".equals(nextParamDef.getPath())) { + addPredicateCoords(theResourceName, theParamName, nextAnd); + } + } + break; + } + } else { + if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { + // These are handled later + } else if (Constants.PARAM_FILTER.equals(theParamName)) { + // Parse the predicates enumerated in the _filter separated by AND or OR... + if (theAndOrParams.get(0).get(0) instanceof StringParam) { + String filterString = ((StringParam) theAndOrParams.get(0).get(0)).getValue(); + SearchFilterParser.Filter filter; + try { + filter = SearchFilterParser.parse(filterString); + } catch (SearchFilterParser.FilterSyntaxException theE) { + throw new InvalidRequestException("Error parsing _filter syntax: " + theE.getMessage()); + } + if (filter != null) { + + if (!myDaoConfig.isFilterParameterEnabled()) { + throw new InvalidRequestException(Constants.PARAM_FILTER + " parameter is disabled on this server"); + } + + // TODO: we clear the predicates below because the filter builds up + // its own collection of predicates. It'd probably be good at some + // point to do something more fancy... + ArrayList holdPredicates = new ArrayList<>(myPredicates); + + Predicate filterPredicate = processFilter(filter, theResourceName, theRequest); + myPredicates.clear(); + myPredicates.addAll(holdPredicates); + myPredicates.add(filterPredicate); + } + } + + + } else { + throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName); + } + } + break; + } + } + + private void addPredicateCoords(String theResourceName, String theParamName, List theNextAnd) { + // FIXME KHS consolidate these predicate builders (e.g. all these mathods should have the same name) + myPredicateBuilderCoords.addPredicateCoords(theResourceName, theParamName, theNextAnd); + } + + private void addPredicateDate(String theResourceName, String theParamName, List theNextAnd) { + myPredicateBuilderDate.addPredicateDate(theResourceName, theParamName, theNextAnd, null); + } + + private Predicate addPredicateDate(String theResourceName, String theParamName, List theNextAnd, SearchFilterParser.CompareOperation theOperation) { + return myPredicateBuilderDate.addPredicateDate(theResourceName, theParamName, theNextAnd, theOperation); + } + + private void addPredicateNumber(String theResourceName, String theParamName, List theNextAnd) { + myPredicateBuilderNumber.addPredicateNumber(theResourceName, theParamName, theNextAnd, null); + } + + private Predicate addPredicateNumber(String theResourceName, String theParamName, List theNextAnd, SearchFilterParser.CompareOperation theOperation) { + return myPredicateBuilderNumber.addPredicateNumber(theResourceName, theParamName, theNextAnd, theOperation); + } + + private void addPredicateQuantity(String theResourceName, String theParamName, List theNextAnd) { + myPredicateBuilderQuantity.addPredicateQuantity(theResourceName, theParamName, theNextAnd, null); + } + + private Predicate addPredicateQuantity(String theResourceName, String theParamName, List theNextAnd, SearchFilterParser.CompareOperation theOperation) { + return myPredicateBuilderQuantity.addPredicateQuantity(theResourceName, theParamName, theNextAnd, theOperation); + } + + private void addPredicateString(String theResourceName, String theParamName, List theNextAnd) { + myPredicateBuilderString.addPredicateString(theResourceName, theParamName, theNextAnd, SearchFilterParser.CompareOperation.sw); + } + + private Predicate addPredicateString(String theResourceName, String theParamName, List theNextAnd, SearchFilterParser.CompareOperation theOperation) { + return myPredicateBuilderString.addPredicateString(theResourceName, theParamName, theNextAnd, theOperation); + } + + private void addPredicateTag(List> theAndOrParams, String theParamName) { + myPredicateBuilderTag.addPredicateTag(theAndOrParams, theParamName); + } + + private void addPredicateToken(String theResourceName, String theParamName, List theNextAnd) { + myPredicateBuilderToken.addPredicateToken(theResourceName, theParamName, theNextAnd, null); + } + + private Predicate addPredicateToken(String theResourceName, String theParamName, List theNextAnd, SearchFilterParser.CompareOperation theOperation) { + return myPredicateBuilderToken.addPredicateToken(theResourceName, theParamName, theNextAnd, theOperation); + } + + private void addPredicateUri(String theResourceName, String theParamName, List theNextAnd) { + myPredicateBuilderUri.addPredicateUri(theResourceName, theParamName, theNextAnd, SearchFilterParser.CompareOperation.eq); + } + + private Predicate addPredicateUri(String theResourceName, String theName, List theSingletonList, SearchFilterParser.CompareOperation theOperation) { + return myPredicateBuilderUri.addPredicateUri(theResourceName, theName, theSingletonList, theOperation); + } + + private Predicate addPredicateLanguage(List> theList) { + return addPredicateLanguage(theList, + null); + } + + private Predicate addPredicateLanguage(List> theList, + SearchFilterParser.CompareOperation operation) { + for (List nextList : theList) { + + Set values = new HashSet<>(); + for (IQueryParameterType next : nextList) { + if (next instanceof StringParam) { + String nextValue = ((StringParam) next).getValue(); + if (isBlank(nextValue)) { + continue; + } + values.add(nextValue); + } else { + throw new InternalErrorException("Language parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName()); + } + } + + if (values.isEmpty()) { + continue; + } + + Predicate predicate = null; + if ((operation == null) || + (operation == SearchFilterParser.CompareOperation.eq)) { + predicate = myResourceTableRoot.get("myLanguage").as(String.class).in(values); + } else if (operation == SearchFilterParser.CompareOperation.ne) { + predicate = myResourceTableRoot.get("myLanguage").as(String.class).in(values).not(); + } else { + throw new InvalidRequestException("Unsupported operator specified in language query, only \"eq\" and \"ne\" are supported"); + } + myPredicates.add(predicate); + if (operation != null) { + return predicate; + } + } + + return null; + } + private void addPredicateSource(List> theAndOrParams, RequestDetails theRequest) { for (List nextAnd : theAndOrParams) { addPredicateSource(nextAnd, SearchFilterParser.CompareOperation.eq, theRequest); @@ -770,70 +752,12 @@ public class SearchBuilder implements ISearchBuilder { return retVal; } - private Subquery createLinkSubquery(boolean theFoundChainMatch, String theChain, String theSubResourceName, List theOrValues, RequestDetails theRequest) { - Subquery subQ = myResourceTableQuery.subquery(Long.class); - Root subQfrom = subQ.from(ResourceTable.class); - subQ.select(subQfrom.get("myId").as(Long.class)); - - List> andOrParams = new ArrayList<>(); - andOrParams.add(theOrValues); - - /* - * We're doing a chain call, so push the current query root - * and predicate list down and put new ones at the top of the - * stack and run a subquery - */ - Root stackRoot = myResourceTableRoot; - ArrayList stackPredicates = myPredicates; - Map> stackIndexJoins = myIndexJoins; - myResourceTableRoot = subQfrom; - myPredicates = Lists.newArrayList(); - myIndexJoins = Maps.newHashMap(); - - // Create the subquery predicates - myPredicates.add(myBuilder.equal(myResourceTableRoot.get("myResourceType"), theSubResourceName)); - myPredicates.add(myBuilder.isNull(myResourceTableRoot.get("myDeleted"))); - - if (theFoundChainMatch) { - searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams, theRequest); - subQ.where(toArray(myPredicates)); - } - - /* - * Pop the old query root and predicate list back - */ - myResourceTableRoot = stackRoot; - myPredicates = stackPredicates; - myIndexJoins = stackIndexJoins; - return subQ; - } - - private IQueryParameterType mapReferenceChainToRawParamType(String remainingChain, RuntimeSearchParam param, String theParamName, String qualifier, Class nextType, String chain, boolean isMeta, String resourceId) { - IQueryParameterType chainValue; - if (remainingChain != null) { - if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { - ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain); - return null; - } - - chainValue = new ReferenceParam(); - chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); - ((ReferenceParam) chainValue).setChain(remainingChain); - } else if (isMeta) { - IQueryParameterType type = myMatchUrlService.newInstanceType(chain); - type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); - chainValue = type; - } else { - chainValue = toParameterType(param, qualifier, resourceId); - } - - return chainValue; - } private void addPredicateResourceId(String theResourceName, List> theValues, RequestDetails theRequest) { addPredicateResourceId(theValues, theResourceName, null, theRequest); } + private Predicate addPredicateResourceId(List> theValues, String theResourceName, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest) { Predicate nextPredicate = createPredicateResourceId(myResourceTableRoot, theResourceName, theValues, theOperation, theRequest); @@ -912,396 +836,161 @@ public class SearchBuilder implements ISearchBuilder { return nextPredicate; } + private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams, RequestDetails theRequest) { + myParams = theParams; - private void addPredicateString(String theResourceName, - String theParamName, - List theList) { - addPredicateString(theResourceName, - theParamName, - theList, - SearchFilterParser.CompareOperation.sw); - } + // Remove any empty parameters + theParams.clean(); - private Predicate addPredicateString(String theResourceName, - String theParamName, - List theList, - SearchFilterParser.CompareOperation operation) { - - Join join = createJoin(JoinEnum.STRING, theParamName); - - if (theList.get(0).getMissing() != null) { - addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); - return null; + /* + * 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) { + attemptCompositeUniqueSpProcessing(theParams, theRequest); } - List codePredicates = new ArrayList<>(); - for (IQueryParameterType nextOr : theList) { - IQueryParameterType theParameter = nextOr; - Predicate singleCode = createPredicateString(theParameter, - theResourceName, - theParamName, - myBuilder, - join, - operation); - codePredicates.add(singleCode); + // Handle each parameter + for (Map.Entry>> nextParamEntry : myParams.entrySet()) { + String nextParamName = nextParamEntry.getKey(); + List> andOrParams = nextParamEntry.getValue(); + searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams, theRequest); } - Predicate retVal = myBuilder.or(toArray(codePredicates)); - myPredicates.add(retVal); - return retVal; } - private void addPredicateTag(List> theList, String theParamName) { - TagTypeEnum tagType; - if (Constants.PARAM_TAG.equals(theParamName)) { - tagType = TagTypeEnum.TAG; - } else if (Constants.PARAM_PROFILE.equals(theParamName)) { - tagType = TagTypeEnum.PROFILE; - } else if (Constants.PARAM_SECURITY.equals(theParamName)) { - tagType = TagTypeEnum.SECURITY_LABEL; + private IQueryParameterType mapReferenceChainToRawParamType(String remainingChain, RuntimeSearchParam param, String theParamName, String qualifier, Class nextType, String chain, boolean isMeta, String resourceId) { + IQueryParameterType chainValue; + if (remainingChain != null) { + if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) { + ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain); + return null; + } + + chainValue = new ReferenceParam(); + chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); + ((ReferenceParam) chainValue).setChain(remainingChain); + } else if (isMeta) { + IQueryParameterType type = myMatchUrlService.newInstanceType(chain); + type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId); + chainValue = type; } else { - throw new IllegalArgumentException("Param name: " + theParamName); // shouldn't happen + chainValue = toParameterType(param, qualifier, resourceId); } - List> notTags = Lists.newArrayList(); - for (List nextAndParams : theList) { - for (IQueryParameterType nextOrParams : nextAndParams) { - if (nextOrParams instanceof TokenParam) { - TokenParam param = (TokenParam) nextOrParams; - if (param.getModifier() == TokenParamModifier.NOT) { - if (isNotBlank(param.getSystem()) || isNotBlank(param.getValue())) { - notTags.add(Pair.of(param.getSystem(), param.getValue())); - } - } - } - } - } + return chainValue; + } + + private Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From from) { + return createResourceLinkPathPredicate(myContext, theParamName, from, theResourceName); + } + + private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From theFrom, + String theResourceType) { + RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType); + RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); + List path = param.getPathsSplit(); /* - * We have a parameter of ResourceType?_tag:not=foo This means match resources that don't have the given tag(s) + * SearchParameters can declare paths on multiple resource + * types. Here we only want the ones that actually apply. */ - if (notTags.isEmpty() == false) { - // CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); - // CriteriaQuery cq = builder.createQuery(Long.class); - // Root from = cq.from(ResourceTable.class); - // cq.select(from.get("myId").as(Long.class)); - // - // Subquery subQ = cq.subquery(Long.class); - // Root subQfrom = subQ.from(ResourceTag.class); - // subQ.select(subQfrom.get("myResourceId").as(Long.class)); - // Predicate subQname = builder.equal(subQfrom.get("myParamName"), theParamName); - // Predicate subQtype = builder.equal(subQfrom.get("myResourceType"), myResourceName); - // subQ.where(builder.and(subQtype, subQname)); - // - // List predicates = new ArrayList(); - // predicates.add(builder.not(builder.in(from.get("myId")).value(subQ))); - // predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); - // predicates.add(builder.isNull(from.get("myDeleted"))); - // createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); + path = new ArrayList<>(path); + + ListIterator iter = path.listIterator(); + while (iter.hasNext()) { + String nextPath = trim(iter.next()); + if (!nextPath.contains(theResourceType + ".")) { + iter.remove(); + } } - for (List nextAndParams : theList) { - boolean haveTags = false; - for (IQueryParameterType nextParamUncasted : nextAndParams) { - if (nextParamUncasted instanceof TokenParam) { - TokenParam nextParam = (TokenParam) nextParamUncasted; - if (isNotBlank(nextParam.getValue())) { - haveTags = true; - } else if (isNotBlank(nextParam.getSystem())) { - throw new InvalidRequestException("Invalid " + theParamName + " parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken(myContext)); - } - } else { - UriParam nextParam = (UriParam) nextParamUncasted; - if (isNotBlank(nextParam.getValue())) { - haveTags = true; - } - } + return theFrom.get("mySourcePath").in(path); + } + + + private void addPredicateHas(List> theHasParameters, RequestDetails theRequest) { + + for (List nextOrList : theHasParameters) { + + String targetResourceType = null; + String paramReference = null; + String parameterName = null; + + String paramName = null; + List parameters = new ArrayList<>(); + for (IQueryParameterType nextParam : nextOrList) { + HasParam next = (HasParam) nextParam; + targetResourceType = next.getTargetResourceType(); + paramReference = next.getReferenceFieldName(); + parameterName = next.getParameterName(); + paramName = parameterName.replaceAll("\\..*", ""); + parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myContext))); } - if (!haveTags) { + + if (paramName == null) { continue; } - boolean paramInverted = false; - List> tokens = Lists.newArrayList(); - for (IQueryParameterType nextOrParams : nextAndParams) { - String code; - String system; - if (nextOrParams instanceof TokenParam) { - TokenParam nextParam = (TokenParam) nextOrParams; - code = nextParam.getValue(); - system = nextParam.getSystem(); - if (nextParam.getModifier() == TokenParamModifier.NOT) { - paramInverted = true; - } - } else { - UriParam nextParam = (UriParam) nextOrParams; - code = nextParam.getValue(); - system = null; - } - - if (isNotBlank(code)) { - tokens.add(Pair.of(system, code)); - } + RuntimeResourceDefinition targetResourceDefinition; + try { + targetResourceDefinition = myContext.getResourceDefinition(targetResourceType); + } catch (DataFormatException e) { + throw new InvalidRequestException("Invalid resource type: " + targetResourceType); } - if (tokens.isEmpty()) { - continue; + assert parameterName != null; + RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); + if (owningParameterDef == null) { + throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + parameterName); } - if (paramInverted) { - ourLog.debug("Searching for _tag:not"); - - Subquery subQ = myResourceTableQuery.subquery(Long.class); - Root subQfrom = subQ.from(ResourceTag.class); - subQ.select(subQfrom.get("myResourceId").as(Long.class)); - - myPredicates.add(myBuilder.not(myBuilder.in(myResourceTableRoot.get("myId")).value(subQ))); - - Subquery defJoin = subQ.subquery(Long.class); - Root defJoinFrom = defJoin.from(TagDefinition.class); - defJoin.select(defJoinFrom.get("myId").as(Long.class)); - - subQ.where(subQfrom.get("myTagId").as(Long.class).in(defJoin)); - - Predicate tagListPredicate = createPredicateTagList(defJoinFrom, myBuilder, tagType, tokens); - defJoin.where(tagListPredicate); - - continue; + owningParameterDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramReference); + if (owningParameterDef == null) { + throw new InvalidRequestException("Unknown parameter name: " + targetResourceType + ':' + paramReference); } - Join tagJoin = myResourceTableRoot.join("myTags", JoinType.LEFT); - From defJoin = tagJoin.join("myTag"); + RuntimeSearchParam paramDef = mySearchParamRegistry.getSearchParamByName(targetResourceDefinition, paramName); - Predicate tagListPredicate = createPredicateTagList(defJoin, myBuilder, tagType, tokens); - myPredicates.add(tagListPredicate); + IQueryParameterAnd> parsedParam = (IQueryParameterAnd>) ParameterUtil.parseQueryParams(myContext, paramDef, paramName, parameters); - } + ArrayList orValues = Lists.newArrayList(); - } - - private Predicate addPredicateToken(String theResourceName, - String theParamName, - List theList) { - return addPredicateToken(theResourceName, - theParamName, - theList, - null); - } - - private Predicate addPredicateToken(String theResourceName, - String theParamName, - List theList, - SearchFilterParser.CompareOperation operation) { - - if (theList.get(0).getMissing() != null) { - Join join = createJoin(JoinEnum.TOKEN, theParamName); - addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); - return null; - } - - List codePredicates = new ArrayList<>(); - List tokens = new ArrayList<>(); - for (IQueryParameterType nextOr : theList) { - - if (nextOr instanceof TokenParam) { - TokenParam id = (TokenParam) nextOr; - if (id.isText()) { - addPredicateString(theResourceName, theParamName, theList); - break; - } + for (IQueryParameterOr next : parsedParam.getValuesAsQueryTokens()) { + orValues.addAll(next.getValuesAsQueryTokens()); } - tokens.add(nextOr); + Subquery subQ = createLinkSubquery(true, parameterName, targetResourceType, orValues, theRequest); + + Join join = myResourceTableRoot.join("myResourceLinksAsTarget", JoinType.LEFT); + Predicate pathPredicate = createResourceLinkPathPredicate(targetResourceType, paramReference, join); + Predicate pidPredicate = join.get("mySourceResourcePid").in(subQ); + Predicate andPredicate = myBuilder.and(pathPredicate, pidPredicate); + myPredicates.add(andPredicate); } - - if (tokens.isEmpty()) { - return null; - } - - Join join = createJoin(JoinEnum.TOKEN, theParamName); - Collection singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join, operation); - assert singleCode != null; - codePredicates.addAll(singleCode); - - Predicate spPredicate = myBuilder.or(toArray(codePredicates)); - myPredicates.add(spPredicate); - return spPredicate; } - private Predicate addPredicateUri(String theResourceName, - String theParamName, - List theList) { - return addPredicateUri(theResourceName, - theParamName, - theList, - SearchFilterParser.CompareOperation.eq); - } + private void addPredicateComposite(String theResourceName, RuntimeSearchParam theParamDef, List theNextAnd) { + // TODO: fail if missing is set for a composite query - private Predicate addPredicateUri(String theResourceName, - String theParamName, - List theList, - SearchFilterParser.CompareOperation operation) { - - Join join = createJoin(JoinEnum.URI, theParamName); - - if (theList.get(0).getMissing() != null) { - addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); - return null; + IQueryParameterType or = theNextAnd.get(0); + if (!(or instanceof CompositeParam)) { + throw new InvalidRequestException("Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + or.getClass()); } + CompositeParam cp = (CompositeParam) or; - List codePredicates = new ArrayList<>(); - for (IQueryParameterType nextOr : theList) { + RuntimeSearchParam left = theParamDef.getCompositeOf().get(0); + IQueryParameterType leftValue = cp.getLeftValue(); + myPredicates.add(createCompositeParamPart(theResourceName, myResourceTableRoot, left, leftValue)); - if (nextOr instanceof UriParam) { - UriParam param = (UriParam) nextOr; + RuntimeSearchParam right = theParamDef.getCompositeOf().get(1); + IQueryParameterType rightValue = cp.getRightValue(); + myPredicates.add(createCompositeParamPart(theResourceName, myResourceTableRoot, right, rightValue)); - String value = param.getValue(); - if (value == null) { - continue; - } - - if (param.getQualifier() == UriParamQualifierEnum.ABOVE) { - - /* - * :above is an inefficient query- It means that the user is supplying a more specific URL (say - * http://example.com/foo/bar/baz) and that we should match on any URLs that are less - * specific but otherwise the same. For example http://example.com and http://example.com/foo would both - * match. - * - * We do this by querying the DB for all candidate URIs and then manually checking each one. This isn't - * very efficient, but this is also probably not a very common type of query to do. - * - * If we ever need to make this more efficient, lucene could certainly be used as an optimization. - */ - ourLog.info("Searching for candidate URI:above parameters for Resource[{}] param[{}]", myResourceName, theParamName); - Collection candidates = myResourceIndexedSearchParamUriDao.findAllByResourceTypeAndParamName(myResourceName, theParamName); - List toFind = new ArrayList<>(); - for (String next : candidates) { - if (value.length() >= next.length()) { - if (value.substring(0, next.length()).equals(next)) { - toFind.add(next); - } - } - } - - if (toFind.isEmpty()) { - continue; - } - - Predicate uriPredicate = join.get("myUri").as(String.class).in(toFind); - Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate); - codePredicates.add(hashAndUriPredicate); - - } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { - - Predicate uriPredicate = myBuilder.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); - 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); - codePredicates.add(hashPredicate); - } else if (operation == SearchFilterParser.CompareOperation.ne) { - uriPredicate = myBuilder.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)); - } else if (operation == SearchFilterParser.CompareOperation.gt) { - uriPredicate = myBuilder.greaterThan(join.get("myUri").as(String.class), value); - } else if (operation == SearchFilterParser.CompareOperation.lt) { - uriPredicate = myBuilder.lessThan(join.get("myUri").as(String.class), value); - } else if (operation == SearchFilterParser.CompareOperation.ge) { - uriPredicate = myBuilder.greaterThanOrEqualTo(join.get("myUri").as(String.class), value); - } else if (operation == SearchFilterParser.CompareOperation.le) { - uriPredicate = myBuilder.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)); - } else if (operation == SearchFilterParser.CompareOperation.ew) { - uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createRightMatchLikeExpression(value)); - } else { - throw new IllegalArgumentException(String.format("Unsupported operator specified in _filter clause, %s", - operation.toString())); - } - - if (uriPredicate != null) { - long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); - Predicate hashIdentityPredicate = myBuilder.equal(join.get("myHashIdentity"), hashIdentity); - codePredicates.add(myBuilder.and(hashIdentityPredicate, uriPredicate)); - } - } - } - - } else { - throw new IllegalArgumentException("Invalid URI type: " + nextOr.getClass()); - } - - } - - /* - * If we haven't found any of the requested URIs in the candidates, then we'll - * just add a predicate that can never match - */ - if (codePredicates.isEmpty()) { - Predicate predicate = myBuilder.isNull(join.get("myMissing").as(String.class)); - myPredicates.add(predicate); - return null; - } - - Predicate orPredicate = myBuilder.or(toArray(codePredicates)); - - Predicate outerPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, - theParamName, - join, - orPredicate); - myPredicates.add(outerPredicate); - return outerPredicate; - } - - private Predicate addPredicateCoords(String theResourceName, - String theParamName, - List theList) { - Join join = createJoin(JoinEnum.COORDS, theParamName); - - if (theList.get(0).getMissing() != null) { - addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); - return null; - } - - List codePredicates = new ArrayList(); - for (IQueryParameterType nextOr : theList) { - - Predicate singleCode = createPredicateCoords(nextOr, - theResourceName, - theParamName, - myBuilder, - join - ); - codePredicates.add(singleCode); - } - - Predicate retVal = myBuilder.or(toArray(codePredicates)); - myPredicates.add(retVal); - return retVal; - } - - private 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); - return outerPredicate; - } - - long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); - Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); - return myBuilder.and(hashIdentityPredicate, thePredicate); } private Predicate createCompositeParamPart(String theResourceName, Root theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue) { @@ -1309,24 +998,24 @@ public class SearchBuilder implements ISearchBuilder { switch (theParam.getParamType()) { case STRING: { From stringJoin = theRoot.join("myParamsString", JoinType.INNER); - retVal = createPredicateString(leftValue, theResourceName, theParam.getName(), myBuilder, stringJoin); + retVal = myPredicateBuilderString.createPredicateString(leftValue, theResourceName, theParam.getName(), myBuilder, stringJoin); break; } case TOKEN: { From tokenJoin = theRoot.join("myParamsToken", JoinType.INNER); List tokens = Collections.singletonList(leftValue); - Collection tokenPredicates = createPredicateToken(tokens, theResourceName, theParam.getName(), myBuilder, tokenJoin); + Collection tokenPredicates = myPredicateBuilderToken.createPredicateToken(tokens, theResourceName, theParam.getName(), myBuilder, tokenJoin); retVal = myBuilder.and(tokenPredicates.toArray(new Predicate[0])); break; } case DATE: { From dateJoin = theRoot.join("myParamsDate", JoinType.INNER); - retVal = createPredicateDate(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); + retVal = myPredicateBuilderDate.createPredicateDate(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); break; } case QUANTITY: { From dateJoin = theRoot.join("myParamsQuantity", JoinType.INNER); - retVal = createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); + retVal = myPredicateBuilderQuantity.createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myBuilder, dateJoin); break; } case COMPOSITE: @@ -1345,779 +1034,7 @@ public class SearchBuilder implements ISearchBuilder { return retVal; } - @SuppressWarnings("unchecked") - private Join createJoin(JoinEnum theType, String theSearchParameterName) { - Join join = null; - switch (theType) { - case DATE: - join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); - break; - case NUMBER: - join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); - break; - case QUANTITY: - join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); - break; - case REFERENCE: - join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); - break; - case STRING: - join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); - break; - case URI: - join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); - break; - case TOKEN: - join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); - break; - case COORDS: - join = myResourceTableRoot.join("myParamsCoords", JoinType.LEFT); - break; - } - JoinKey key = new JoinKey(theSearchParameterName, theType); - myIndexJoins.put(key, join); - myHaveIndexJoins = true; - - return (Join) join; - } - - private Predicate createPredicateDate(IQueryParameterType theParam, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom) { - return createPredicateDate(theParam, - theResourceName, - theParamName, - theBuilder, - theFrom, - null); - } - - private Predicate createPredicateDate(IQueryParameterType theParam, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom, - SearchFilterParser.CompareOperation operation) { - - Predicate p; - if (theParam instanceof DateParam) { - DateParam date = (DateParam) theParam; - if (!date.isEmpty()) { - DateRangeParam range = new DateRangeParam(date); - p = createPredicateDateFromRange(theBuilder, - theFrom, - range, - operation); - } else { - // TODO: handle missing date param? - p = null; - } - } else if (theParam instanceof DateRangeParam) { - DateRangeParam range = (DateRangeParam) theParam; - p = createPredicateDateFromRange(theBuilder, - theFrom, - range, - operation); - } else { - throw new IllegalArgumentException("Invalid token type: " + theParam.getClass()); - } - - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, p); - } - - private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, - From theFrom, - DateRangeParam theRange, - SearchFilterParser.CompareOperation operation) { - Date lowerBound = theRange.getLowerBoundAsInstant(); - Date upperBound = theRange.getUpperBoundAsInstant(); - Predicate lt = null; - Predicate gt = null; - Predicate lb = null; - Predicate ub = null; - - if (operation == SearchFilterParser.CompareOperation.lt) { - if (lowerBound == null) { - throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); - } - lb = theBuilder.lessThan(theFrom.get("myValueLow"), lowerBound); - } else if (operation == SearchFilterParser.CompareOperation.le) { - if (upperBound == null) { - throw new InvalidRequestException("upperBound value not correctly specified for compare operation"); - } - lb = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); - } else if (operation == SearchFilterParser.CompareOperation.gt) { - if (upperBound == null) { - throw new InvalidRequestException("upperBound value not correctly specified for compare operation"); - } - lb = theBuilder.greaterThan(theFrom.get("myValueHigh"), upperBound); - } else if (operation == SearchFilterParser.CompareOperation.ge) { - if (lowerBound == null) { - throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); - } - lb = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); - } else if (operation == SearchFilterParser.CompareOperation.ne) { - if ((lowerBound == null) || - (upperBound == null)) { - throw new InvalidRequestException("lowerBound and/or upperBound value not correctly specified for compare operation"); - } - /*Predicate*/ - lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); - /*Predicate*/ - gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); - lb = theBuilder.or(lt, - gt); - } else if ((operation == SearchFilterParser.CompareOperation.eq) || - (operation == null)) { - if (lowerBound != null) { - /*Predicate*/ - gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); - /*Predicate*/ - lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound); - if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) { - lb = gt; - } else { - lb = theBuilder.or(gt, lt); - } - } - - if (upperBound != null) { - /*Predicate*/ - gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound); - /*Predicate*/ - lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); - if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { - ub = lt; - } else { - ub = theBuilder.or(gt, lt); - } - } - } else { - throw new InvalidRequestException(String.format("Unsupported operator specified, operator=%s", - operation.name())); - } - - ourLog.trace("Date range is {} - {}", lowerBound, upperBound); - - if (lb != null && ub != null) { - return (theBuilder.and(lb, ub)); - } else if (lb != null) { - return (lb); - } else { - return (ub); - } - } - - private Predicate createPredicateNumeric(String theResourceName, - String theParamName, - From theFrom, - CriteriaBuilder builder, - IQueryParameterType theParam, - ParamPrefixEnum thePrefix, - BigDecimal theValue, - final Expression thePath, - String invalidMessageName) { - Predicate num; - // Per discussions with Grahame Grieve and James Agnew on 11/13/19, modified logic for EQUAL and NOT_EQUAL operators below so as to - // use exact value matching. The "fuzz amount" matching is still used with the APPROXIMATE operator. - switch (thePrefix) { - case GREATERTHAN: - num = builder.gt(thePath, theValue); - break; - case GREATERTHAN_OR_EQUALS: - num = builder.ge(thePath, theValue); - break; - case LESSTHAN: - num = builder.lt(thePath, theValue); - break; - case LESSTHAN_OR_EQUALS: - num = builder.le(thePath, theValue); - break; - case EQUAL: - num = builder.equal(thePath, theValue); - break; - case NOT_EQUAL: - num = builder.notEqual(thePath, theValue); - break; - case APPROXIMATE: - BigDecimal mul = calculateFuzzAmount(thePrefix, theValue); - BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64); - BigDecimal high = theValue.add(mul, MathContext.DECIMAL64); - Predicate lowPred; - Predicate highPred; - lowPred = builder.ge(thePath.as(BigDecimal.class), low); - highPred = builder.le(thePath.as(BigDecimal.class), high); - num = builder.and(lowPred, highPred); - ourLog.trace("Searching for {} <= val <= {}", low, high); - break; - case ENDS_BEFORE: - case STARTS_AFTER: - default: - String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, invalidMessageName, thePrefix.getValue(), theParam.getValueAsQueryToken(myContext)); - throw new InvalidRequestException(msg); - } - - if (theParamName == null) { - return num; - } - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num); - } - - private Predicate createPredicateQuantity(IQueryParameterType theParam, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom) { - return createPredicateQuantity(theParam, - theResourceName, - theParamName, - theBuilder, - theFrom, - null); - } - - private Predicate createPredicateQuantity(IQueryParameterType theParam, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom, - SearchFilterParser.CompareOperation operation) { - String systemValue; - String unitsValue; - ParamPrefixEnum cmpValue = null; - BigDecimal valueValue; - - if (operation == SearchFilterParser.CompareOperation.ne) { - cmpValue = ParamPrefixEnum.NOT_EQUAL; - } else if (operation == SearchFilterParser.CompareOperation.lt) { - cmpValue = ParamPrefixEnum.LESSTHAN; - } else if (operation == SearchFilterParser.CompareOperation.le) { - cmpValue = ParamPrefixEnum.LESSTHAN_OR_EQUALS; - } else if (operation == SearchFilterParser.CompareOperation.gt) { - cmpValue = ParamPrefixEnum.GREATERTHAN; - } else if (operation == SearchFilterParser.CompareOperation.ge) { - cmpValue = ParamPrefixEnum.GREATERTHAN_OR_EQUALS; - } else if (operation == SearchFilterParser.CompareOperation.eq) { - cmpValue = ParamPrefixEnum.EQUAL; - } else if (operation != null) { - throw new IllegalArgumentException("Invalid operator specified for quantity type"); - } - - if (theParam instanceof BaseQuantityDt) { - BaseQuantityDt param = (BaseQuantityDt) theParam; - systemValue = param.getSystemElement().getValueAsString(); - unitsValue = param.getUnitsElement().getValueAsString(); - if (operation == null) { - cmpValue = ParamPrefixEnum.forValue(param.getComparatorElement().getValueAsString()); - } - valueValue = param.getValueElement().getValue(); - } else if (theParam instanceof QuantityParam) { - QuantityParam param = (QuantityParam) theParam; - systemValue = param.getSystem(); - unitsValue = param.getUnits(); - if (operation == null) { - cmpValue = param.getPrefix(); - } - valueValue = param.getValue(); - } else { - throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); - } - - if (myDontUseHashesForSearch) { - Predicate system = null; - if (!isBlank(systemValue)) { - system = theBuilder.equal(theFrom.get("mySystem"), systemValue); - } - - Predicate code = null; - if (!isBlank(unitsValue)) { - code = theBuilder.equal(theFrom.get("myUnits"), unitsValue); - } - - cmpValue = defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); - final Expression path = theFrom.get("myValue"); - String invalidMessageName = "invalidQuantityPrefix"; - - Predicate num = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); - - Predicate singleCode; - if (system == null && code == null) { - singleCode = num; - } else if (system == null) { - singleCode = theBuilder.and(code, num); - } else if (code == null) { - singleCode = theBuilder.and(system, num); - } else { - singleCode = theBuilder.and(system, code, num); - } - - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } - - Predicate hashPredicate; - if (!isBlank(systemValue) && !isBlank(unitsValue)) { - long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue); - hashPredicate = myBuilder.equal(theFrom.get("myHashIdentitySystemAndUnits"), hash); - } else if (!isBlank(unitsValue)) { - long hash = ResourceIndexedSearchParamQuantity.calculateHashUnits(theResourceName, theParamName, unitsValue); - hashPredicate = myBuilder.equal(theFrom.get("myHashIdentityAndUnits"), hash); - } else { - long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); - hashPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hash); - } - - cmpValue = defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); - final Expression path = theFrom.get("myValue"); - String invalidMessageName = "invalidQuantityPrefix"; - - Predicate numericPredicate = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); - - return theBuilder.and(hashPredicate, numericPredicate); - } - - private Predicate createPredicateString(IQueryParameterType theParameter, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom) { - return createPredicateString(theParameter, - theResourceName, - theParamName, - theBuilder, - theFrom, - null); - } - - private Predicate createPredicateString(IQueryParameterType theParameter, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom, - SearchFilterParser.CompareOperation operation) { - String rawSearchTerm; - if (theParameter instanceof TokenParam) { - TokenParam id = (TokenParam) theParameter; - if (!id.isText()) { - throw new IllegalStateException("Trying to process a text search on a non-text token parameter"); - } - rawSearchTerm = id.getValue(); - } else if (theParameter instanceof StringParam) { - StringParam id = (StringParam) theParameter; - rawSearchTerm = id.getValue(); - if (id.isContains()) { - if (!myDaoConfig.isAllowContainsSearches()) { - throw new MethodNotAllowedException(":contains modifier is disabled on this server"); - } - } - } else if (theParameter instanceof IPrimitiveDatatype) { - IPrimitiveDatatype id = (IPrimitiveDatatype) theParameter; - rawSearchTerm = id.getValueAsString(); - } else { - throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass()); - } - - if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { - throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed (" - + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); - } - - if (myDontUseHashesForSearch) { - String likeExpression = StringNormalizer.normalizeString(rawSearchTerm); - if (myDaoConfig.isAllowContainsSearches()) { - if (theParameter instanceof StringParam) { - if (((StringParam) theParameter).isContains()) { - likeExpression = createLeftAndRightMatchLikeExpression(likeExpression); - } else { - likeExpression = createLeftMatchLikeExpression(likeExpression); - } - } else { - likeExpression = createLeftMatchLikeExpression(likeExpression); - } - } else { - likeExpression = createLeftMatchLikeExpression(likeExpression); - } - - Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); - if (theParameter instanceof StringParam && ((StringParam) theParameter).isExact()) { - Predicate exactCode = theBuilder.equal(theFrom.get("myValueExact"), rawSearchTerm); - singleCode = theBuilder.and(singleCode, exactCode); - } - - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } - - boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact(); - if (exactMatch) { - // Exact match - Long hash = ResourceIndexedSearchParamString.calculateHashExact(theResourceName, theParamName, rawSearchTerm); - return theBuilder.equal(theFrom.get("myHashExact").as(Long.class), hash); - } else { - // Normalized Match - String normalizedString = StringNormalizer.normalizeString(rawSearchTerm); - String likeExpression; - if ((theParameter instanceof StringParam) && - (((((StringParam) theParameter).isContains()) && - (myCallingDao.getConfig().isAllowContainsSearches())) || - (operation == SearchFilterParser.CompareOperation.co))) { - likeExpression = createLeftAndRightMatchLikeExpression(normalizedString); - } else if ((operation != SearchFilterParser.CompareOperation.ne) && - (operation != SearchFilterParser.CompareOperation.gt) && - (operation != SearchFilterParser.CompareOperation.lt) && - (operation != SearchFilterParser.CompareOperation.ge) && - (operation != SearchFilterParser.CompareOperation.le)) { - if (operation == SearchFilterParser.CompareOperation.ew) { - likeExpression = createRightMatchLikeExpression(normalizedString); - } else { - likeExpression = createLeftMatchLikeExpression(normalizedString); - } - } else { - likeExpression = normalizedString; - } - - Predicate predicate; - if ((operation == null) || - (operation == SearchFilterParser.CompareOperation.sw)) { - Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig.getModelConfig(), theResourceName, theParamName, normalizedString); - Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash); - Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = theBuilder.and(hashCode, singleCode); - } else if ((operation == SearchFilterParser.CompareOperation.ew) || - (operation == SearchFilterParser.CompareOperation.co)) { - Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } else if (operation == SearchFilterParser.CompareOperation.eq) { - Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig.getModelConfig(), theResourceName, theParamName, normalizedString); - Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash); - Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), normalizedString); - predicate = theBuilder.and(hashCode, singleCode); - } else if (operation == SearchFilterParser.CompareOperation.ne) { - Predicate singleCode = theBuilder.notEqual(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } else if (operation == SearchFilterParser.CompareOperation.gt) { - Predicate singleCode = theBuilder.greaterThan(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } else if (operation == SearchFilterParser.CompareOperation.lt) { - Predicate singleCode = theBuilder.lessThan(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } else if (operation == SearchFilterParser.CompareOperation.ge) { - Predicate singleCode = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } else if (operation == SearchFilterParser.CompareOperation.le) { - Predicate singleCode = theBuilder.lessThanOrEqualTo(theFrom.get("myValueNormalized").as(String.class), likeExpression); - predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } else { - throw new IllegalArgumentException("Don't yet know how to handle operation " + operation + " on a string"); - } - - return predicate; - } - } - - private Predicate createPredicateTagList(Path theDefJoin, CriteriaBuilder theBuilder, TagTypeEnum theTagType, List> theTokens) { - Predicate typePredicate = theBuilder.equal(theDefJoin.get("myTagType"), theTagType); - - List orPredicates = Lists.newArrayList(); - for (Pair next : theTokens) { - Predicate codePredicate = theBuilder.equal(theDefJoin.get("myCode"), next.getRight()); - if (isNotBlank(next.getLeft())) { - Predicate systemPredicate = theBuilder.equal(theDefJoin.get("mySystem"), next.getLeft()); - orPredicates.add(theBuilder.and(typePredicate, systemPredicate, codePredicate)); - } else { - orPredicates.add(theBuilder.and(typePredicate, codePredicate)); - } - } - - return theBuilder.or(toArray(orPredicates)); - } - - private Collection createPredicateToken(Collection theParameters, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom) { - return createPredicateToken( - theParameters, - theResourceName, - theParamName, - theBuilder, - theFrom, - null); - } - - private Collection createPredicateToken(Collection theParameters, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom, - SearchFilterParser.CompareOperation operation) { - final List codes = new ArrayList<>(); - - TokenParamModifier modifier = null; - for (IQueryParameterType nextParameter : theParameters) { - - String code; - String system; - if (nextParameter instanceof TokenParam) { - TokenParam id = (TokenParam) nextParameter; - system = id.getSystem(); - code = (id.getValue()); - modifier = id.getModifier(); - } else if (nextParameter instanceof BaseIdentifierDt) { - BaseIdentifierDt id = (BaseIdentifierDt) nextParameter; - system = id.getSystemElement().getValueAsString(); - code = (id.getValueElement().getValue()); - } else if (nextParameter instanceof BaseCodingDt) { - BaseCodingDt id = (BaseCodingDt) nextParameter; - system = id.getSystemElement().getValueAsString(); - code = (id.getCodeElement().getValue()); - } else if (nextParameter instanceof NumberParam) { - NumberParam number = (NumberParam) nextParameter; - system = null; - code = number.getValueAsQueryToken(myContext); - } else { - throw new IllegalArgumentException("Invalid token type: " + nextParameter.getClass()); - } - - if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { - throw new InvalidRequestException( - "Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); - } - - if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { - throw new InvalidRequestException( - "Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code); - } - - /* - * Process token modifiers (:in, :below, :above) - */ - - if (modifier == TokenParamModifier.IN) { - codes.addAll(myTerminologySvc.expandValueSet(code)); - } else if (modifier == TokenParamModifier.ABOVE) { - system = determineSystemIfMissing(theParamName, code, system); - validateHaveSystemAndCodeForToken(theParamName, code, system); - codes.addAll(myTerminologySvc.findCodesAbove(system, code)); - } else if (modifier == TokenParamModifier.BELOW) { - system = determineSystemIfMissing(theParamName, code, system); - validateHaveSystemAndCodeForToken(theParamName, code, system); - codes.addAll(myTerminologySvc.findCodesBelow(system, code)); - } else { - codes.add(new VersionIndependentConcept(system, code)); - } - - } - - List sortedCodesList = codes - .stream() - .filter(t -> t.getCode() != null || t.getSystem() != null) - .sorted() - .distinct() - .collect(Collectors.toList()); - - if (codes.isEmpty()) { - // This will never match anything - return Collections.singletonList(new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false)); - } - - List retVal = new ArrayList<>(); - - // System only - List systemOnlyCodes = sortedCodesList.stream().filter(t -> isBlank(t.getCode())).collect(Collectors.toList()); - if (!systemOnlyCodes.isEmpty()) { - retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemOnlyCodes, modifier, TokenModeEnum.SYSTEM_ONLY)); - } - - // Code only - List codeOnlyCodes = sortedCodesList.stream().filter(t -> t.getSystem() == null).collect(Collectors.toList()); - if (!codeOnlyCodes.isEmpty()) { - retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, codeOnlyCodes, modifier, TokenModeEnum.VALUE_ONLY)); - } - - // System and code - List systemAndCodeCodes = sortedCodesList.stream().filter(t -> isNotBlank(t.getCode()) && t.getSystem() != null).collect(Collectors.toList()); - if (!systemAndCodeCodes.isEmpty()) { - retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemAndCodeCodes, modifier, TokenModeEnum.SYSTEM_AND_VALUE)); - } - - return retVal; - } - - private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) { - String systemDesc = defaultIfBlank(theSystem, "(missing)"); - String codeDesc = defaultIfBlank(theCode, "(missing)"); - if (isBlank(theCode)) { - String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc); - throw new InvalidRequestException(msg); - } - if (isBlank(theSystem)) { - String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc); - throw new InvalidRequestException(msg); - } - } - - private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From theFrom, List theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) { - if (myDontUseHashesForSearch) { - final Path systemExpression = theFrom.get("mySystem"); - final Path valueExpression = theFrom.get("myValue"); - - List orPredicates = new ArrayList<>(); - switch (theTokenMode) { - case SYSTEM_ONLY: { - List systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList()); - Predicate orPredicate = systemExpression.in(systems); - orPredicates.add(orPredicate); - break; - } - case VALUE_ONLY: - List codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList()); - Predicate orPredicate = valueExpression.in(codes); - orPredicates.add(orPredicate); - break; - case SYSTEM_AND_VALUE: - for (VersionIndependentConcept next : theTokens) { - orPredicates.add(theBuilder.and( - toEqualOrIsNullPredicate(systemExpression, next.getSystem()), - toEqualOrIsNullPredicate(valueExpression, next.getCode()) - )); - } - break; - } - - Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0])); - if (theModifier == TokenParamModifier.NOT) { - or = theBuilder.not(or); - } - - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or); - } - - /* - * Note: A null system value means "match any system", but - * an empty-string system value means "match values that - * explicitly have no system". - */ - Expression hashField; - List values; - switch (theTokenMode) { - case SYSTEM_ONLY: - hashField = theFrom.get("myHashSystem").as(Long.class); - values = theTokens - .stream() - .map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, t.getSystem())) - .collect(Collectors.toList()); - break; - case VALUE_ONLY: - hashField = theFrom.get("myHashValue").as(Long.class); - values = theTokens - .stream() - .map(t -> ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, t.getCode())) - .collect(Collectors.toList()); - break; - case SYSTEM_AND_VALUE: - default: - hashField = theFrom.get("myHashSystemAndValue").as(Long.class); - values = theTokens - .stream() - .map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, t.getSystem(), t.getCode())) - .collect(Collectors.toList()); - break; - } - - Predicate predicate = hashField.in(values); - if (theModifier == TokenParamModifier.NOT) { - Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName)); - Predicate disjunctionPredicate = theBuilder.not(predicate); - predicate = theBuilder.and(identityPredicate, disjunctionPredicate); - } - return predicate; - } - - private Expression toEqualOrIsNullPredicate(Path theExpression, T theCode) { - if (theCode == null) { - return myBuilder.isNull(theExpression); - } - return myBuilder.equal(theExpression, theCode); - } - - private Predicate createPredicateCoords(IQueryParameterType theParam, - String theResourceName, - String theParamName, - CriteriaBuilder theBuilder, - From theFrom) { - String latitudeValue; - String longitudeValue; - Double distanceKm = 0.0; - - if (theParam instanceof TokenParam) { // DSTU3 - TokenParam param = (TokenParam) theParam; - String value = param.getValue(); - String[] parts = value.split(":"); - if (parts.length != 2) { - throw new IllegalArgumentException("Invalid position format '" + value + "'. Required format is 'latitude:longitude'"); - } - latitudeValue = parts[0]; - longitudeValue = parts[1]; - if (isBlank(latitudeValue) || isBlank(longitudeValue)) { - throw new IllegalArgumentException("Invalid position format '" + value + "'. Both latitude and longitude must be provided."); - } - QuantityParam distanceParam = myParams.getNearDistanceParam(); - if (distanceParam != null) { - distanceKm = distanceParam.getValue().doubleValue(); - } - } else if (theParam instanceof SpecialParam) { // R4 - SpecialParam param = (SpecialParam) theParam; - String value = param.getValue(); - String[] parts = value.split("\\|"); - if (parts.length < 2 || parts.length > 4) { - throw new IllegalArgumentException("Invalid position format '" + value + "'. Required format is 'latitude|longitude' or 'latitude|longitude|distance' or 'latitude|longitude|distance|units'"); - } - latitudeValue = parts[0]; - longitudeValue = parts[1]; - if (isBlank(latitudeValue) || isBlank(longitudeValue)) { - throw new IllegalArgumentException("Invalid position format '" + value + "'. Both latitude and longitude must be provided."); - } - if (parts.length >= 3) { - String distanceString = parts[2]; - if (!isBlank(distanceString)) { - distanceKm = Double.valueOf(distanceString); - } - } - } else { - throw new IllegalArgumentException("Invalid position type: " + theParam.getClass()); - } - - Predicate latitudePredicate; - Predicate longitudePredicate; - if (distanceKm == 0.0) { - latitudePredicate = theBuilder.equal(theFrom.get("myLatitude"), latitudeValue); - longitudePredicate = theBuilder.equal(theFrom.get("myLongitude"), longitudeValue); - } else if (distanceKm < 0.0) { - throw new IllegalArgumentException("Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be >= 0.0"); - } else { - Double latitudeDegrees = Double.valueOf(latitudeValue); - Double longitudeDegrees = Double.valueOf(longitudeValue); - - Point northPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 0.0, distanceKm); - Point eastPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 90.0, distanceKm); - Point southPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 180.0, distanceKm); - Point westPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 270.0, distanceKm); - - latitudePredicate = theBuilder.and( - theBuilder.greaterThanOrEqualTo(theFrom.get("myLatitude"), southPoint.getLatitude()), - theBuilder.lessThanOrEqualTo(theFrom.get("myLatitude"), northPoint.getLatitude()) - ); - longitudePredicate = theBuilder.and( - theBuilder.greaterThanOrEqualTo(theFrom.get("myLongitude"), westPoint.getLongitude()), - theBuilder.lessThanOrEqualTo(theFrom.get("myLongitude"), eastPoint.getLongitude()) - ); - } - Predicate singleCode = theBuilder.and(latitudePredicate, longitudePredicate); - return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); - } @Override public Iterator createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest) { @@ -2275,11 +1192,6 @@ public class SearchBuilder implements ISearchBuilder { return query; } - private Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From from) { - return createResourceLinkPathPredicate(myContext, theParamName, from, theResourceName); - } - - /** * @return Returns {@literal true} if any search parameter sorts were found, or false if * no sorts were found, or only non-search parameters ones (e.g. _id, _lastUpdated) @@ -2320,43 +1232,43 @@ public class SearchBuilder implements ISearchBuilder { String joinAttrName; String[] sortAttrName; - JoinEnum joinType; + SearchBuilderJoinEnum joinType; switch (param.getParamType()) { case STRING: joinAttrName = "myParamsString"; sortAttrName = new String[]{"myValueExact"}; - joinType = JoinEnum.STRING; + joinType = SearchBuilderJoinEnum.STRING; break; case DATE: joinAttrName = "myParamsDate"; sortAttrName = new String[]{"myValueLow"}; - joinType = JoinEnum.DATE; + joinType = SearchBuilderJoinEnum.DATE; break; case REFERENCE: joinAttrName = "myResourceLinks"; sortAttrName = new String[]{"myTargetResourcePid"}; - joinType = JoinEnum.REFERENCE; + joinType = SearchBuilderJoinEnum.REFERENCE; break; case TOKEN: joinAttrName = "myParamsToken"; sortAttrName = new String[]{"mySystem", "myValue"}; - joinType = JoinEnum.TOKEN; + joinType = SearchBuilderJoinEnum.TOKEN; break; case NUMBER: joinAttrName = "myParamsNumber"; sortAttrName = new String[]{"myValue"}; - joinType = JoinEnum.NUMBER; + joinType = SearchBuilderJoinEnum.NUMBER; break; case URI: joinAttrName = "myParamsUri"; sortAttrName = new String[]{"myUri"}; - joinType = JoinEnum.URI; + joinType = SearchBuilderJoinEnum.URI; break; case QUANTITY: joinAttrName = "myParamsQuantity"; sortAttrName = new String[]{"myValue"}; - joinType = JoinEnum.QUANTITY; + joinType = SearchBuilderJoinEnum.QUANTITY; break; case SPECIAL: case COMPOSITE: @@ -2369,7 +1281,7 @@ public class SearchBuilder implements ISearchBuilder { * If we've already got a join for the specific parameter we're * sorting on, we'll also sort with it. Otherwise we need a new join. */ - JoinKey key = new JoinKey(theSort.getParamName(), joinType); + SearchBuilderJoinKey key = new SearchBuilderJoinKey(theSort.getParamName(), joinType); Join join = myIndexJoins.get(key); if (join == null) { join = theFrom.join(joinAttrName, JoinType.LEFT); @@ -2403,36 +1315,6 @@ public class SearchBuilder implements ISearchBuilder { return true; } - private String determineSystemIfMissing(String theParamName, String code, String theSystem) { - String retVal = theSystem; - if (retVal == null) { - RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName); - RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); - if (param != null) { - Set valueSetUris = Sets.newHashSet(); - for (String nextPath : param.getPathsSplit()) { - BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, nextPath); - if (def instanceof BaseRuntimeDeclaredChildDefinition) { - String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet(); - if (isNotBlank(valueSet)) { - valueSetUris.add(valueSet); - } - } - } - if (valueSetUris.size() == 1) { - String valueSet = valueSetUris.iterator().next(); - List candidateCodes = myTerminologySvc.expandValueSet(valueSet); - for (VersionIndependentConcept nextCandidate : candidateCodes) { - if (nextCandidate.getCode().equals(code)) { - retVal = nextCandidate.getSystem(); - break; - } - } - } - } - } - return retVal; - } private void doLoadPids(Collection thePids, Collection theIncludedPids, List theResourceListToPopulate, boolean theForHistoryOperation, Map thePosition, RequestDetails theRequest) { @@ -2741,33 +1623,6 @@ public class SearchBuilder implements ISearchBuilder { } } - private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams, RequestDetails theRequest) { - myParams = theParams; - - // Remove any empty parameters - theParams.clean(); - - /* - * 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) { - attemptCompositeUniqueSpProcessing(theParams, theRequest); - } - - // Handle each parameter - for (Entry>> nextParamEntry : myParams.entrySet()) { - String nextParamName = nextParamEntry.getKey(); - List> andOrParams = nextParamEntry.getValue(); - searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams, theRequest); - } - - } - private void attemptCompositeUniqueSpProcessing(@Nonnull SearchParameterMap theParams, RequestDetails theRequest) { // Since we're going to remove elements below theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList)); @@ -2984,138 +1839,12 @@ public class SearchBuilder implements ISearchBuilder { return null; } - private void searchForIdsWithAndOr(String theResourceName, String theParamName, List> theAndOrParams, RequestDetails theRequest) { - - if (theAndOrParams.isEmpty()) { - return; - } - - switch (theParamName) { - case IAnyResource.SP_RES_ID: - addPredicateResourceId(theResourceName, theAndOrParams, theRequest); - break; - - case IAnyResource.SP_RES_LANGUAGE: - addPredicateLanguage(theAndOrParams); - break; - - case Constants.PARAM_HAS: - addPredicateHas(theAndOrParams, theRequest); - break; - - case Constants.PARAM_TAG: - case Constants.PARAM_PROFILE: - case Constants.PARAM_SECURITY: - addPredicateTag(theAndOrParams, theParamName); - break; - - case Constants.PARAM_SOURCE: - addPredicateSource(theAndOrParams, theRequest); - break; - - default: - - RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); - if (nextParamDef != null) { - switch (nextParamDef.getParamType()) { - case DATE: - for (List nextAnd : theAndOrParams) { - addPredicateDate(theResourceName, theParamName, nextAnd); - } - break; - case QUANTITY: - for (List nextAnd : theAndOrParams) { - addPredicateQuantity(theResourceName, theParamName, nextAnd); - } - break; - case REFERENCE: - for (List nextAnd : theAndOrParams) { - addPredicateReference(theResourceName, theParamName, nextAnd, theRequest); - } - break; - case STRING: - for (List nextAnd : theAndOrParams) { - addPredicateString(theResourceName, theParamName, nextAnd); - } - break; - case TOKEN: - for (List nextAnd : theAndOrParams) { - if ("Location.position".equals(nextParamDef.getPath())) { - addPredicateCoords(theResourceName, theParamName, nextAnd); - } else { - addPredicateToken(theResourceName, theParamName, nextAnd); - } - } - break; - case NUMBER: - for (List nextAnd : theAndOrParams) { - addPredicateNumber(theResourceName, theParamName, nextAnd); - } - break; - case COMPOSITE: - for (List nextAnd : theAndOrParams) { - addPredicateComposite(theResourceName, nextParamDef, nextAnd); - } - break; - case URI: - for (List nextAnd : theAndOrParams) { - addPredicateUri(theResourceName, theParamName, nextAnd); - } - break; - case HAS: - case SPECIAL: - for (List nextAnd : theAndOrParams) { - if ("Location.position".equals(nextParamDef.getPath())) { - addPredicateCoords(theResourceName, theParamName, nextAnd); - } - } - break; - } - } else { - if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) { - // These are handled later - } else if (Constants.PARAM_FILTER.equals(theParamName)) { - // Parse the predicates enumerated in the _filter separated by AND or OR... - if (theAndOrParams.get(0).get(0) instanceof StringParam) { - String filterString = ((StringParam) theAndOrParams.get(0).get(0)).getValue(); - SearchFilterParser.Filter filter; - try { - filter = SearchFilterParser.parse(filterString); - } catch (SearchFilterParser.FilterSyntaxException theE) { - throw new InvalidRequestException("Error parsing _filter syntax: " + theE.getMessage()); - } - if (filter != null) { - - if (!myDaoConfig.isFilterParameterEnabled()) { - throw new InvalidRequestException(Constants.PARAM_FILTER + " parameter is disabled on this server"); - } - - // TODO: we clear the predicates below because the filter builds up - // its own collection of predicates. It'd probably be good at some - // point to do something more fancy... - ArrayList holdPredicates = new ArrayList<>(myPredicates); - - Predicate filterPredicate = processFilter(filter, theResourceName, theRequest); - myPredicates.clear(); - myPredicates.addAll(holdPredicates); - myPredicates.add(filterPredicate); - } - } - - - } else { - throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName); - } - } - break; - } - } - @Override public void setFetchSize(int theFetchSize) { myFetchSize = theFetchSize; } + // FIXME KHS move this into constructor and make these final @Override public void setType(Class theResourceType, String theResourceName) { myResourceType = theResourceType; @@ -3168,35 +1897,12 @@ public class SearchBuilder implements ISearchBuilder { return qp; } - private Predicate createResourceLinkPathPredicate(FhirContext theContext, String theParamName, From theFrom, - String theResourceType) { - RuntimeResourceDefinition resourceDef = theContext.getResourceDefinition(theResourceType); - RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); - List path = param.getPathsSplit(); - - /* - * SearchParameters can declare paths on multiple resource - * types. Here we only want the ones that actually apply. - */ - path = new ArrayList<>(path); - - ListIterator iter = path.listIterator(); - while (iter.hasNext()) { - String nextPath = trim(iter.next()); - if (!nextPath.contains(theResourceType + ".")) { - iter.remove(); - } - } - - return theFrom.get("mySourcePath").in(path); - } - @VisibleForTesting void setParamsForUnitTest(SearchParameterMap theParams) { myParams = theParams; } - SearchParameterMap getParams() { + public SearchParameterMap getParams() { return myParams; } @@ -3205,28 +1911,46 @@ public class SearchBuilder implements ISearchBuilder { myEntityManager = theEntityManager; } - private enum TokenModeEnum { - SYSTEM_ONLY, - VALUE_ONLY, - SYSTEM_AND_VALUE + public CriteriaBuilder getBuilder() { + return myBuilder; + } + + public Root getResourceTableRoot() { + return myResourceTableRoot; + } + + public IndexJoins getIndexJoins() { + return myIndexJoins; + } + + public ArrayList getPredicates() { + return myPredicates; + } + + public AbstractQuery getResourceTableQuery() { + return myResourceTableQuery; + } + + public Class getResourceType() { + return myResourceType; + } + + public String getResourceName() { + return myResourceName; + } + + public PredicateBuilderString getPredicateBuilderString() { + return myPredicateBuilderString; + } + + public BaseHapiFhirDao getCallingDao() { + return myCallingDao; } public enum HandlerTypeEnum { UNIQUE_INDEX, STANDARD_QUERY } - private enum JoinEnum { - DATE, - NUMBER, - QUANTITY, - REFERENCE, - STRING, - TOKEN, - URI, - COORDS - - } - public class IncludesIterator extends BaseIterator implements Iterator { private final RequestDetails myRequest; @@ -3486,63 +2210,6 @@ public class SearchBuilder implements ISearchBuilder { } } - private static class JoinKey { - private final JoinEnum myJoinType; - private final String myParamName; - - JoinKey(String theParamName, JoinEnum theJoinType) { - super(); - myParamName = theParamName; - myJoinType = theJoinType; - } - - @Override - public boolean equals(Object theObj) { - if (!(theObj instanceof JoinKey)) { - return false; - } - JoinKey obj = (JoinKey) theObj; - return new EqualsBuilder() - .append(myParamName, obj.myParamName) - .append(myJoinType, obj.myJoinType) - .isEquals(); - } - - @Override - public int hashCode() { - return new HashCodeBuilder() - .append(myParamName) - .append(myJoinType) - .toHashCode(); - } - } - - private static String createRightMatchLikeExpression(String likeExpression) { - return "%" + likeExpression.replace("%", "[%]"); - } - - /** - * Figures out the tolerance for a search. For example, if the user is searching for 4.00, this method - * returns 0.005 because we shold actually match values which are - * 4 (+/-) 0.005 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 createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From from) { List lastUpdatedPredicates = new ArrayList<>(); if (theLastUpdated != null) { @@ -3559,14 +2226,6 @@ public class SearchBuilder implements ISearchBuilder { return lastUpdatedPredicates; } - private static String createLeftAndRightMatchLikeExpression(String likeExpression) { - return "%" + likeExpression.replace("%", "[%]") + "%"; - } - - private static String createLeftMatchLikeExpression(String likeExpression) { - return likeExpression.replace("%", "[%]") + "%"; - } - private static List filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection thePids) { if (thePids.isEmpty()) { return Collections.emptyList(); 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 new file mode 100644 index 00000000000..86a7281d916 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/BasePredicateBuilder.java @@ -0,0 +1,229 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.PostConstruct; +import javax.persistence.criteria.*; +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.ArrayList; +import java.util.List; + +abstract class BasePredicateBuilder { + private static final Logger ourLog = LoggerFactory.getLogger(BasePredicateBuilder.class); + @Autowired + FhirContext myContext; + @Autowired + DaoConfig myDaoConfig; + + boolean myDontUseHashesForSearch; + final BaseHapiFhirDao myCallingDao; + final CriteriaBuilder myBuilder; + final Root myResourceTableRoot; + final IndexJoins myIndexJoins; + final ArrayList myPredicates; + final Class myResourceType; + final String myResourceName; + final AbstractQuery myResourceTableQuery; + final SearchParameterMap myParams; + + // FIXME KHS autowire with lookup + BasePredicateBuilder(SearchBuilder theSearchBuilder) { + myCallingDao = theSearchBuilder.getCallingDao(); + myBuilder = theSearchBuilder.getBuilder(); + myResourceTableRoot = theSearchBuilder.getResourceTableRoot(); + myIndexJoins = theSearchBuilder.getIndexJoins(); + myPredicates = theSearchBuilder.getPredicates(); + myResourceType = theSearchBuilder.getResourceType(); + myResourceName = theSearchBuilder.getResourceName(); + myResourceTableQuery = theSearchBuilder.getResourceTableQuery(); + myParams = theSearchBuilder.getParams(); + } + + @PostConstruct + private void postConstruct() { + myDontUseHashesForSearch = myDaoConfig.getDisableHashBasedSearches(); + } + + @SuppressWarnings("unchecked") + Join createJoin(SearchBuilderJoinEnum theType, String theSearchParameterName) { + Join join = null; + switch (theType) { + case DATE: + join = myResourceTableRoot.join("myParamsDate", JoinType.LEFT); + break; + case NUMBER: + join = myResourceTableRoot.join("myParamsNumber", JoinType.LEFT); + break; + case QUANTITY: + join = myResourceTableRoot.join("myParamsQuantity", JoinType.LEFT); + break; + case REFERENCE: + join = myResourceTableRoot.join("myResourceLinks", JoinType.LEFT); + break; + case STRING: + join = myResourceTableRoot.join("myParamsString", JoinType.LEFT); + break; + case URI: + join = myResourceTableRoot.join("myParamsUri", JoinType.LEFT); + break; + case TOKEN: + join = myResourceTableRoot.join("myParamsToken", JoinType.LEFT); + break; + case COORDS: + join = myResourceTableRoot.join("myParamsCoords", JoinType.LEFT); + break; + } + + SearchBuilderJoinKey key = new SearchBuilderJoinKey(theSearchParameterName, theType); + myIndexJoins.put(key, join); + + return (Join) join; + } + + void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) { +// if (myDontUseHashesForSearch) { +// Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); +// Join paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT); +// +// myPredicates.add(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName)); +// myPredicates.add(myBuilder.equal(paramJoin.get("myParamName"), theParamName)); +// myPredicates.add(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing)); +// } + + Join paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT); + + Expression hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); + Long hash = SearchParamPresent.calculateHashPresence(theResourceName, theParamName, !theMissing); + myPredicates.add(myBuilder.equal(hashPresence, hash)); + } + + void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing, Join theJoin) { + + myPredicates.add(myBuilder.equal(theJoin.get("myResourceType"), theResourceName)); + myPredicates.add(myBuilder.equal(theJoin.get("myParamName"), theParamName)); + myPredicates.add(myBuilder.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); + return outerPredicate; + } + + long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); + Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); + return myBuilder.and(hashIdentityPredicate, thePredicate); + } + + + Predicate createPredicateNumeric(String theResourceName, + String theParamName, + From theFrom, + CriteriaBuilder builder, + IQueryParameterType theParam, + ParamPrefixEnum thePrefix, + BigDecimal theValue, + final Expression thePath, + String invalidMessageName) { + Predicate num; + // Per discussions with Grahame Grieve and James Agnew on 11/13/19, modified logic for EQUAL and NOT_EQUAL operators below so as to + // use exact value matching. The "fuzz amount" matching is still used with the APPROXIMATE operator. + switch (thePrefix) { + case GREATERTHAN: + num = builder.gt(thePath, theValue); + break; + case GREATERTHAN_OR_EQUALS: + num = builder.ge(thePath, theValue); + break; + case LESSTHAN: + num = builder.lt(thePath, theValue); + break; + case LESSTHAN_OR_EQUALS: + num = builder.le(thePath, theValue); + break; + case EQUAL: + num = builder.equal(thePath, theValue); + break; + case NOT_EQUAL: + num = builder.notEqual(thePath, theValue); + break; + case APPROXIMATE: + BigDecimal mul = calculateFuzzAmount(thePrefix, theValue); + BigDecimal low = theValue.subtract(mul, MathContext.DECIMAL64); + BigDecimal high = theValue.add(mul, MathContext.DECIMAL64); + Predicate lowPred; + Predicate highPred; + lowPred = builder.ge(thePath.as(BigDecimal.class), low); + highPred = builder.le(thePath.as(BigDecimal.class), high); + num = builder.and(lowPred, highPred); + ourLog.trace("Searching for {} <= val <= {}", low, high); + break; + case ENDS_BEFORE: + case STARTS_AFTER: + default: + String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, invalidMessageName, thePrefix.getValue(), theParam.getValueAsQueryToken(myContext)); + throw new InvalidRequestException(msg); + } + + if (theParamName == null) { + return num; + } + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num); + } + + /** + * Figures out the tolerance for a search. For example, if the user is searching for 4.00, this method + * returns 0.005 because we shold actually match values which are + * 4 (+/-) 0.005 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); + } + } + + static String createLeftAndRightMatchLikeExpression(String likeExpression) { + return "%" + likeExpression.replace("%", "[%]") + "%"; + } + + static String createLeftMatchLikeExpression(String likeExpression) { + return likeExpression.replace("%", "[%]") + "%"; + } + + static String createRightMatchLikeExpression(String likeExpression) { + return "%" + likeExpression.replace("%", "[%]"); + } + + static Predicate[] toArray(List thePredicates) { + return thePredicates.toArray(new Predicate[0]); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java new file mode 100644 index 00000000000..75179133350 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/IndexJoins.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import com.google.common.collect.Maps; + +import javax.persistence.criteria.Join; +import java.util.Map; + +public class IndexJoins { + Map> myIndexJoins = Maps.newHashMap(); + + public void put(SearchBuilderJoinKey theKey, Join theJoin) { + myIndexJoins.put(theKey, theJoin); + } + + public boolean haveIndexJoins() { + return !myIndexJoins.isEmpty(); + } + + public Join get(SearchBuilderJoinKey theKey) { + myIndexJoins.get(theKey); + } +} 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 new file mode 100644 index 00000000000..a04b2f65b82 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderCoords.java @@ -0,0 +1,135 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.util.CoordCalculator; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.dstu2.resource.Location; +import ca.uhn.fhir.rest.param.QuantityParam; +import ca.uhn.fhir.rest.param.SpecialParam; +import ca.uhn.fhir.rest.param.TokenParam; +import org.hibernate.search.spatial.impl.Point; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class PredicateBuilderCoords extends BasePredicateBuilder { + + PredicateBuilderCoords(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + private Predicate createPredicateCoords(IQueryParameterType theParam, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom) { + String latitudeValue; + String longitudeValue; + Double distanceKm = 0.0; + + if (theParam instanceof TokenParam) { // DSTU3 + TokenParam param = (TokenParam) theParam; + String value = param.getValue(); + String[] parts = value.split(":"); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid position format '" + value + "'. Required format is 'latitude:longitude'"); + } + latitudeValue = parts[0]; + longitudeValue = parts[1]; + if (isBlank(latitudeValue) || isBlank(longitudeValue)) { + throw new IllegalArgumentException("Invalid position format '" + value + "'. Both latitude and longitude must be provided."); + } + QuantityParam distanceParam = myParams.getNearDistanceParam(); + if (distanceParam != null) { + distanceKm = distanceParam.getValue().doubleValue(); + } + } else if (theParam instanceof SpecialParam) { // R4 + SpecialParam param = (SpecialParam) theParam; + String value = param.getValue(); + String[] parts = value.split("\\|"); + if (parts.length < 2 || parts.length > 4) { + throw new IllegalArgumentException("Invalid position format '" + value + "'. Required format is 'latitude|longitude' or 'latitude|longitude|distance' or 'latitude|longitude|distance|units'"); + } + latitudeValue = parts[0]; + longitudeValue = parts[1]; + if (isBlank(latitudeValue) || isBlank(longitudeValue)) { + throw new IllegalArgumentException("Invalid position format '" + value + "'. Both latitude and longitude must be provided."); + } + if (parts.length >= 3) { + String distanceString = parts[2]; + if (!isBlank(distanceString)) { + distanceKm = Double.valueOf(distanceString); + } + } + } else { + throw new IllegalArgumentException("Invalid position type: " + theParam.getClass()); + } + + Predicate latitudePredicate; + Predicate longitudePredicate; + if (distanceKm == 0.0) { + latitudePredicate = theBuilder.equal(theFrom.get("myLatitude"), latitudeValue); + longitudePredicate = theBuilder.equal(theFrom.get("myLongitude"), longitudeValue); + } else if (distanceKm < 0.0) { + throw new IllegalArgumentException("Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be >= 0.0"); + } else { + Double latitudeDegrees = Double.valueOf(latitudeValue); + Double longitudeDegrees = Double.valueOf(longitudeValue); + + Point northPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 0.0, distanceKm); + Point eastPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 90.0, distanceKm); + Point southPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 180.0, distanceKm); + Point westPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 270.0, distanceKm); + + latitudePredicate = theBuilder.and( + theBuilder.greaterThanOrEqualTo(theFrom.get("myLatitude"), southPoint.getLatitude()), + theBuilder.lessThanOrEqualTo(theFrom.get("myLatitude"), northPoint.getLatitude()) + ); + longitudePredicate = theBuilder.and( + theBuilder.greaterThanOrEqualTo(theFrom.get("myLongitude"), westPoint.getLongitude()), + theBuilder.lessThanOrEqualTo(theFrom.get("myLongitude"), eastPoint.getLongitude()) + ); + } + Predicate singleCode = theBuilder.and(latitudePredicate, longitudePredicate); + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } + + public Predicate addPredicateCoords(String theResourceName, + String theParamName, + List theList) { + Join join = createJoin(SearchBuilderJoinEnum.COORDS, theParamName); + + if (theList.get(0).getMissing() != null) { + addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); + return null; + } + + List codePredicates = new ArrayList(); + for (IQueryParameterType nextOr : theList) { + + Predicate singleCode = createPredicateCoords(nextOr, + theResourceName, + theParamName, + myBuilder, + join + ); + codePredicates.add(singleCode); + } + + Predicate retVal = myBuilder.or(toArray(codePredicates)); + myPredicates.add(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 new file mode 100644 index 00000000000..cf0e6fa4bbc --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderDate.java @@ -0,0 +1,193 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchFilterParser; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class PredicateBuilderDate extends BasePredicateBuilder { + private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderDate.class); + + PredicateBuilderDate(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + public Predicate addPredicateDate(String theResourceName, + String theParamName, + List theList, + SearchFilterParser.CompareOperation operation) { + + Join join = createJoin(SearchBuilderJoinEnum.DATE, theParamName); + + if (theList.get(0).getMissing() != null) { + Boolean missing = theList.get(0).getMissing(); + addPredicateParamMissing(theResourceName, theParamName, missing, join); + return null; + } + + List codePredicates = new ArrayList<>(); + for (IQueryParameterType nextOr : theList) { + IQueryParameterType params = nextOr; + Predicate p = createPredicateDate(params, + theResourceName, + theParamName, + myBuilder, + join, + operation); + codePredicates.add(p); + } + + Predicate orPredicates = myBuilder.or(toArray(codePredicates)); + myPredicates.add(orPredicates); + return orPredicates; + } + + + + public Predicate createPredicateDate(IQueryParameterType theParam, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom) { + return createPredicateDate(theParam, + theResourceName, + theParamName, + theBuilder, + theFrom, + null); + } + + private Predicate createPredicateDate(IQueryParameterType theParam, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom, + SearchFilterParser.CompareOperation operation) { + + Predicate p; + if (theParam instanceof DateParam) { + DateParam date = (DateParam) theParam; + if (!date.isEmpty()) { + DateRangeParam range = new DateRangeParam(date); + p = createPredicateDateFromRange(theBuilder, + theFrom, + range, + operation); + } else { + // TODO: handle missing date param? + p = null; + } + } else if (theParam instanceof DateRangeParam) { + DateRangeParam range = (DateRangeParam) theParam; + p = createPredicateDateFromRange(theBuilder, + theFrom, + range, + operation); + } else { + throw new IllegalArgumentException("Invalid token type: " + theParam.getClass()); + } + + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, p); + } + + private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder, + From theFrom, + DateRangeParam theRange, + SearchFilterParser.CompareOperation operation) { + Date lowerBound = theRange.getLowerBoundAsInstant(); + Date upperBound = theRange.getUpperBoundAsInstant(); + Predicate lt = null; + Predicate gt = null; + Predicate lb = null; + Predicate ub = null; + + if (operation == SearchFilterParser.CompareOperation.lt) { + if (lowerBound == null) { + throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); + } + lb = theBuilder.lessThan(theFrom.get("myValueLow"), lowerBound); + } else if (operation == SearchFilterParser.CompareOperation.le) { + if (upperBound == null) { + throw new InvalidRequestException("upperBound value not correctly specified for compare operation"); + } + lb = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); + } else if (operation == SearchFilterParser.CompareOperation.gt) { + if (upperBound == null) { + throw new InvalidRequestException("upperBound value not correctly specified for compare operation"); + } + lb = theBuilder.greaterThan(theFrom.get("myValueHigh"), upperBound); + } else if (operation == SearchFilterParser.CompareOperation.ge) { + if (lowerBound == null) { + throw new InvalidRequestException("lowerBound value not correctly specified for compare operation"); + } + lb = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); + } else if (operation == SearchFilterParser.CompareOperation.ne) { + if ((lowerBound == null) || + (upperBound == null)) { + throw new InvalidRequestException("lowerBound and/or upperBound value not correctly specified for compare operation"); + } + /*Predicate*/ + lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); + /*Predicate*/ + gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); + lb = theBuilder.or(lt, + gt); + } else if ((operation == SearchFilterParser.CompareOperation.eq) || + (operation == null)) { + if (lowerBound != null) { + /*Predicate*/ + gt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueLow"), lowerBound); + /*Predicate*/ + lt = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueHigh"), lowerBound); + if (theRange.getLowerBound().getPrefix() == ParamPrefixEnum.STARTS_AFTER || theRange.getLowerBound().getPrefix() == ParamPrefixEnum.EQUAL) { + lb = gt; + } else { + lb = theBuilder.or(gt, lt); + } + } + + if (upperBound != null) { + /*Predicate*/ + gt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueLow"), upperBound); + /*Predicate*/ + lt = theBuilder.lessThanOrEqualTo(theFrom.get("myValueHigh"), upperBound); + if (theRange.getUpperBound().getPrefix() == ParamPrefixEnum.ENDS_BEFORE || theRange.getUpperBound().getPrefix() == ParamPrefixEnum.EQUAL) { + ub = lt; + } else { + ub = theBuilder.or(gt, lt); + } + } + } else { + throw new InvalidRequestException(String.format("Unsupported operator specified, operator=%s", + operation.name())); + } + + ourLog.trace("Date range is {} - {}", lowerBound, upperBound); + + if (lb != null && ub != null) { + return (theBuilder.and(lb, ub)); + } else if (lb != null) { + return (lb); + } else { + return (ub); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderFactory.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderFactory.java new file mode 100644 index 00000000000..70665dbd809 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderFactory.java @@ -0,0 +1,25 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import org.springframework.beans.factory.annotation.Lookup; +import org.springframework.stereotype.Service; + +@Service +public abstract class PredicateBuilderFactory { + @Lookup + public abstract PredicateBuilderCoords newPredicateBuilderCoords(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderDate newPredicateBuilderDate(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderNumber newPredicateBuilderNumber(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderQuantity newPredicateBuilderQuantity(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderString newPredicateBuilderString(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderTag newPredicateBuilderTag(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderToken newPredicateBuilderToken(SearchBuilder theSearchBuilder); + @Lookup + public abstract PredicateBuilderUri newPredicateBuilderUri(SearchBuilder theSearchBuilder); +} 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 new file mode 100644 index 00000000000..1da2d2d5e5d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderNumber.java @@ -0,0 +1,88 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchFilterParser; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.criteria.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + +public class PredicateBuilderNumber extends BasePredicateBuilder { + private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderNumber.class); + + PredicateBuilderNumber(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + public Predicate addPredicateNumber(String theResourceName, + String theParamName, + List theList, + SearchFilterParser.CompareOperation operation) { + + Join join = createJoin(SearchBuilderJoinEnum.NUMBER, theParamName); + + if (theList.get(0).getMissing() != null) { + addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); + return null; + } + + List codePredicates = new ArrayList<>(); + for (IQueryParameterType nextOr : theList) { + + if (nextOr instanceof NumberParam) { + NumberParam param = (NumberParam) nextOr; + + BigDecimal value = param.getValue(); + if (value == null) { + continue; + } + + final Expression fromObj = join.get("myValue"); + ParamPrefixEnum prefix = defaultIfNull(param.getPrefix(), ParamPrefixEnum.EQUAL); + if (operation == SearchFilterParser.CompareOperation.ne) { + prefix = ParamPrefixEnum.NOT_EQUAL; + } else if (operation == SearchFilterParser.CompareOperation.lt) { + prefix = ParamPrefixEnum.LESSTHAN; + } else if (operation == SearchFilterParser.CompareOperation.le) { + prefix = ParamPrefixEnum.LESSTHAN_OR_EQUALS; + } else if (operation == SearchFilterParser.CompareOperation.gt) { + prefix = ParamPrefixEnum.GREATERTHAN; + } else if (operation == SearchFilterParser.CompareOperation.ge) { + prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS; + } else if (operation == SearchFilterParser.CompareOperation.eq) { + prefix = ParamPrefixEnum.EQUAL; + } else if (operation != null) { + throw new IllegalArgumentException("Invalid operator specified for number type"); + } + + + String invalidMessageName = "invalidNumberPrefix"; + + Predicate predicateNumeric = createPredicateNumeric(theResourceName, theParamName, join, myBuilder, nextOr, prefix, value, fromObj, invalidMessageName); + Predicate predicateOuter = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, predicateNumeric); + codePredicates.add(predicateOuter); + + } else { + throw new IllegalArgumentException("Invalid token type: " + nextOr.getClass()); + } + + } + + Predicate predicate = myBuilder.or(toArray(codePredicates)); + myPredicates.add(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 new file mode 100644 index 00000000000..91531ce5e43 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderQuantity.java @@ -0,0 +1,171 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchFilterParser; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.base.composite.BaseQuantityDt; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.QuantityParam; + +import javax.persistence.criteria.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class PredicateBuilderQuantity extends BasePredicateBuilder { + + PredicateBuilderQuantity(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + public Predicate addPredicateQuantity(String theResourceName, + String theParamName, + List theList, + SearchFilterParser.CompareOperation operation) { + Join join = createJoin(SearchBuilderJoinEnum.QUANTITY, theParamName); + + if (theList.get(0).getMissing() != null) { + addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); + return null; + } + + List codePredicates = new ArrayList(); + for (IQueryParameterType nextOr : theList) { + + Predicate singleCode = createPredicateQuantity(nextOr, + theResourceName, + theParamName, + myBuilder, + join, + operation); + codePredicates.add(singleCode); + } + + Predicate retVal = myBuilder.or(toArray(codePredicates)); + myPredicates.add(retVal); + return retVal; + } + + public Predicate createPredicateQuantity(IQueryParameterType theParam, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom) { + return createPredicateQuantity(theParam, + theResourceName, + theParamName, + theBuilder, + theFrom, + null); + } + + private Predicate createPredicateQuantity(IQueryParameterType theParam, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom, + SearchFilterParser.CompareOperation operation) { + String systemValue; + String unitsValue; + ParamPrefixEnum cmpValue = null; + BigDecimal valueValue; + + if (operation == SearchFilterParser.CompareOperation.ne) { + cmpValue = ParamPrefixEnum.NOT_EQUAL; + } else if (operation == SearchFilterParser.CompareOperation.lt) { + cmpValue = ParamPrefixEnum.LESSTHAN; + } else if (operation == SearchFilterParser.CompareOperation.le) { + cmpValue = ParamPrefixEnum.LESSTHAN_OR_EQUALS; + } else if (operation == SearchFilterParser.CompareOperation.gt) { + cmpValue = ParamPrefixEnum.GREATERTHAN; + } else if (operation == SearchFilterParser.CompareOperation.ge) { + cmpValue = ParamPrefixEnum.GREATERTHAN_OR_EQUALS; + } else if (operation == SearchFilterParser.CompareOperation.eq) { + cmpValue = ParamPrefixEnum.EQUAL; + } else if (operation != null) { + throw new IllegalArgumentException("Invalid operator specified for quantity type"); + } + + if (theParam instanceof BaseQuantityDt) { + BaseQuantityDt param = (BaseQuantityDt) theParam; + systemValue = param.getSystemElement().getValueAsString(); + unitsValue = param.getUnitsElement().getValueAsString(); + if (operation == null) { + cmpValue = ParamPrefixEnum.forValue(param.getComparatorElement().getValueAsString()); + } + valueValue = param.getValueElement().getValue(); + } else if (theParam instanceof QuantityParam) { + QuantityParam param = (QuantityParam) theParam; + systemValue = param.getSystem(); + unitsValue = param.getUnits(); + if (operation == null) { + cmpValue = param.getPrefix(); + } + valueValue = param.getValue(); + } else { + throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); + } + + if (myDontUseHashesForSearch) { + Predicate system = null; + if (!isBlank(systemValue)) { + system = theBuilder.equal(theFrom.get("mySystem"), systemValue); + } + + Predicate code = null; + if (!isBlank(unitsValue)) { + code = theBuilder.equal(theFrom.get("myUnits"), unitsValue); + } + + cmpValue = defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); + final Expression path = theFrom.get("myValue"); + String invalidMessageName = "invalidQuantityPrefix"; + + Predicate num = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); + + Predicate singleCode; + if (system == null && code == null) { + singleCode = num; + } else if (system == null) { + singleCode = theBuilder.and(code, num); + } else if (code == null) { + singleCode = theBuilder.and(system, num); + } else { + singleCode = theBuilder.and(system, code, num); + } + + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } + + Predicate hashPredicate; + if (!isBlank(systemValue) && !isBlank(unitsValue)) { + long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue); + hashPredicate = myBuilder.equal(theFrom.get("myHashIdentitySystemAndUnits"), hash); + } else if (!isBlank(unitsValue)) { + long hash = ResourceIndexedSearchParamQuantity.calculateHashUnits(theResourceName, theParamName, unitsValue); + hashPredicate = myBuilder.equal(theFrom.get("myHashIdentityAndUnits"), hash); + } else { + long hash = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); + hashPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hash); + } + + cmpValue = defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL); + final Expression path = theFrom.get("myValue"); + String invalidMessageName = "invalidQuantityPrefix"; + + Predicate numericPredicate = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName); + + return theBuilder.and(hashPredicate, numericPredicate); + } + + +} 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 new file mode 100644 index 00000000000..acf5455559d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderString.java @@ -0,0 +1,196 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchFilterParser; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.util.StringNormalizer; +import ca.uhn.fhir.model.api.IPrimitiveDatatype; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; + +public class PredicateBuilderString extends BasePredicateBuilder { + + PredicateBuilderString(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + public Predicate addPredicateString(String theResourceName, + String theParamName, + List theList, + SearchFilterParser.CompareOperation operation) { + + Join join = createJoin(SearchBuilderJoinEnum.STRING, theParamName); + + if (theList.get(0).getMissing() != null) { + addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); + return null; + } + + List codePredicates = new ArrayList<>(); + for (IQueryParameterType nextOr : theList) { + IQueryParameterType theParameter = nextOr; + Predicate singleCode = createPredicateString(theParameter, + theResourceName, + theParamName, + myBuilder, + join, + operation); + codePredicates.add(singleCode); + } + + Predicate retVal = myBuilder.or(toArray(codePredicates)); + myPredicates.add(retVal); + return retVal; + } + + public Predicate createPredicateString(IQueryParameterType theParameter, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom) { + return createPredicateString(theParameter, + theResourceName, + theParamName, + theBuilder, + theFrom, + null); + } + + private Predicate createPredicateString(IQueryParameterType theParameter, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom, + SearchFilterParser.CompareOperation operation) { + String rawSearchTerm; + if (theParameter instanceof TokenParam) { + TokenParam id = (TokenParam) theParameter; + if (!id.isText()) { + throw new IllegalStateException("Trying to process a text search on a non-text token parameter"); + } + rawSearchTerm = id.getValue(); + } else if (theParameter instanceof StringParam) { + StringParam id = (StringParam) theParameter; + rawSearchTerm = id.getValue(); + if (id.isContains()) { + if (!myDaoConfig.isAllowContainsSearches()) { + throw new MethodNotAllowedException(":contains modifier is disabled on this server"); + } + } + } else if (theParameter instanceof IPrimitiveDatatype) { + IPrimitiveDatatype id = (IPrimitiveDatatype) theParameter; + rawSearchTerm = id.getValueAsString(); + } else { + throw new IllegalArgumentException("Invalid token type: " + theParameter.getClass()); + } + + if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { + throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed (" + + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); + } + + if (myDontUseHashesForSearch) { + String likeExpression = StringNormalizer.normalizeString(rawSearchTerm); + if (myDaoConfig.isAllowContainsSearches()) { + if (theParameter instanceof StringParam) { + if (((StringParam) theParameter).isContains()) { + likeExpression = createLeftAndRightMatchLikeExpression(likeExpression); + } else { + likeExpression = createLeftMatchLikeExpression(likeExpression); + } + } else { + likeExpression = createLeftMatchLikeExpression(likeExpression); + } + } else { + likeExpression = createLeftMatchLikeExpression(likeExpression); + } + + Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); + if (theParameter instanceof StringParam && ((StringParam) theParameter).isExact()) { + Predicate exactCode = theBuilder.equal(theFrom.get("myValueExact"), rawSearchTerm); + singleCode = theBuilder.and(singleCode, exactCode); + } + + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } + boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact(); + if (exactMatch) { + // Exact match + Long hash = ResourceIndexedSearchParamString.calculateHashExact(theResourceName, theParamName, rawSearchTerm); + return theBuilder.equal(theFrom.get("myHashExact").as(Long.class), hash); + } else { + // Normalized Match + String normalizedString = StringNormalizer.normalizeString(rawSearchTerm); + String likeExpression; + if ((theParameter instanceof StringParam) && + (((((StringParam) theParameter).isContains()) && + (myCallingDao.getConfig().isAllowContainsSearches())) || + (operation == SearchFilterParser.CompareOperation.co))) { + likeExpression = createLeftAndRightMatchLikeExpression(normalizedString); + } else if ((operation != SearchFilterParser.CompareOperation.ne) && + (operation != SearchFilterParser.CompareOperation.gt) && + (operation != SearchFilterParser.CompareOperation.lt) && + (operation != SearchFilterParser.CompareOperation.ge) && + (operation != SearchFilterParser.CompareOperation.le)) { + if (operation == SearchFilterParser.CompareOperation.ew) { + likeExpression = createRightMatchLikeExpression(normalizedString); + } else { + likeExpression = createLeftMatchLikeExpression(normalizedString); + } + } else { + likeExpression = normalizedString; + } + + Predicate predicate; + if ((operation == null) || + (operation == SearchFilterParser.CompareOperation.sw)) { + Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig.getModelConfig(), theResourceName, theParamName, normalizedString); + Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash); + Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = theBuilder.and(hashCode, singleCode); + } else if ((operation == SearchFilterParser.CompareOperation.ew) || + (operation == SearchFilterParser.CompareOperation.co)) { + Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } else if (operation == SearchFilterParser.CompareOperation.eq) { + Long hash = ResourceIndexedSearchParamString.calculateHashNormalized(myDaoConfig.getModelConfig(), theResourceName, theParamName, normalizedString); + Predicate hashCode = theBuilder.equal(theFrom.get("myHashNormalizedPrefix").as(Long.class), hash); + Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), normalizedString); + predicate = theBuilder.and(hashCode, singleCode); + } else if (operation == SearchFilterParser.CompareOperation.ne) { + Predicate singleCode = theBuilder.notEqual(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } else if (operation == SearchFilterParser.CompareOperation.gt) { + Predicate singleCode = theBuilder.greaterThan(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } else if (operation == SearchFilterParser.CompareOperation.lt) { + Predicate singleCode = theBuilder.lessThan(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } else if (operation == SearchFilterParser.CompareOperation.ge) { + Predicate singleCode = theBuilder.greaterThanOrEqualTo(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } else if (operation == SearchFilterParser.CompareOperation.le) { + Predicate singleCode = theBuilder.lessThanOrEqualTo(theFrom.get("myValueNormalized").as(String.class), likeExpression); + predicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode); + } else { + throw new IllegalArgumentException("Don't yet know how to handle operation " + operation + " on a string"); + } + + return predicate; + } + } +} 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 new file mode 100644 index 00000000000..3c853285165 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderTag.java @@ -0,0 +1,179 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; +import ca.uhn.fhir.jpa.model.entity.TagDefinition; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.google.common.collect.Lists; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.criteria.*; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class PredicateBuilderTag extends BasePredicateBuilder { + private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderTag.class); + + PredicateBuilderTag(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + public void addPredicateTag(List> theList, String theParamName) { + TagTypeEnum tagType; + if (Constants.PARAM_TAG.equals(theParamName)) { + tagType = TagTypeEnum.TAG; + } else if (Constants.PARAM_PROFILE.equals(theParamName)) { + tagType = TagTypeEnum.PROFILE; + } else if (Constants.PARAM_SECURITY.equals(theParamName)) { + tagType = TagTypeEnum.SECURITY_LABEL; + } else { + throw new IllegalArgumentException("Param name: " + theParamName); // shouldn't happen + } + + List> notTags = Lists.newArrayList(); + for (List nextAndParams : theList) { + for (IQueryParameterType nextOrParams : nextAndParams) { + if (nextOrParams instanceof TokenParam) { + TokenParam param = (TokenParam) nextOrParams; + if (param.getModifier() == TokenParamModifier.NOT) { + if (isNotBlank(param.getSystem()) || isNotBlank(param.getValue())) { + notTags.add(Pair.of(param.getSystem(), param.getValue())); + } + } + } + } + } + + /* + * We have a parameter of ResourceType?_tag:not=foo This means match resources that don't have the given tag(s) + */ + if (notTags.isEmpty() == false) { + // CriteriaBuilder builder = myEntityManager.getCriteriaBuilder(); + // CriteriaQuery cq = builder.createQuery(Long.class); + // Root from = cq.from(ResourceTable.class); + // cq.select(from.get("myId").as(Long.class)); + // + // Subquery subQ = cq.subquery(Long.class); + // Root subQfrom = subQ.from(ResourceTag.class); + // subQ.select(subQfrom.get("myResourceId").as(Long.class)); + // Predicate subQname = builder.equal(subQfrom.get("myParamName"), theParamName); + // Predicate subQtype = builder.equal(subQfrom.get("myResourceType"), myResourceName); + // subQ.where(builder.and(subQtype, subQname)); + // + // List predicates = new ArrayList(); + // predicates.add(builder.not(builder.in(from.get("myId")).value(subQ))); + // predicates.add(builder.equal(from.get("myResourceType"), myResourceName)); + // predicates.add(builder.isNull(from.get("myDeleted"))); + // createPredicateResourceId(builder, cq, predicates, from.get("myId").as(Long.class)); + } + + for (List nextAndParams : theList) { + boolean haveTags = false; + for (IQueryParameterType nextParamUncasted : nextAndParams) { + if (nextParamUncasted instanceof TokenParam) { + TokenParam nextParam = (TokenParam) nextParamUncasted; + if (isNotBlank(nextParam.getValue())) { + haveTags = true; + } else if (isNotBlank(nextParam.getSystem())) { + throw new InvalidRequestException("Invalid " + theParamName + " parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken(myContext)); + } + } else { + UriParam nextParam = (UriParam) nextParamUncasted; + if (isNotBlank(nextParam.getValue())) { + haveTags = true; + } + } + } + if (!haveTags) { + continue; + } + + boolean paramInverted = false; + List> tokens = Lists.newArrayList(); + for (IQueryParameterType nextOrParams : nextAndParams) { + String code; + String system; + if (nextOrParams instanceof TokenParam) { + TokenParam nextParam = (TokenParam) nextOrParams; + code = nextParam.getValue(); + system = nextParam.getSystem(); + if (nextParam.getModifier() == TokenParamModifier.NOT) { + paramInverted = true; + } + } else { + UriParam nextParam = (UriParam) nextOrParams; + code = nextParam.getValue(); + system = null; + } + + if (isNotBlank(code)) { + tokens.add(Pair.of(system, code)); + } + } + + if (tokens.isEmpty()) { + continue; + } + + if (paramInverted) { + ourLog.debug("Searching for _tag:not"); + + Subquery subQ = myResourceTableQuery.subquery(Long.class); + Root subQfrom = subQ.from(ResourceTag.class); + subQ.select(subQfrom.get("myResourceId").as(Long.class)); + + myPredicates.add(myBuilder.not(myBuilder.in(myResourceTableRoot.get("myId")).value(subQ))); + + Subquery defJoin = subQ.subquery(Long.class); + Root defJoinFrom = defJoin.from(TagDefinition.class); + defJoin.select(defJoinFrom.get("myId").as(Long.class)); + + subQ.where(subQfrom.get("myTagId").as(Long.class).in(defJoin)); + + Predicate tagListPredicate = createPredicateTagList(defJoinFrom, myBuilder, tagType, tokens); + defJoin.where(tagListPredicate); + + continue; + } + + Join tagJoin = myResourceTableRoot.join("myTags", JoinType.LEFT); + From defJoin = tagJoin.join("myTag"); + + Predicate tagListPredicate = createPredicateTagList(defJoin, myBuilder, tagType, tokens); + myPredicates.add(tagListPredicate); + + } + + } + + private Predicate createPredicateTagList(Path theDefJoin, CriteriaBuilder theBuilder, TagTypeEnum theTagType, List> theTokens) { + Predicate typePredicate = theBuilder.equal(theDefJoin.get("myTagType"), theTagType); + + List orPredicates = Lists.newArrayList(); + for (Pair next : theTokens) { + Predicate codePredicate = theBuilder.equal(theDefJoin.get("myCode"), next.getRight()); + if (isNotBlank(next.getLeft())) { + Predicate systemPredicate = theBuilder.equal(theDefJoin.get("mySystem"), next.getLeft()); + orPredicates.add(theBuilder.and(typePredicate, systemPredicate, codePredicate)); + } else { + orPredicates.add(theBuilder.and(typePredicate, codePredicate)); + } + } + + return theBuilder.or(toArray(orPredicates)); + } + +} 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 new file mode 100644 index 00000000000..dc4d9e7eeeb --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderToken.java @@ -0,0 +1,325 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.*; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchFilterParser; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.jpa.term.VersionIndependentConcept; +import ca.uhn.fhir.jpa.term.api.ITermReadSvc; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.model.base.composite.BaseCodingDt; +import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; +import ca.uhn.fhir.rest.param.NumberParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.google.common.collect.Sets; +import org.hibernate.query.criteria.internal.CriteriaBuilderImpl; +import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.criteria.*; +import java.util.*; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class PredicateBuilderToken extends BasePredicateBuilder { + @Autowired + private ITermReadSvc myTerminologySvc; + @Autowired + private ISearchParamRegistry mySearchParamRegistry; + private final PredicateBuilderString myPredicateBuilderString; + + PredicateBuilderToken(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + myPredicateBuilderString = theSearchBuilder.getPredicateBuilderString(); + } + + public Predicate addPredicateToken(String theResourceName, + String theParamName, + List theList, + SearchFilterParser.CompareOperation operation) { + + if (theList.get(0).getMissing() != null) { + Join join = createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); + addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); + return null; + } + + List codePredicates = new ArrayList<>(); + List tokens = new ArrayList<>(); + for (IQueryParameterType nextOr : theList) { + + if (nextOr instanceof TokenParam) { + TokenParam id = (TokenParam) nextOr; + if (id.isText()) { + myPredicateBuilderString.addPredicateString(theResourceName, theParamName, theList); + break; + } + } + + tokens.add(nextOr); + } + + if (tokens.isEmpty()) { + return null; + } + + Join join = createJoin(SearchBuilderJoinEnum.TOKEN, theParamName); + Collection singleCode = createPredicateToken(tokens, theResourceName, theParamName, myBuilder, join, operation); + assert singleCode != null; + codePredicates.addAll(singleCode); + + Predicate spPredicate = myBuilder.or(toArray(codePredicates)); + myPredicates.add(spPredicate); + return spPredicate; + } + + public Collection createPredicateToken(Collection theParameters, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom) { + return createPredicateToken( + theParameters, + theResourceName, + theParamName, + theBuilder, + theFrom, + null); + } + + private Collection createPredicateToken(Collection theParameters, + String theResourceName, + String theParamName, + CriteriaBuilder theBuilder, + From theFrom, + SearchFilterParser.CompareOperation operation) { + final List codes = new ArrayList<>(); + + TokenParamModifier modifier = null; + for (IQueryParameterType nextParameter : theParameters) { + + String code; + String system; + if (nextParameter instanceof TokenParam) { + TokenParam id = (TokenParam) nextParameter; + system = id.getSystem(); + code = (id.getValue()); + modifier = id.getModifier(); + } else if (nextParameter instanceof BaseIdentifierDt) { + BaseIdentifierDt id = (BaseIdentifierDt) nextParameter; + system = id.getSystemElement().getValueAsString(); + code = (id.getValueElement().getValue()); + } else if (nextParameter instanceof BaseCodingDt) { + BaseCodingDt id = (BaseCodingDt) nextParameter; + system = id.getSystemElement().getValueAsString(); + code = (id.getCodeElement().getValue()); + } else if (nextParameter instanceof NumberParam) { + NumberParam number = (NumberParam) nextParameter; + system = null; + code = number.getValueAsQueryToken(myContext); + } else { + throw new IllegalArgumentException("Invalid token type: " + nextParameter.getClass()); + } + + if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { + throw new InvalidRequestException( + "Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); + } + + if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { + throw new InvalidRequestException( + "Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code); + } + + /* + * Process token modifiers (:in, :below, :above) + */ + + if (modifier == TokenParamModifier.IN) { + codes.addAll(myTerminologySvc.expandValueSet(code)); + } else if (modifier == TokenParamModifier.ABOVE) { + system = determineSystemIfMissing(theParamName, code, system); + validateHaveSystemAndCodeForToken(theParamName, code, system); + codes.addAll(myTerminologySvc.findCodesAbove(system, code)); + } else if (modifier == TokenParamModifier.BELOW) { + system = determineSystemIfMissing(theParamName, code, system); + validateHaveSystemAndCodeForToken(theParamName, code, system); + codes.addAll(myTerminologySvc.findCodesBelow(system, code)); + } else { + codes.add(new VersionIndependentConcept(system, code)); + } + + } + + List sortedCodesList = codes + .stream() + .filter(t -> t.getCode() != null || t.getSystem() != null) + .sorted() + .distinct() + .collect(Collectors.toList()); + + if (codes.isEmpty()) { + // This will never match anything + return Collections.singletonList(new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false)); + } + + List retVal = new ArrayList<>(); + + // System only + List systemOnlyCodes = sortedCodesList.stream().filter(t -> isBlank(t.getCode())).collect(Collectors.toList()); + if (!systemOnlyCodes.isEmpty()) { + retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemOnlyCodes, modifier, SearchBuilderTokenModeEnum.SYSTEM_ONLY)); + } + + // Code only + List codeOnlyCodes = sortedCodesList.stream().filter(t -> t.getSystem() == null).collect(Collectors.toList()); + if (!codeOnlyCodes.isEmpty()) { + retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, codeOnlyCodes, modifier, SearchBuilderTokenModeEnum.VALUE_ONLY)); + } + + // System and code + List systemAndCodeCodes = sortedCodesList.stream().filter(t -> isNotBlank(t.getCode()) && t.getSystem() != null).collect(Collectors.toList()); + if (!systemAndCodeCodes.isEmpty()) { + retVal.add(addPredicateToken(theResourceName, theParamName, theBuilder, theFrom, systemAndCodeCodes, modifier, SearchBuilderTokenModeEnum.SYSTEM_AND_VALUE)); + } + + return retVal; + } + + private String determineSystemIfMissing(String theParamName, String code, String theSystem) { + String retVal = theSystem; + if (retVal == null) { + RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(myResourceName); + RuntimeSearchParam param = mySearchParamRegistry.getSearchParamByName(resourceDef, theParamName); + if (param != null) { + Set valueSetUris = Sets.newHashSet(); + for (String nextPath : param.getPathsSplit()) { + BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, nextPath); + if (def instanceof BaseRuntimeDeclaredChildDefinition) { + String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet(); + if (isNotBlank(valueSet)) { + valueSetUris.add(valueSet); + } + } + } + if (valueSetUris.size() == 1) { + String valueSet = valueSetUris.iterator().next(); + List candidateCodes = myTerminologySvc.expandValueSet(valueSet); + for (VersionIndependentConcept nextCandidate : candidateCodes) { + if (nextCandidate.getCode().equals(code)) { + retVal = nextCandidate.getSystem(); + break; + } + } + } + } + } + return retVal; + } + + private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) { + String systemDesc = defaultIfBlank(theSystem, "(missing)"); + String codeDesc = defaultIfBlank(theCode, "(missing)"); + if (isBlank(theCode)) { + String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc); + throw new InvalidRequestException(msg); + } + if (isBlank(theSystem)) { + String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc); + throw new InvalidRequestException(msg); + } + } + + private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From theFrom, List theTokens, TokenParamModifier theModifier, SearchBuilderTokenModeEnum theTokenMode) { + if (myDontUseHashesForSearch) { + final Path systemExpression = theFrom.get("mySystem"); + final Path valueExpression = theFrom.get("myValue"); + + List orPredicates = new ArrayList<>(); + switch (theTokenMode) { + case SYSTEM_ONLY: { + List systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList()); + Predicate orPredicate = systemExpression.in(systems); + orPredicates.add(orPredicate); + break; + } + case VALUE_ONLY: + List codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList()); + Predicate orPredicate = valueExpression.in(codes); + orPredicates.add(orPredicate); + break; + case SYSTEM_AND_VALUE: + for (VersionIndependentConcept next : theTokens) { + orPredicates.add(theBuilder.and( + toEqualOrIsNullPredicate(systemExpression, next.getSystem()), + toEqualOrIsNullPredicate(valueExpression, next.getCode()) + )); + } + break; + } + + Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0])); + if (theModifier == TokenParamModifier.NOT) { + or = theBuilder.not(or); + } + + return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or); + } + + /* + * Note: A null system value means "match any system", but + * an empty-string system value means "match values that + * explicitly have no system". + */ + Expression hashField; + List values; + switch (theTokenMode) { + case SYSTEM_ONLY: + hashField = theFrom.get("myHashSystem").as(Long.class); + values = theTokens + .stream() + .map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(theResourceName, theParamName, t.getSystem())) + .collect(Collectors.toList()); + break; + case VALUE_ONLY: + hashField = theFrom.get("myHashValue").as(Long.class); + values = theTokens + .stream() + .map(t -> ResourceIndexedSearchParamToken.calculateHashValue(theResourceName, theParamName, t.getCode())) + .collect(Collectors.toList()); + break; + case SYSTEM_AND_VALUE: + default: + hashField = theFrom.get("myHashSystemAndValue").as(Long.class); + values = theTokens + .stream() + .map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(theResourceName, theParamName, t.getSystem(), t.getCode())) + .collect(Collectors.toList()); + break; + } + + Predicate predicate = hashField.in(values); + if (theModifier == TokenParamModifier.NOT) { + Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName)); + Predicate disjunctionPredicate = theBuilder.not(predicate); + predicate = theBuilder.and(identityPredicate, disjunctionPredicate); + } + return predicate; + } + + private Expression toEqualOrIsNullPredicate(Path theExpression, T theCode) { + if (theCode == null) { + return myBuilder.isNull(theExpression); + } + return myBuilder.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 new file mode 100644 index 00000000000..8e407496340 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/PredicateBuilderUri.java @@ -0,0 +1,160 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.SearchBuilder; +import ca.uhn.fhir.jpa.dao.SearchFilterParser; +import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; +import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.param.UriParamQualifierEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class PredicateBuilderUri extends BasePredicateBuilder { + private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderUri.class); + @Autowired + private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; + + PredicateBuilderUri(SearchBuilder theSearchBuilder) { + super(theSearchBuilder); + } + + public Predicate addPredicateUri(String theResourceName, + String theParamName, + List theList, + SearchFilterParser.CompareOperation operation) { + + Join join = createJoin(SearchBuilderJoinEnum.URI, theParamName); + + if (theList.get(0).getMissing() != null) { + addPredicateParamMissing(theResourceName, theParamName, theList.get(0).getMissing(), join); + return null; + } + + List codePredicates = new ArrayList<>(); + for (IQueryParameterType nextOr : theList) { + + if (nextOr instanceof UriParam) { + UriParam param = (UriParam) nextOr; + + String value = param.getValue(); + if (value == null) { + continue; + } + + if (param.getQualifier() == UriParamQualifierEnum.ABOVE) { + + /* + * :above is an inefficient query- It means that the user is supplying a more specific URL (say + * http://example.com/foo/bar/baz) and that we should match on any URLs that are less + * specific but otherwise the same. For example http://example.com and http://example.com/foo would both + * match. + * + * We do this by querying the DB for all candidate URIs and then manually checking each one. This isn't + * very efficient, but this is also probably not a very common type of query to do. + * + * If we ever need to make this more efficient, lucene could certainly be used as an optimization. + */ + ourLog.info("Searching for candidate URI:above parameters for Resource[{}] param[{}]", myResourceName, theParamName); + Collection candidates = myResourceIndexedSearchParamUriDao.findAllByResourceTypeAndParamName(myResourceName, theParamName); + List toFind = new ArrayList<>(); + for (String next : candidates) { + if (value.length() >= next.length()) { + if (value.substring(0, next.length()).equals(next)) { + toFind.add(next); + } + } + } + + if (toFind.isEmpty()) { + continue; + } + + Predicate uriPredicate = join.get("myUri").as(String.class).in(toFind); + Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate); + codePredicates.add(hashAndUriPredicate); + + } else if (param.getQualifier() == UriParamQualifierEnum.BELOW) { + + Predicate uriPredicate = myBuilder.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); + 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); + codePredicates.add(hashPredicate); + } else if (operation == SearchFilterParser.CompareOperation.ne) { + uriPredicate = myBuilder.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)); + } else if (operation == SearchFilterParser.CompareOperation.gt) { + uriPredicate = myBuilder.greaterThan(join.get("myUri").as(String.class), value); + } else if (operation == SearchFilterParser.CompareOperation.lt) { + uriPredicate = myBuilder.lessThan(join.get("myUri").as(String.class), value); + } else if (operation == SearchFilterParser.CompareOperation.ge) { + uriPredicate = myBuilder.greaterThanOrEqualTo(join.get("myUri").as(String.class), value); + } else if (operation == SearchFilterParser.CompareOperation.le) { + uriPredicate = myBuilder.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)); + } else if (operation == SearchFilterParser.CompareOperation.ew) { + uriPredicate = myBuilder.like(join.get("myUri").as(String.class), createRightMatchLikeExpression(value)); + } else { + throw new IllegalArgumentException(String.format("Unsupported operator specified in _filter clause, %s", + operation.toString())); + } + + if (uriPredicate != null) { + long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName); + Predicate hashIdentityPredicate = myBuilder.equal(join.get("myHashIdentity"), hashIdentity); + codePredicates.add(myBuilder.and(hashIdentityPredicate, uriPredicate)); + } + } + } + + } else { + throw new IllegalArgumentException("Invalid URI type: " + nextOr.getClass()); + } + + } + + /* + * If we haven't found any of the requested URIs in the candidates, then we'll + * just add a predicate that can never match + */ + if (codePredicates.isEmpty()) { + Predicate predicate = myBuilder.isNull(join.get("myMissing").as(String.class)); + myPredicates.add(predicate); + return null; + } + + Predicate orPredicate = myBuilder.or(toArray(codePredicates)); + + Predicate outerPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, + theParamName, + join, + orPredicate); + myPredicates.add(outerPredicate); + return outerPredicate; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java new file mode 100644 index 00000000000..298634c7379 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinEnum.java @@ -0,0 +1,13 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +public enum SearchBuilderJoinEnum { + DATE, + NUMBER, + QUANTITY, + REFERENCE, + STRING, + TOKEN, + URI, + COORDS + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinKey.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinKey.java new file mode 100644 index 00000000000..49ef285e2b7 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderJoinKey.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class SearchBuilderJoinKey { + private final SearchBuilderJoinEnum myJoinType; + private final String myParamName; + + public SearchBuilderJoinKey(String theParamName, SearchBuilderJoinEnum theJoinType) { + super(); + myParamName = theParamName; + myJoinType = theJoinType; + } + + @Override + public boolean equals(Object theObj) { + if (!(theObj instanceof SearchBuilderJoinKey)) { + return false; + } + SearchBuilderJoinKey obj = (SearchBuilderJoinKey) theObj; + return new EqualsBuilder() + .append(myParamName, obj.myParamName) + .append(myJoinType, obj.myJoinType) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder() + .append(myParamName) + .append(myJoinType) + .toHashCode(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderTokenModeEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderTokenModeEnum.java new file mode 100644 index 00000000000..c7ba68fee06 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/predicate/SearchBuilderTokenModeEnum.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.jpa.dao.predicate; + +public enum SearchBuilderTokenModeEnum { + SYSTEM_ONLY, + VALUE_ONLY, + SYSTEM_AND_VALUE +}