001package ca.uhn.fhir.jpa.search.builder;
002
003/*
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.api.config.DaoConfig;
028import ca.uhn.fhir.jpa.dao.BaseStorageDao;
029import ca.uhn.fhir.jpa.dao.LegacySearchBuilder;
030import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderToken;
031import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
032import ca.uhn.fhir.jpa.model.config.PartitionSettings;
033import ca.uhn.fhir.jpa.model.entity.ModelConfig;
034import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
035import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
036import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
037import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder;
038import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder;
039import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder;
040import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
041import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder;
042import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder;
043import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder;
044import ca.uhn.fhir.jpa.search.builder.predicate.QuantityBasePredicateBuilder;
045import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder;
046import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
047import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder;
048import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder;
049import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder;
050import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder;
051import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder;
052import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder;
053import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder;
054import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
055import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
056import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
057import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
058import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
059import ca.uhn.fhir.model.api.IQueryParameterAnd;
060import ca.uhn.fhir.model.api.IQueryParameterOr;
061import ca.uhn.fhir.model.api.IQueryParameterType;
062import ca.uhn.fhir.parser.DataFormatException;
063import ca.uhn.fhir.rest.api.Constants;
064import ca.uhn.fhir.rest.api.QualifiedParamList;
065import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
066import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
067import ca.uhn.fhir.rest.api.server.RequestDetails;
068import ca.uhn.fhir.rest.param.CompositeParam;
069import ca.uhn.fhir.rest.param.DateParam;
070import ca.uhn.fhir.rest.param.HasParam;
071import ca.uhn.fhir.rest.param.NumberParam;
072import ca.uhn.fhir.rest.param.ParamPrefixEnum;
073import ca.uhn.fhir.rest.param.QuantityParam;
074import ca.uhn.fhir.rest.param.ReferenceParam;
075import ca.uhn.fhir.rest.param.StringParam;
076import ca.uhn.fhir.rest.param.TokenParam;
077import ca.uhn.fhir.rest.param.TokenParamModifier;
078import ca.uhn.fhir.rest.param.UriParam;
079import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
080import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
081import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
082import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
083import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
084import com.google.common.collect.Lists;
085import com.google.common.collect.Maps;
086import com.google.common.collect.Sets;
087import com.healthmarketscience.sqlbuilder.BinaryCondition;
088import com.healthmarketscience.sqlbuilder.ComboCondition;
089import com.healthmarketscience.sqlbuilder.Condition;
090import com.healthmarketscience.sqlbuilder.Expression;
091import com.healthmarketscience.sqlbuilder.InCondition;
092import com.healthmarketscience.sqlbuilder.OrderObject;
093import com.healthmarketscience.sqlbuilder.SelectQuery;
094import com.healthmarketscience.sqlbuilder.SetOperationQuery;
095import com.healthmarketscience.sqlbuilder.Subquery;
096import com.healthmarketscience.sqlbuilder.UnionQuery;
097import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
098import org.apache.commons.collections4.BidiMap;
099import org.apache.commons.collections4.bidimap.DualHashBidiMap;
100import org.apache.commons.collections4.bidimap.UnmodifiableBidiMap;
101import org.apache.commons.lang3.StringUtils;
102import org.apache.commons.lang3.builder.EqualsBuilder;
103import org.apache.commons.lang3.builder.HashCodeBuilder;
104import org.apache.commons.lang3.tuple.Triple;
105import org.hl7.fhir.instance.model.api.IAnyResource;
106import org.slf4j.Logger;
107import org.slf4j.LoggerFactory;
108
109import javax.annotation.Nonnull;
110import javax.annotation.Nullable;
111import java.math.BigDecimal;
112import java.util.ArrayList;
113import java.util.Arrays;
114import java.util.Collection;
115import java.util.Collections;
116import java.util.EnumSet;
117import java.util.HashMap;
118import java.util.List;
119import java.util.Map;
120import java.util.Objects;
121import java.util.Set;
122import java.util.function.Supplier;
123import java.util.stream.Collectors;
124
125import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
126import static org.apache.commons.lang3.StringUtils.isBlank;
127import static org.apache.commons.lang3.StringUtils.isNotBlank;
128import static org.apache.commons.lang3.StringUtils.split;
129
130public class QueryStack {
131
132        private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class);
133        private static final BidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> ourCompareOperationToParamPrefix;
134
135        static {
136                DualHashBidiMap<SearchFilterParser.CompareOperation, ParamPrefixEnum> compareOperationToParamPrefix = new DualHashBidiMap<>();
137                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ap, ParamPrefixEnum.APPROXIMATE);
138                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eq, ParamPrefixEnum.EQUAL);
139                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.gt, ParamPrefixEnum.GREATERTHAN);
140                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ge, ParamPrefixEnum.GREATERTHAN_OR_EQUALS);
141                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.lt, ParamPrefixEnum.LESSTHAN);
142                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.le, ParamPrefixEnum.LESSTHAN_OR_EQUALS);
143                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.ne, ParamPrefixEnum.NOT_EQUAL);
144                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.eb, ParamPrefixEnum.ENDS_BEFORE);
145                compareOperationToParamPrefix.put(SearchFilterParser.CompareOperation.sa, ParamPrefixEnum.STARTS_AFTER);
146                ourCompareOperationToParamPrefix = UnmodifiableBidiMap.unmodifiableBidiMap(compareOperationToParamPrefix);
147        }
148
149        private final ModelConfig myModelConfig;
150        private final FhirContext myFhirContext;
151        private final SearchQueryBuilder mySqlBuilder;
152        private final SearchParameterMap mySearchParameters;
153        private final ISearchParamRegistry mySearchParamRegistry;
154        private final PartitionSettings myPartitionSettings;
155        private final DaoConfig myDaoConfig;
156        private final EnumSet<PredicateBuilderTypeEnum> myReusePredicateBuilderTypes;
157        private Map<PredicateBuilderCacheKey, BaseJoiningPredicateBuilder> myJoinMap;
158
159        /**
160         * Constructor
161         */
162        public QueryStack(SearchParameterMap theSearchParameters, DaoConfig theDaoConfig, ModelConfig theModelConfig, FhirContext theFhirContext, SearchQueryBuilder theSqlBuilder, ISearchParamRegistry theSearchParamRegistry, PartitionSettings thePartitionSettings) {
163                this(theSearchParameters, theDaoConfig, theModelConfig, theFhirContext, theSqlBuilder, theSearchParamRegistry, thePartitionSettings, EnumSet.of(PredicateBuilderTypeEnum.DATE));
164        }
165
166        /**
167         * Constructor
168         */
169        private QueryStack(SearchParameterMap theSearchParameters, DaoConfig theDaoConfig, ModelConfig theModelConfig, FhirContext theFhirContext, SearchQueryBuilder theSqlBuilder, ISearchParamRegistry theSearchParamRegistry, PartitionSettings thePartitionSettings, EnumSet<PredicateBuilderTypeEnum> theReusePredicateBuilderTypes) {
170                myPartitionSettings = thePartitionSettings;
171                assert theSearchParameters != null;
172                assert theDaoConfig != null;
173                assert theModelConfig != null;
174                assert theFhirContext != null;
175                assert theSqlBuilder != null;
176
177                mySearchParameters = theSearchParameters;
178                myDaoConfig = theDaoConfig;
179                myModelConfig = theModelConfig;
180                myFhirContext = theFhirContext;
181                mySqlBuilder = theSqlBuilder;
182                mySearchParamRegistry = theSearchParamRegistry;
183                myReusePredicateBuilderTypes = theReusePredicateBuilderTypes;
184        }
185
186        public void addSortOnDate(String theResourceName, String theParamName, boolean theAscending) {
187                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
188                DatePredicateBuilder sortPredicateBuilder = mySqlBuilder.addDatePredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
189
190                Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
191                mySqlBuilder.addPredicate(hashIdentityPredicate);
192                mySqlBuilder.addSortDate(sortPredicateBuilder.getColumnValueLow(), theAscending);
193        }
194
195        public void addSortOnLastUpdated(boolean theAscending) {
196                ResourceTablePredicateBuilder resourceTablePredicateBuilder;
197                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
198                if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) {
199                        resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder;
200                } else {
201                        resourceTablePredicateBuilder = mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
202                }
203                mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending);
204        }
205
206        public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) {
207                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
208                NumberPredicateBuilder sortPredicateBuilder = mySqlBuilder.addNumberPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
209
210                Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
211                mySqlBuilder.addPredicate(hashIdentityPredicate);
212                mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnValue(), theAscending);
213        }
214
215        public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) {
216                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
217
218                QuantityBasePredicateBuilder sortPredicateBuilder;
219                sortPredicateBuilder = mySqlBuilder.addQuantityPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
220
221                Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
222                mySqlBuilder.addPredicate(hashIdentityPredicate);
223                mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnValue(), theAscending);
224        }
225
226        public void addSortOnResourceId(boolean theAscending) {
227                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
228                ForcedIdPredicateBuilder sortPredicateBuilder = mySqlBuilder.addForcedIdPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
229                if (!theAscending) {
230                        mySqlBuilder.addSortString(sortPredicateBuilder.getColumnForcedId(), false, OrderObject.NullOrder.FIRST);
231                } else {
232                        mySqlBuilder.addSortString(sortPredicateBuilder.getColumnForcedId(), true);
233                }
234                mySqlBuilder.addSortNumeric(firstPredicateBuilder.getResourceIdColumn(), theAscending);
235
236        }
237
238        public void addSortOnResourceLink(String theResourceName, String theParamName, boolean theAscending) {
239                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
240                ResourceLinkPredicateBuilder sortPredicateBuilder = mySqlBuilder.addReferencePredicateBuilder(this, firstPredicateBuilder.getResourceIdColumn());
241
242                Condition pathPredicate = sortPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName, new ArrayList<>());
243                mySqlBuilder.addPredicate(pathPredicate);
244                mySqlBuilder.addSortNumeric(sortPredicateBuilder.getColumnTargetResourceId(), theAscending);
245        }
246
247        public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) {
248                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
249                StringPredicateBuilder sortPredicateBuilder = mySqlBuilder.addStringPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
250
251                Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
252                mySqlBuilder.addPredicate(hashIdentityPredicate);
253                mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValueNormalized(), theAscending);
254        }
255
256        public void addSortOnToken(String theResourceName, String theParamName, boolean theAscending) {
257                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
258                TokenPredicateBuilder sortPredicateBuilder = mySqlBuilder.addTokenPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
259
260                Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
261                mySqlBuilder.addPredicate(hashIdentityPredicate);
262                mySqlBuilder.addSortString(sortPredicateBuilder.getColumnSystem(), theAscending);
263                mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValue(), theAscending);
264        }
265
266        public void addSortOnUri(String theResourceName, String theParamName, boolean theAscending) {
267                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
268                UriPredicateBuilder sortPredicateBuilder = mySqlBuilder.addUriPredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
269
270                Condition hashIdentityPredicate = sortPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
271                mySqlBuilder.addPredicate(hashIdentityPredicate);
272                mySqlBuilder.addSortString(sortPredicateBuilder.getColumnValue(), theAscending);
273        }
274
275        @SuppressWarnings("unchecked")
276        private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder(PredicateBuilderTypeEnum theType, DbColumn theSourceJoinColumn, String theParamName, Supplier<T> theFactoryMethod) {
277                boolean cacheHit = false;
278                BaseJoiningPredicateBuilder retVal;
279                if (myReusePredicateBuilderTypes.contains(theType)) {
280                        PredicateBuilderCacheKey key = new PredicateBuilderCacheKey(theSourceJoinColumn, theType, theParamName);
281                        if (myJoinMap == null) {
282                                myJoinMap = new HashMap<>();
283                        }
284                        retVal = myJoinMap.get(key);
285                        if (retVal != null) {
286                                cacheHit = true;
287                        } else {
288                                retVal = theFactoryMethod.get();
289                                myJoinMap.put(key, retVal);
290                        }
291                } else {
292                        retVal = theFactoryMethod.get();
293                }
294                return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal);
295        }
296
297        private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId) {
298                return createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDef, theNextAnd, theRequestPartitionId, mySqlBuilder);
299        }
300
301        private Condition createPredicateComposite(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
302
303                Condition orCondidtion = null;
304                for (IQueryParameterType next : theNextAnd) {
305
306                        if (!(next instanceof CompositeParam<?, ?>)) {
307                                throw new InvalidRequestException(Msg.code(1203) + "Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + next.getClass());
308                        }
309                        CompositeParam<?, ?> cp = (CompositeParam<?, ?>) next;
310
311                        List<RuntimeSearchParam> componentParams = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef);
312                        RuntimeSearchParam left = componentParams.get(0);
313                        IQueryParameterType leftValue = cp.getLeftValue();
314                        Condition leftPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, left, leftValue, theRequestPartitionId, theSqlBuilder);
315
316                        RuntimeSearchParam right = componentParams.get(1);
317                        IQueryParameterType rightValue = cp.getRightValue();
318                        Condition rightPredicate = createPredicateCompositePart(theSourceJoinColumn, theResourceName, theSpnamePrefix, right, rightValue, theRequestPartitionId, theSqlBuilder);
319
320                        Condition andCondition = toAndPredicate(leftPredicate, rightPredicate);
321
322                        if (orCondidtion == null) {
323                                orCondidtion = toOrPredicate(andCondition);
324                        } else {
325                                orCondidtion = toOrPredicate(orCondidtion, andCondition);
326                        }
327                }
328
329                return orCondidtion;
330        }
331
332        private Condition createPredicateCompositePart(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, RuntimeSearchParam theParam, IQueryParameterType theParamValue, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
333
334                switch (theParam.getParamType()) {
335                        case STRING: {
336                                return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder);
337                        }
338                        case TOKEN: {
339                                return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder);
340                        }
341                        case DATE: {
342                                return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), toOperation(((DateParam) theParamValue).getPrefix()), theRequestPartitionId, theSqlBuilder);
343                        }
344                        case QUANTITY: {
345                                return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParam, Collections.singletonList(theParamValue), null, theRequestPartitionId, theSqlBuilder);
346                        }
347                        case NUMBER:
348                        case REFERENCE:
349                        case COMPOSITE:
350                        case URI:
351                        case HAS:
352                        case SPECIAL:
353                        default:
354                                throw new InvalidRequestException(Msg.code(1204) + "Don't know how to handle composite parameter with type of " + theParam.getParamType());
355                }
356
357        }
358
359        public Condition createPredicateCoords(@Nullable DbColumn theSourceJoinColumn,
360                                                                                                                String theResourceName,
361                                                                                                                RuntimeSearchParam theSearchParam,
362                                                                                                                List<? extends IQueryParameterType> theList,
363                                                                                                                RequestPartitionId theRequestPartitionId) {
364
365                CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.COORDS, theSourceJoinColumn, theSearchParam.getName(), () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn)).getResult();
366
367                if (theList.get(0).getMissing() != null) {
368                        return predicateBuilder.createPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), theRequestPartitionId);
369                }
370
371                List<Condition> codePredicates = new ArrayList<>();
372                for (IQueryParameterType nextOr : theList) {
373                        Condition singleCode = predicateBuilder.createPredicateCoords(mySearchParameters, nextOr, theResourceName, theSearchParam, predicateBuilder, theRequestPartitionId);
374                        codePredicates.add(singleCode);
375                }
376
377                return predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
378        }
379
380        public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
381                                                                                                         String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
382                                                                                                         SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
383                return createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
384        }
385        public Condition createPredicateDate(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
386                                                                                                         String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
387                                                                                                         SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
388
389                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
390
391                PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.DATE, theSourceJoinColumn, paramName, () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn));
392                DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult();
393                boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
394
395                if (theList.get(0).getMissing() != null) {
396                        Boolean missing = theList.get(0).getMissing();
397                        return predicateBuilder.createPredicateParamMissingForNonReference(theResourceName, paramName, missing, theRequestPartitionId);
398                }
399
400                List<Condition> codePredicates = new ArrayList<>();
401
402                for (IQueryParameterType nextOr : theList) {
403                        Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation);
404                        codePredicates.add(p);
405                }
406
407                Condition predicate = toOrPredicate(codePredicates);
408
409                if (!cacheHit) {
410                        predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate);
411                        predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
412                }
413
414                return predicate;
415
416        }
417
418        private Condition createPredicateFilter(QueryStack theQueryStack3, SearchFilterParser.Filter theFilter, String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
419
420                if (theFilter instanceof SearchFilterParser.FilterParameter) {
421                        return createPredicateFilter(theQueryStack3, (SearchFilterParser.FilterParameter) theFilter, theResourceName, theRequest, theRequestPartitionId);
422                } else if (theFilter instanceof SearchFilterParser.FilterLogical) {
423                        // Left side
424                        Condition xPredicate = createPredicateFilter(theQueryStack3, ((SearchFilterParser.FilterLogical) theFilter).getFilter1(), theResourceName, theRequest, theRequestPartitionId);
425
426                        // Right side
427                        Condition yPredicate = createPredicateFilter(theQueryStack3, ((SearchFilterParser.FilterLogical) theFilter).getFilter2(), theResourceName, theRequest, theRequestPartitionId);
428
429                        if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.and) {
430                                return ComboCondition.and(xPredicate, yPredicate);
431                        } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.or) {
432                                return ComboCondition.or(xPredicate, yPredicate);
433                        } else {
434                                // Shouldn't happen
435                                throw new InvalidRequestException(Msg.code(1205) + "Don't know how to handle operation " + ((SearchFilterParser.FilterLogical) theFilter).getOperation());
436                        }
437                } else {
438                        return createPredicateFilter(theQueryStack3, ((SearchFilterParser.FilterParameterGroup) theFilter).getContained(), theResourceName, theRequest, theRequestPartitionId);
439                }
440        }
441
442        private Condition createPredicateFilter(QueryStack theQueryStack3, SearchFilterParser.FilterParameter theFilter, String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
443
444                String paramName = theFilter.getParamPath().getName();
445
446                switch (paramName) {
447                        case IAnyResource.SP_RES_ID: {
448                                TokenParam param = new TokenParam();
449                                param.setValueAsQueryToken(null, null, null, theFilter.getValue());
450                                return theQueryStack3.createPredicateResourceId(null, Collections.singletonList(Collections.singletonList(param)), theResourceName, theFilter.getOperation(), theRequestPartitionId);
451                        }
452                        case Constants.PARAM_SOURCE: {
453                                TokenParam param = new TokenParam();
454                                param.setValueAsQueryToken(null, null, null, theFilter.getValue());
455                                return createPredicateSource(null, Collections.singletonList(param));
456                        }
457                        default:
458                                RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramName);
459                                if (searchParam == null) {
460                                        Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName);
461                                        String msg = myFhirContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSearchParameter", paramName, theResourceName, validNames);
462                                        throw new InvalidRequestException(Msg.code(1206) + msg);
463                                }
464                                RestSearchParameterTypeEnum typeEnum = searchParam.getParamType();
465                                if (typeEnum == RestSearchParameterTypeEnum.URI) {
466                                        return theQueryStack3.createPredicateUri(null, theResourceName, null, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequest, theRequestPartitionId);
467                                } else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
468                                        return theQueryStack3.createPredicateString(null, theResourceName, null, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
469                                } else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
470                                        return theQueryStack3.createPredicateDate(null, theResourceName, null, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
471                                } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
472                                        return theQueryStack3.createPredicateNumber(null, theResourceName, null, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
473                                } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
474                                        SearchFilterParser.CompareOperation operation = theFilter.getOperation();
475                                        String resourceType = null; // The value can either have (Patient/123) or not have (123) a resource type, either way it's not needed here
476                                        String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
477                                        String value = theFilter.getValue();
478                                        ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
479                                        return theQueryStack3.createPredicateReference(null, theResourceName, paramName, new ArrayList<>(), Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
480                                } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
481                                        return theQueryStack3.createPredicateQuantity(null, theResourceName, null, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
482                                } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
483                                        throw new InvalidRequestException(Msg.code(1207) + "Composite search parameters not currently supported with _filter clauses");
484                                } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) {
485                                        TokenParam param = new TokenParam();
486                                        param.setValueAsQueryToken(null,
487                                                null,
488                                                null,
489                                                theFilter.getValue());
490                                        return theQueryStack3.createPredicateToken(null, theResourceName, null, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId);
491                                }
492                                break;
493                }
494                return null;
495        }
496
497        private Condition createPredicateHas(@Nullable DbColumn theSourceJoinColumn, String theResourceType, List<List<IQueryParameterType>> theHasParameters, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
498
499                List<Condition> andPredicates = new ArrayList<>();
500                for (List<? extends IQueryParameterType> nextOrList : theHasParameters) {
501
502                        String targetResourceType = null;
503                        String paramReference = null;
504                        String parameterName = null;
505
506                        String paramName = null;
507                        List<QualifiedParamList> parameters = new ArrayList<>();
508                        for (IQueryParameterType nextParam : nextOrList) {
509                                HasParam next = (HasParam) nextParam;
510                                targetResourceType = next.getTargetResourceType();
511                                paramReference = next.getReferenceFieldName();
512                                parameterName = next.getParameterName();
513                                paramName = parameterName.replaceAll("\\..*", "");
514                                parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myFhirContext)));
515                        }
516
517                        if (paramName == null) {
518                                continue;
519                        }
520
521                        try {
522                                myFhirContext.getResourceDefinition(targetResourceType);
523                        } catch (DataFormatException e) {
524                                throw new InvalidRequestException(Msg.code(1208) + "Invalid resource type: " + targetResourceType);
525                        }
526
527                        ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
528
529                        if (paramName.startsWith("_has:")) {
530
531                                ourLog.trace("Handing double _has query: {}", paramName);
532
533                                String qualifier = paramName.substring(4);
534                                for (IQueryParameterType next : nextOrList) {
535                                        HasParam nextHasParam = new HasParam();
536                                        nextHasParam.setValueAsQueryToken(myFhirContext, Constants.PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext));
537                                        orValues.add(nextHasParam);
538                                }
539
540                        } else {
541
542                                //Ensure that the name of the search param
543                                // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val)
544                                // exists on the target resource type.
545                                RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramName);
546
547                                //Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in Patient?_has:Observation:subject:code=sys|val)
548                                //exists on the target resource, or in the top-level Resource resource.
549                                mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramReference);
550
551
552                                IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams(mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters);
553
554                                for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) {
555                                        orValues.addAll(next.getValuesAsQueryTokens());
556                                }
557
558                        }
559
560                        //Handle internal chain inside the has.
561                        if (parameterName.contains(".")) {
562                                String chainedPartOfParameter = getChainedPart(parameterName);
563                                orValues.stream()
564                                        .filter(qp -> qp instanceof ReferenceParam)
565                                        .map(qp -> (ReferenceParam) qp)
566                                        .forEach(rp -> rp.setChain(getChainedPart(chainedPartOfParameter)));
567
568                                parameterName = parameterName.substring(0, parameterName.indexOf('.'));
569                        }
570
571                        int colonIndex = parameterName.indexOf(':');
572                        if (colonIndex != -1) {
573                                parameterName = parameterName.substring(0, colonIndex);
574                        }
575
576                        ResourceLinkPredicateBuilder join = mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
577                        Condition partitionPredicate = join.createPartitionIdPredicate(theRequestPartitionId);
578
579                        List<String> paths = join.createResourceLinkPaths(targetResourceType, paramReference, new ArrayList<>());
580                        Condition typePredicate = BinaryCondition.equalTo(join.getColumnTargetResourceType(), mySqlBuilder.generatePlaceholder(theResourceType));
581                        Condition pathPredicate = toEqualToOrInPredicate(join.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
582                        Condition linkedPredicate = searchForIdsWithAndOr(join.getColumnSrcResourceId(), targetResourceType, parameterName, Collections.singletonList(orValues), theRequest, theRequestPartitionId, SearchContainedModeEnum.FALSE);
583                        andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate));
584                }
585
586                return toAndPredicate(andPredicates);
587        }
588
589        public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
590                                                                                                                String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
591                                                                                                                SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
592                return createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
593        }
594
595        public Condition createPredicateNumber(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
596                                                                                                                String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
597                                                                                                                SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
598
599                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
600
601                NumberPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.NUMBER, theSourceJoinColumn, paramName, () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn)).getResult();
602
603                if (theList.get(0).getMissing() != null) {
604                        return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
605                }
606
607                List<Condition> codePredicates = new ArrayList<>();
608                for (IQueryParameterType nextOr : theList) {
609
610                        if (nextOr instanceof NumberParam) {
611                                NumberParam param = (NumberParam) nextOr;
612
613                                BigDecimal value = param.getValue();
614                                if (value == null) {
615                                        continue;
616                                }
617
618                                SearchFilterParser.CompareOperation operation = theOperation;
619                                if (operation == null) {
620                                        operation = toOperation(param.getPrefix());
621                                }
622
623
624                                Condition predicate = join.createPredicateNumeric(theResourceName, paramName, operation, value, theRequestPartitionId, nextOr);
625                                codePredicates.add(predicate);
626
627                        } else {
628                                throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass());
629                        }
630
631                }
632
633                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
634        }
635
636        public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
637                                                                                                                  String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
638                                                                                                                  SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
639                return createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
640        }
641
642        public Condition createPredicateQuantity(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
643                                                                                                                  String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
644                                                                                                                  SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
645
646                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
647
648                if (theList.get(0).getMissing() != null) {
649                        QuantityBasePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, theSearchParam.getName(), () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult();
650                        return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
651                }
652
653                List<QuantityParam> quantityParams = theList
654                        .stream()
655                        .map(t -> QuantityParam.toQuantityParam(t))
656                        .collect(Collectors.toList());
657
658                QuantityBasePredicateBuilder join = null;
659                boolean normalizedSearchEnabled = myModelConfig.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
660                if (normalizedSearchEnabled) {
661                        List<QuantityParam> normalizedQuantityParams = quantityParams
662                                .stream()
663                                .map(t -> UcumServiceUtil.toCanonicalQuantityOrNull(t))
664                                .filter(t -> t != null)
665                                .collect(Collectors.toList());
666
667                        if (normalizedQuantityParams.size() == quantityParams.size()) {
668                                join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn)).getResult();
669                                quantityParams = normalizedQuantityParams;
670                        }
671                }
672
673                if (join == null) {
674                        join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.QUANTITY, theSourceJoinColumn, paramName, () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn)).getResult();
675                }
676
677                List<Condition> codePredicates = new ArrayList<>();
678                for (QuantityParam nextOr : quantityParams) {
679                        Condition singleCode = join.createPredicateQuantity(nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId);
680                        codePredicates.add(singleCode);
681                }
682
683                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
684        }
685
686        public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
687                                                                                                                        String theResourceName,
688                                                                                                                        String theParamName,
689                                                                                                                        List<String> theQualifiers,
690                                                                                                                        List<? extends IQueryParameterType> theList,
691                                                                                                                        SearchFilterParser.CompareOperation theOperation,
692                                                                                                                        RequestDetails theRequest,
693                                                                                                                        RequestPartitionId theRequestPartitionId) {
694                return createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequest, theRequestPartitionId, mySqlBuilder);
695        }
696
697        public Condition createPredicateReference(@Nullable DbColumn theSourceJoinColumn,
698                                                                                                                        String theResourceName,
699                                                                                                                        String theParamName,
700                                                                                                                        List<String> theQualifiers,
701                                                                                                                        List<? extends IQueryParameterType> theList,
702                                                                                                                        SearchFilterParser.CompareOperation theOperation,
703                                                                                                                        RequestDetails theRequest,
704                                                                                                                        RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
705
706                if ((theOperation != null) &&
707                        (theOperation != SearchFilterParser.CompareOperation.eq) &&
708                        (theOperation != SearchFilterParser.CompareOperation.ne)) {
709                        throw new InvalidRequestException(Msg.code(1212) + "Invalid operator specified for reference predicate.  Supported operators for reference predicate are \"eq\" and \"ne\".");
710                }
711
712                if (theList.get(0).getMissing() != null) {
713                        SearchParamPresentPredicateBuilder join = theSqlBuilder.addSearchParamPresentPredicateBuilder(theSourceJoinColumn);
714                        return join.createPredicateParamMissingForReference(theResourceName, theParamName, theList.get(0).getMissing(), theRequestPartitionId);
715
716                }
717
718                ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.REFERENCE, theSourceJoinColumn, theParamName, () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn)).getResult();
719                return predicateBuilder.createPredicate(theRequest, theResourceName, theParamName, theQualifiers, theList, theOperation, theRequestPartitionId);
720        }
721
722        private class ChainElement {
723                private final String myResourceType;
724                private final String mySearchParameterName;
725                private final String myPath;
726
727                public ChainElement(String theResourceType, String theSearchParameterName, String thePath) {
728                        this.myResourceType = theResourceType;
729                        this.mySearchParameterName = theSearchParameterName;
730                        this.myPath = thePath;
731                }
732
733                public String getResourceType() {
734                        return myResourceType;
735                }
736
737                public String getPath() { return myPath; }
738                
739                public String getSearchParameterName() { return mySearchParameterName; }
740
741                @Override
742                public boolean equals(Object o) {
743                        if (this == o) return true;
744                        if (o == null || getClass() != o.getClass()) return false;
745                        ChainElement that = (ChainElement) o;
746                        return myResourceType.equals(that.myResourceType) && mySearchParameterName.equals(that.mySearchParameterName) && myPath.equals(that.myPath);
747                }
748
749                @Override
750                public int hashCode() {
751                        return Objects.hash(myResourceType, mySearchParameterName, myPath);
752                }
753        }
754
755        private class ReferenceChainExtractor {
756                private final Map<List<ChainElement>,Set<LeafNodeDefinition>> myChains = Maps.newHashMap();
757
758                public Map<List<ChainElement>,Set<LeafNodeDefinition>> getChains() { return myChains; }
759
760                private boolean isReferenceParamValid(ReferenceParam theReferenceParam) {
761                        return split(theReferenceParam.getChain(), '.').length <= 3;
762                }
763
764                private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) {
765                        List<String> pathsForType = theSearchParam.getPathsSplit().stream()
766                                .map(String::trim)
767                                .filter(t -> t.startsWith(theResourceType))
768                                .collect(Collectors.toList());
769                        if (pathsForType.isEmpty()) {
770                                ourLog.warn("Search parameter {} does not have a path for resource type {}.", theSearchParam.getName(), theResourceType);
771                        }
772
773                        return pathsForType;
774                }
775
776                public void deriveChains(String theResourceType, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList) {
777                        List<String> paths = extractPaths(theResourceType, theSearchParam);
778                        for (String path : paths) {
779                                List<ChainElement> searchParams = Lists.newArrayList();
780                                searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path));
781                                for (IQueryParameterType nextOr : theList) {
782                                        String targetValue = nextOr.getValueAsQueryToken(myFhirContext);
783                                        if (nextOr instanceof ReferenceParam) {
784                                                ReferenceParam referenceParam = (ReferenceParam) nextOr;
785
786                                                if (!isReferenceParamValid(referenceParam)) {
787                                                        throw new InvalidRequestException(Msg.code(2007) +
788                                                                "The search chain " + theSearchParam.getName() + "." + referenceParam.getChain() +
789                                                                " is too long. Only chains up to three references are supported.");
790                                                }
791
792                                                String targetChain = referenceParam.getChain();
793                                                List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType());
794
795                                                processNextLinkInChain(searchParams, theSearchParam, targetChain, targetValue, qualifiers, referenceParam.getResourceType());
796
797                                        }
798                                }
799                        }
800                }
801
802                private void processNextLinkInChain(List<ChainElement> theSearchParams, RuntimeSearchParam thePreviousSearchParam, String theChain, String theTargetValue, List<String> theQualifiers, String theResourceType) {
803
804                        String nextParamName = theChain;
805                        String nextChain = null;
806                        String nextQualifier = null;
807                        int linkIndex = theChain.indexOf('.');
808                        if (linkIndex != -1) {
809                                nextParamName = theChain.substring(0, linkIndex);
810                                nextChain = theChain.substring(linkIndex+1);
811                        }
812
813                        int qualifierIndex = nextParamName.indexOf(':');
814                        if (qualifierIndex != -1) {
815                                nextParamName = nextParamName.substring(0, qualifierIndex);
816                                nextQualifier = nextParamName.substring(qualifierIndex);
817                        }
818
819                        List<String> qualifiersBranch = Lists.newArrayList();
820                        qualifiersBranch.addAll(theQualifiers);
821                        qualifiersBranch.add(nextQualifier);
822
823                        boolean searchParamFound = false;
824                        for (String nextTarget : thePreviousSearchParam.getTargets()) {
825                                RuntimeSearchParam nextSearchParam = null;
826                                if (StringUtils.isBlank(theResourceType) || theResourceType.equals(nextTarget)) {
827                                        nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName);
828                                }
829                                if (nextSearchParam != null) {
830                                        searchParamFound = true;
831                                        // If we find a search param on this resource type for this parameter name, keep iterating
832                                        //  Otherwise, abandon this branch and carry on to the next one
833                                        if (StringUtils.isEmpty(nextChain)) {
834                                                // We've reached the end of the chain
835                                                ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
836
837                                                if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) {
838                                                        orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue));
839                                                } else {
840                                                        IQueryParameterType qp = toParameterType(nextSearchParam);
841                                                        qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue);
842                                                        orValues.add(qp);
843                                                }
844
845                                                Set<LeafNodeDefinition> leafNodes = myChains.get(theSearchParams);
846                                                if (leafNodes == null) {
847                                                        leafNodes = Sets.newHashSet();
848                                                        myChains.put(theSearchParams, leafNodes);
849                                                }
850                                                leafNodes.add(new LeafNodeDefinition(nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch));
851                                        } else {
852                                                List<String> nextPaths = extractPaths(nextTarget, nextSearchParam);
853                                                for (String nextPath : nextPaths) {
854                                                        List<ChainElement> searchParamBranch = Lists.newArrayList();
855                                                        searchParamBranch.addAll(theSearchParams);
856
857                                                        searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath));
858                                                        processNextLinkInChain(searchParamBranch, nextSearchParam, nextChain, theTargetValue, qualifiersBranch, nextQualifier);
859                                                }
860                                        }
861                                }
862                        }
863                        if (!searchParamFound) {
864                                throw new InvalidRequestException(Msg.code(1214) + myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidParameterChain", thePreviousSearchParam.getName() + '.' + theChain));
865                        }
866                }
867        }
868
869        private static class LeafNodeDefinition {
870                private final RuntimeSearchParam myParamDefinition;
871                private final ArrayList<IQueryParameterType> myOrValues;
872                private final String myLeafTarget;
873                private final String myLeafParamName;
874                private final String myLeafPathPrefix;
875                private final List<String> myQualifiers;
876
877                public LeafNodeDefinition(RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, String theLeafTarget, String theLeafParamName, String theLeafPathPrefix, List<String> theQualifiers) {
878                        myParamDefinition = theParamDefinition;
879                        myOrValues = theOrValues;
880                        myLeafTarget = theLeafTarget;
881                        myLeafParamName = theLeafParamName;
882                        myLeafPathPrefix = theLeafPathPrefix;
883                        myQualifiers = theQualifiers;
884                }
885
886                public RuntimeSearchParam getParamDefinition() {
887                        return myParamDefinition;
888                }
889
890                public ArrayList<IQueryParameterType> getOrValues() {
891                        return myOrValues;
892                }
893
894                public String getLeafTarget() {
895                        return myLeafTarget;
896                }
897
898                public String getLeafParamName() {
899                        return myLeafParamName;
900                }
901
902                public String getLeafPathPrefix() {
903                        return myLeafPathPrefix;
904                }
905
906                public List<String> getQualifiers() {
907                        return myQualifiers;
908                }
909
910                public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) {
911                        return new LeafNodeDefinition(myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers);
912                }
913
914                @Override
915                public boolean equals(Object o) {
916                        if (this == o) return true;
917                        if (o == null || getClass() != o.getClass()) return false;
918                        LeafNodeDefinition that = (LeafNodeDefinition) o;
919                        return Objects.equals(myParamDefinition, that.myParamDefinition) && Objects.equals(myOrValues, that.myOrValues) && Objects.equals(myLeafTarget, that.myLeafTarget) && Objects.equals(myLeafParamName, that.myLeafParamName) && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix) && Objects.equals(myQualifiers, that.myQualifiers);
920                }
921
922                @Override
923                public int hashCode() {
924                        return Objects.hash(myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers);
925                }
926        }
927
928        public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
929                                                                                                                                                                          String theResourceName, RuntimeSearchParam theSearchParam,
930                                                                                                                                                                          List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
931                                                                                                                                                                          RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
932                // A bit of a hack, but we need to turn off cache reuse while in this method so that we don't try to reuse builders across different subselects
933                EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes = EnumSet.copyOf(myReusePredicateBuilderTypes);
934                myReusePredicateBuilderTypes.clear();
935
936                UnionQuery union = new UnionQuery(SetOperationQuery.Type.UNION_ALL);
937
938                ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor();
939                chainExtractor.deriveChains(theResourceName, theSearchParam, theList);
940                Map<List<ChainElement>,Set<LeafNodeDefinition>> chains = chainExtractor.getChains();
941
942                Map<List<String>,Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap();
943                for (List<ChainElement> nextChain : chains.keySet()) {
944                        Set<LeafNodeDefinition> leafNodes = chains.get(nextChain);
945
946                        collateChainedSearchOptions(referenceLinks, nextChain, leafNodes);
947                }
948
949                for (List<String> nextReferenceLink: referenceLinks.keySet()) {
950                        for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) {
951                                SearchQueryBuilder builder = mySqlBuilder.newChildSqlBuilder();
952                                DbColumn previousJoinColumn = null;
953
954                                // Create a reference link predicate to the subselect for every link but the last one
955                                for (String nextLink : nextReferenceLink) {
956                                        // We don't want to call createPredicateReference() here, because the whole point is to avoid the recursion.
957                                        // TODO: Are we missing any important business logic from that method? All tests are passing.
958                                        ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = builder.addReferencePredicateBuilder(this, previousJoinColumn);
959                                        builder.addPredicate(resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink)));
960                                        previousJoinColumn = resourceLinkPredicateBuilder.getColumnTargetResourceId();
961                                }
962
963                                Condition containedCondition = createIndexPredicate(
964                                        previousJoinColumn,
965                                        leafNodeDefinition.getLeafTarget(),
966                                        leafNodeDefinition.getLeafPathPrefix(),
967                                        leafNodeDefinition.getLeafParamName(),
968                                        leafNodeDefinition.getParamDefinition(),
969                                        leafNodeDefinition.getOrValues(),
970                                        theOperation,
971                                        leafNodeDefinition.getQualifiers(),
972                                        theRequest,
973                                        theRequestPartitionId,
974                                        builder);
975
976                                builder.addPredicate(containedCondition);
977
978                                union.addQueries(builder.getSelect());
979                        }
980                }
981
982                InCondition inCondition;
983                if (theSourceJoinColumn == null) {
984                        inCondition = new InCondition(mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union);
985                } else {
986                        //-- for the resource link, need join with target_resource_id
987                        inCondition = new InCondition(theSourceJoinColumn, union);
988                }
989
990                // restore the state of this collection to turn caching back on before we exit
991                myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes);
992                return inCondition;
993        }
994
995        private void collateChainedSearchOptions(Map<List<String>, Set<LeafNodeDefinition>> referenceLinks, List<ChainElement> nextChain, Set<LeafNodeDefinition> leafNodes) {
996                // Manually collapse the chain using all possible variants of contained resource patterns.
997                // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this someday?
998                // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper support for `_contained`
999                if (nextChain.size() == 1) {
1000                        // discrete -> discrete
1001                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes);
1002                        // discrete -> contained
1003                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(),
1004                                leafNodes
1005                                        .stream()
1006                                        .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName()))
1007                                        .collect(Collectors.toSet()));
1008                } else if (nextChain.size() == 2) {
1009                        // discrete -> discrete -> discrete
1010                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), nextChain.get(1).getPath()), leafNodes);
1011                        // discrete -> discrete -> contained
1012                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()),
1013                                leafNodes
1014                                        .stream()
1015                                        .map(t -> t.withPathPrefix(nextChain.get(1).getResourceType(), nextChain.get(1).getSearchParameterName()))
1016                                        .collect(Collectors.toSet()));
1017                        // discrete -> contained -> discrete
1018                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath())), leafNodes);
1019                        if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
1020                                // discrete -> contained -> contained
1021                                updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(),
1022                                        leafNodes
1023                                                .stream()
1024                                                .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName() + "." + nextChain.get(1).getSearchParameterName()))
1025                                                .collect(Collectors.toSet()));
1026                        }
1027                } else if (nextChain.size() == 3) {
1028                        // discrete -> discrete -> discrete -> discrete
1029                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), nextChain.get(1).getPath(), nextChain.get(2).getPath()), leafNodes);
1030                        // discrete -> discrete -> discrete -> contained
1031                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1032                                leafNodes
1033                                        .stream()
1034                                        .map(t -> t.withPathPrefix(nextChain.get(2).getResourceType(), nextChain.get(2).getSearchParameterName()))
1035                                        .collect(Collectors.toSet()));
1036                        // discrete -> discrete -> contained -> discrete
1037                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath(), mergePaths(nextChain.get(1).getPath(), nextChain.get(2).getPath())), leafNodes);
1038                        // discrete -> contained -> discrete -> discrete
1039                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath()), nextChain.get(2).getPath()), leafNodes);
1040                        // discrete -> contained -> discrete -> contained
1041                        updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath())),
1042                                leafNodes
1043                                        .stream()
1044                                        .map(t -> t.withPathPrefix(nextChain.get(2).getResourceType(), nextChain.get(2).getSearchParameterName()))
1045                                        .collect(Collectors.toSet()));
1046                        if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
1047                                // discrete -> contained -> contained -> discrete
1048                                updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(mergePaths(nextChain.get(0).getPath(), nextChain.get(1).getPath(), nextChain.get(2).getPath())), leafNodes);
1049                                // discrete -> discrete -> contained -> contained
1050                                updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()),
1051                                        leafNodes
1052                                                .stream()
1053                                                .map(t -> t.withPathPrefix(nextChain.get(1).getResourceType(), nextChain.get(1).getSearchParameterName() + "." + nextChain.get(2).getSearchParameterName()))
1054                                                .collect(Collectors.toSet()));
1055                                // discrete -> contained -> contained -> contained
1056                                updateMapOfReferenceLinks(referenceLinks, Lists.newArrayList(),
1057                                        leafNodes
1058                                                .stream()
1059                                                .map(t -> t.withPathPrefix(nextChain.get(0).getResourceType(), nextChain.get(0).getSearchParameterName() + "." + nextChain.get(1).getSearchParameterName() + "." + nextChain.get(2).getSearchParameterName()))
1060                                                .collect(Collectors.toSet()));
1061                        }
1062                } else {
1063                        // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever needs this, we should revisit the approach
1064                        throw new InvalidRequestException(Msg.code(2011) +
1065                                "The search chain is too long. Only chains of up to three references are supported.");
1066                }
1067        }
1068
1069        private void updateMapOfReferenceLinks(Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap, ArrayList<String> thePath, Set<LeafNodeDefinition> theLeafNodesToAdd) {
1070                Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.get(thePath);
1071                if (leafNodes == null) {
1072                        leafNodes = Sets.newHashSet();
1073                        theReferenceLinksMap.put(thePath, leafNodes);
1074                }
1075                leafNodes.addAll(theLeafNodesToAdd);
1076        }
1077
1078        private String mergePaths(String... paths) {
1079                String result = "";
1080                for (String nextPath : paths) {
1081                        int separatorIndex = nextPath.indexOf('.');
1082                        if (StringUtils.isEmpty(result)) {
1083                                result = nextPath;
1084                        } else {
1085                                result = result + nextPath.substring(separatorIndex);
1086                        }
1087                }
1088                return result;
1089        }
1090
1091        private Condition createIndexPredicate(DbColumn theSourceJoinColumn, String theResourceName, String theSpnamePrefix, String theParamName, RuntimeSearchParam theParamDefinition, ArrayList<IQueryParameterType> theOrValues, SearchFilterParser.CompareOperation theOperation, List<String> theQualifiers, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
1092                Condition containedCondition;
1093
1094                switch (theParamDefinition.getParamType()) {
1095                        case DATE:
1096                                containedCondition = createPredicateDate(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1097                                        theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
1098                                break;
1099                        case NUMBER:
1100                                containedCondition = createPredicateNumber(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1101                                        theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
1102                                break;
1103                        case QUANTITY:
1104                                containedCondition = createPredicateQuantity(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1105                                        theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
1106                                break;
1107                        case STRING:
1108                                containedCondition = createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1109                                        theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
1110                                break;
1111                        case TOKEN:
1112                                containedCondition = createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1113                                        theOrValues, theOperation, theRequestPartitionId, theSqlBuilder);
1114                                break;
1115                        case COMPOSITE:
1116                                containedCondition = createPredicateComposite(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1117                                        theOrValues, theRequestPartitionId, theSqlBuilder);
1118                                break;
1119                        case URI:
1120                                containedCondition = createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theParamDefinition,
1121                                        theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
1122                                break;
1123                        case REFERENCE:
1124                                containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, StringUtils.isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName, theQualifiers,
1125                                        theOrValues, theOperation, theRequest, theRequestPartitionId, theSqlBuilder);
1126                                break;
1127                        case HAS:
1128                        case SPECIAL:
1129                        default:
1130                                throw new InvalidRequestException(
1131                                        Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported.");
1132                }
1133                return containedCondition;
1134        }
1135
1136        @Nullable
1137        public Condition createPredicateResourceId(@Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theValues, String theResourceName, SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
1138                ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder();
1139                return builder.createPredicateResourceId(theSourceJoinColumn, theResourceName, theValues, theOperation, theRequestPartitionId);
1140        }
1141
1142        private Condition createPredicateSourceForAndList(@Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) {
1143                List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size());
1144                for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1145                        andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd));
1146                }
1147                return toAndPredicate(andPredicates);
1148        }
1149
1150        private Condition createPredicateSource(@Nullable DbColumn theSourceJoinColumn, List<? extends IQueryParameterType> theList) {
1151                if (myDaoConfig.getStoreMetaSourceInformation() == DaoConfig.StoreMetaSourceInformationEnum.NONE) {
1152                        String msg = myFhirContext.getLocalizer().getMessage(LegacySearchBuilder.class, "sourceParamDisabled");
1153                        throw new InvalidRequestException(Msg.code(1216) + msg);
1154                }
1155
1156                SourcePredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.SOURCE, theSourceJoinColumn, Constants.PARAM_SOURCE, () -> mySqlBuilder.addSourcePredicateBuilder(theSourceJoinColumn)).getResult();
1157
1158                List<Condition> orPredicates = new ArrayList<>();
1159                for (IQueryParameterType nextParameter : theList) {
1160                        SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext));
1161                        String sourceUri = sourceParameter.getSourceUri();
1162                        String requestId = sourceParameter.getRequestId();
1163                        if (isNotBlank(sourceUri) && isNotBlank(requestId)) {
1164                                orPredicates.add(toAndPredicate(
1165                                        join.createPredicateSourceUri(sourceUri),
1166                                        join.createPredicateRequestId(requestId)
1167                                ));
1168                        } else if (isNotBlank(sourceUri)) {
1169                                orPredicates.add(join.createPredicateSourceUri(sourceUri));
1170                        } else if (isNotBlank(requestId)) {
1171                                orPredicates.add(join.createPredicateRequestId(requestId));
1172                        }
1173                }
1174
1175                return toOrPredicate(orPredicates);
1176        }
1177
1178        public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
1179                                                                                                                String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
1180                                                                                                                SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
1181                return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
1182        }
1183
1184        public Condition createPredicateString(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
1185                                                                                                                String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
1186                                                                                                                SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId,
1187                                                                                                                SearchQueryBuilder theSqlBuilder) {
1188
1189                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1190
1191                StringPredicateBuilder join = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.STRING, theSourceJoinColumn, paramName, () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn)).getResult();
1192
1193                if (theList.get(0).getMissing() != null) {
1194                        return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
1195                }
1196
1197                List<Condition> codePredicates = new ArrayList<>();
1198                for (IQueryParameterType nextOr : theList) {
1199                        Condition singleCode = join.createPredicateString(nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation);
1200                        codePredicates.add(singleCode);
1201                }
1202
1203                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates));
1204        }
1205
1206        public Condition createPredicateTag(@Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theList, String theParamName, RequestPartitionId theRequestPartitionId) {
1207                TagTypeEnum tagType;
1208                if (Constants.PARAM_TAG.equals(theParamName)) {
1209                        tagType = TagTypeEnum.TAG;
1210                } else if (Constants.PARAM_PROFILE.equals(theParamName)) {
1211                        tagType = TagTypeEnum.PROFILE;
1212                } else if (Constants.PARAM_SECURITY.equals(theParamName)) {
1213                        tagType = TagTypeEnum.SECURITY_LABEL;
1214                } else {
1215                        throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen
1216                }
1217
1218                List<Condition> andPredicates = new ArrayList<>();
1219                for (List<? extends IQueryParameterType> nextAndParams : theList) {
1220                        if ( ! checkHaveTags(nextAndParams, theParamName)) { continue; }
1221
1222                        List<Triple<String, String, String>> tokens = Lists.newArrayList();
1223                        boolean paramInverted = populateTokens(tokens, nextAndParams);
1224                        if (tokens.isEmpty()) { continue; }
1225
1226                        Condition tagPredicate;
1227                        BaseJoiningPredicateBuilder join;
1228                        if (paramInverted) {
1229
1230                                SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder();
1231                                TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null);
1232                                sqlBuilder.addPredicate(tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId));
1233                                SelectQuery sql = sqlBuilder.getSelect();
1234
1235                                join = mySqlBuilder.getOrCreateFirstPredicateBuilder();
1236                                Expression subSelect = new Subquery(sql);
1237                                tagPredicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true);
1238
1239                        } else {
1240                                // Tag table can't be a query root because it will include deleted resources, and can't select by resource type
1241                                mySqlBuilder.getOrCreateFirstPredicateBuilder();
1242
1243                                TagPredicateBuilder tagJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TAG, theSourceJoinColumn, theParamName, () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn)).getResult();
1244                                tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId);
1245                                join = tagJoin;
1246                        }
1247
1248                        andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate));
1249                }
1250
1251                return toAndPredicate(andPredicates);
1252        }
1253
1254        private boolean populateTokens(List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) {
1255                boolean paramInverted = false;
1256
1257                for (IQueryParameterType nextOrParam : theAndParams) {
1258                        String code;
1259                        String system;
1260                        if (nextOrParam instanceof TokenParam) {
1261                                TokenParam nextParam = (TokenParam) nextOrParam;
1262                                code = nextParam.getValue();
1263                                system = nextParam.getSystem();
1264                                if (nextParam.getModifier() == TokenParamModifier.NOT) {
1265                                        paramInverted = true;
1266                                }
1267                        } else {
1268                                UriParam nextParam = (UriParam) nextOrParam;
1269                                code = nextParam.getValue();
1270                                system = null;
1271                        }
1272
1273                        if (isNotBlank(code)) {
1274                                theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code));
1275                        }
1276                }
1277                return paramInverted;
1278        }
1279
1280        private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) {
1281                for (IQueryParameterType nextParamUncasted : theParams) {
1282                        if (nextParamUncasted instanceof TokenParam) {
1283                                TokenParam nextParam = (TokenParam) nextParamUncasted;
1284                                if (isNotBlank(nextParam.getValue())) { return true; }
1285                                if (isNotBlank(nextParam.getSystem())) {
1286                                        throw new InvalidRequestException(Msg.code(1218) + "Invalid " + theParamName +
1287                                                " parameter (must supply a value/code and not just a system): " + nextParam.getValueAsQueryToken(myFhirContext));
1288                                }
1289                        }
1290
1291                        UriParam nextParam = (UriParam) nextParamUncasted;
1292                        if (isNotBlank(nextParam.getValue())) { return true; }
1293                }
1294
1295                return false;
1296        }
1297
1298        public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
1299                                                                                                          String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
1300                                                                                                          SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId) {
1301                return createPredicateToken(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestPartitionId, mySqlBuilder);
1302        }
1303
1304        public Condition createPredicateToken(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
1305                                                                                                          String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
1306                                                                                                          SearchFilterParser.CompareOperation theOperation, RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
1307
1308                List<IQueryParameterType> tokens = new ArrayList<>(); 
1309                
1310                boolean paramInverted = false;
1311                TokenParamModifier modifier;
1312                
1313                for (IQueryParameterType nextOr : theList) {
1314                        if (nextOr instanceof TokenParam) {
1315                                if (!((TokenParam) nextOr).isEmpty()) {
1316                                        TokenParam id = (TokenParam) nextOr;
1317                                        if (id.isText()) {
1318
1319                                                // Check whether the :text modifier is actually enabled here
1320                                                boolean tokenTextIndexingEnabled = BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(myModelConfig, theSearchParam);
1321                                                if (!tokenTextIndexingEnabled) {
1322                                                        String msg;
1323                                                        if (myModelConfig.isSuppressStringIndexingInTokens()) {
1324                                                                msg = myFhirContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForServer");
1325                                                        } else {
1326                                                                msg = myFhirContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForSearchParam");
1327                                                        }
1328                                                        throw new MethodNotAllowedException(Msg.code(1219) + msg);
1329                                                }
1330
1331                                                return createPredicateString(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, null, theRequestPartitionId, theSqlBuilder);
1332                                        } 
1333                                        
1334                                        modifier = id.getModifier();
1335                                        // for :not modifier, create a token and remove the :not modifier
1336                                        if (modifier == TokenParamModifier.NOT) {
1337                                                tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue()));
1338                                                paramInverted = true;
1339                                        } else {
1340                                                tokens.add(nextOr);
1341                                        }
1342                                }
1343                        } else {
1344                                tokens.add(nextOr);
1345                        }
1346                }
1347
1348                if (tokens.isEmpty()) {
1349                        return null;
1350                }
1351
1352                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1353                Condition predicate;
1354                BaseJoiningPredicateBuilder join;
1355                
1356                if (paramInverted) {
1357                        SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder();
1358                        TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null);
1359                        sqlBuilder.addPredicate(tokenSelector.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId));
1360                        SelectQuery sql = sqlBuilder.getSelect();
1361                        Expression subSelect = new Subquery(sql);
1362                        
1363                        join = theSqlBuilder.getOrCreateFirstPredicateBuilder();
1364                        
1365                        if (theSourceJoinColumn == null) {
1366                                predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true);
1367                        } else {
1368                                //-- for the resource link, need join with target_resource_id
1369                            predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true);
1370                        }
1371                                                
1372                } else {
1373                
1374                        TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(PredicateBuilderTypeEnum.TOKEN, theSourceJoinColumn, paramName, () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn)).getResult();
1375
1376                        if (theList.get(0).getMissing() != null) {
1377                                return tokenJoin.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
1378                        }
1379
1380                        predicate = tokenJoin.createPredicateToken(tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
1381                        join = tokenJoin; 
1382                } 
1383                
1384                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
1385        }
1386
1387        public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
1388                                                                                                        String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
1389                                                                                                        SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails,
1390                                                                                                        RequestPartitionId theRequestPartitionId) {
1391                return createPredicateUri(theSourceJoinColumn, theResourceName, theSpnamePrefix, theSearchParam, theList, theOperation, theRequestDetails, theRequestPartitionId, mySqlBuilder);
1392        }
1393
1394        public Condition createPredicateUri(@Nullable DbColumn theSourceJoinColumn, String theResourceName,
1395                                                                                                        String theSpnamePrefix, RuntimeSearchParam theSearchParam, List<? extends IQueryParameterType> theList,
1396                                                                                                        SearchFilterParser.CompareOperation theOperation, RequestDetails theRequestDetails,
1397                                                                                                        RequestPartitionId theRequestPartitionId, SearchQueryBuilder theSqlBuilder) {
1398
1399                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1400
1401                UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn);
1402
1403                if (theList.get(0).getMissing() != null) {
1404                        return join.createPredicateParamMissingForNonReference(theResourceName, paramName, theList.get(0).getMissing(), theRequestPartitionId);
1405                }
1406
1407                Condition predicate = join.addPredicate(theList, paramName, theOperation, theRequestDetails);
1408                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
1409        }
1410
1411        public QueryStack newChildQueryFactoryWithFullBuilderReuse() {
1412                return new QueryStack(mySearchParameters, myDaoConfig, myModelConfig, myFhirContext, mySqlBuilder, mySearchParamRegistry, myPartitionSettings, EnumSet.allOf(PredicateBuilderTypeEnum.class));
1413        }
1414
1415        @Nullable
1416        public Condition searchForIdsWithAndOr(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId, SearchContainedModeEnum theSearchContainedMode) {
1417
1418                if (theAndOrParams.isEmpty()) {
1419                        return null;
1420                }
1421
1422                switch (theParamName) {
1423                        case IAnyResource.SP_RES_ID:
1424                                return createPredicateResourceId(theSourceJoinColumn, theAndOrParams, theResourceName, null, theRequestPartitionId);
1425
1426                        case Constants.PARAM_HAS:
1427                                return createPredicateHas(theSourceJoinColumn, theResourceName, theAndOrParams, theRequest, theRequestPartitionId);
1428
1429                        case Constants.PARAM_TAG:
1430                        case Constants.PARAM_PROFILE:
1431                        case Constants.PARAM_SECURITY:
1432                                if (myDaoConfig.getTagStorageMode() == DaoConfig.TagStorageModeEnum.INLINE) {
1433                                        return createPredicateSearchParameter(theSourceJoinColumn, theResourceName, theParamName, theAndOrParams, theRequest, theRequestPartitionId);
1434                                } else {
1435                                        return createPredicateTag(theSourceJoinColumn, theAndOrParams, theParamName, theRequestPartitionId);
1436                                }
1437
1438                        case Constants.PARAM_SOURCE:
1439                                return createPredicateSourceForAndList(theSourceJoinColumn, theAndOrParams);
1440
1441                        default:
1442                                return createPredicateSearchParameter(theSourceJoinColumn, theResourceName, theParamName, theAndOrParams, theRequest, theRequestPartitionId);
1443
1444                }
1445
1446        }
1447
1448        @Nullable
1449        private Condition createPredicateSearchParameter(@Nullable DbColumn theSourceJoinColumn, String theResourceName, String theParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
1450                List<Condition> andPredicates = new ArrayList<>();
1451                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
1452                if (nextParamDef != null) {
1453
1454                        if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) {
1455                                if (theRequestPartitionId.isAllPartitions()) {
1456                                        throw new PreconditionFailedException(Msg.code(1220) + "This server is not configured to support search against all partitions");
1457                                }
1458                        }
1459
1460                        switch (nextParamDef.getParamType()) {
1461                                case DATE:
1462                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1463                                                // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt'
1464                                                // to create the predicateDate instead of generic one with operation = null
1465                                                SearchFilterParser.CompareOperation operation = null;
1466                                                if (nextAnd.size() > 0) {
1467                                                        DateParam param = (DateParam) nextAnd.get(0);
1468                                                        operation = toOperation(param.getPrefix());
1469                                                }
1470                                                andPredicates.add(createPredicateDate(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, operation, theRequestPartitionId));
1471                                        }
1472                                        break;
1473                                case QUANTITY:
1474                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1475                                                SearchFilterParser.CompareOperation operation = null;
1476                                                if (nextAnd.size() > 0) {
1477                                                        QuantityParam param = (QuantityParam) nextAnd.get(0);
1478                                                        operation = toOperation(param.getPrefix());
1479                                                }
1480                                                andPredicates.add(createPredicateQuantity(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, operation, theRequestPartitionId));
1481                                        }
1482                                        break;
1483                                case REFERENCE:
1484                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1485                                                if (isEligibleForContainedResourceSearch(nextAnd)) {
1486                                                        andPredicates.add(createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
1487                                                } else {
1488                                                        andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));
1489                                                }
1490                                        }
1491                                        break;
1492                                case STRING:
1493                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1494                                                andPredicates.add(createPredicateString(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.sw, theRequestPartitionId));
1495                                        }
1496                                        break;
1497                                case TOKEN:
1498                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1499                                                if ("Location.position".equals(nextParamDef.getPath())) {
1500                                                        andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId));
1501                                                } else {
1502                                                        andPredicates.add(createPredicateToken(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId));
1503                                                }
1504                                        }
1505                                        break;
1506                                case NUMBER:
1507                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1508                                                andPredicates.add(createPredicateNumber(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, null, theRequestPartitionId));
1509                                        }
1510                                        break;
1511                                case COMPOSITE:
1512                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1513                                                andPredicates.add(createPredicateComposite(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, theRequestPartitionId));
1514                                        }
1515                                        break;
1516                                case URI:
1517                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1518                                                andPredicates.add(createPredicateUri(theSourceJoinColumn, theResourceName, null, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.eq, theRequest, theRequestPartitionId));
1519                                        }
1520                                        break;
1521                                case HAS:
1522                                case SPECIAL:
1523                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1524                                                if ("Location.position".equals(nextParamDef.getPath())) {
1525                                                        andPredicates.add(createPredicateCoords(theSourceJoinColumn, theResourceName, nextParamDef, nextAnd, theRequestPartitionId));
1526                                                }
1527                                        }
1528                                        break;
1529                        }
1530                } else {
1531                        // These are handled later
1532                        if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) {
1533                                if (Constants.PARAM_FILTER.equals(theParamName)) {
1534
1535                                        // Parse the predicates enumerated in the _filter separated by AND or OR...
1536                                        if (theAndOrParams.get(0).get(0) instanceof StringParam) {
1537                                                String filterString = ((StringParam) theAndOrParams.get(0).get(0)).getValue();
1538                                                SearchFilterParser.Filter filter;
1539                                                try {
1540                                                        filter = SearchFilterParser.parse(filterString);
1541                                                } catch (SearchFilterParser.FilterSyntaxException theE) {
1542                                                        throw new InvalidRequestException(Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage());
1543                                                }
1544                                                if (filter != null) {
1545
1546                                                        if (!myDaoConfig.isFilterParameterEnabled()) {
1547                                                                throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER + " parameter is disabled on this server");
1548                                                        }
1549
1550                                                        Condition predicate = createPredicateFilter(this, filter, theResourceName, theRequest, theRequestPartitionId);
1551                                                        if (predicate != null) {
1552                                                                mySqlBuilder.addPredicate(predicate);
1553                                                        }
1554                                                }
1555                                        }
1556
1557                                } else {
1558                                        String msg = myFhirContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSearchParameter", theParamName, theResourceName, mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName));
1559                                        throw new InvalidRequestException(Msg.code(1223) + msg);
1560                                }
1561                        }
1562                }
1563
1564                return toAndPredicate(andPredicates);
1565        }
1566
1567        private boolean isEligibleForContainedResourceSearch(List<? extends IQueryParameterType> nextAnd) {
1568                return myModelConfig.isIndexOnContainedResources() &&
1569                        nextAnd.stream()
1570                                .filter(t -> t instanceof ReferenceParam)
1571                                .map(t -> ((ReferenceParam) t).getChain())
1572                                .anyMatch(StringUtils::isNotBlank);
1573        }
1574
1575        public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
1576                ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
1577                Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString);
1578                mySqlBuilder.addPredicate(predicate);
1579        }
1580
1581        public void addPredicateCompositeNonUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
1582                ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboNonUniquePredicateBuilder();
1583                Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexString);
1584                mySqlBuilder.addPredicate(predicate);
1585        }
1586
1587
1588        // expand out the pids
1589        public void addPredicateEverythingOperation(String theResourceName, Long... theTargetPids) {
1590                ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null);
1591                Condition predicate = table.createEverythingPredicate(theResourceName, theTargetPids);
1592                mySqlBuilder.addPredicate(predicate);
1593        }
1594
1595        private IQueryParameterType toParameterType(RuntimeSearchParam theParam) {
1596
1597                IQueryParameterType qp;
1598                switch (theParam.getParamType()) {
1599                        case DATE:
1600                                qp = new DateParam();
1601                                break;
1602                        case NUMBER:
1603                                qp = new NumberParam();
1604                                break;
1605                        case QUANTITY:
1606                                qp = new QuantityParam();
1607                                break;
1608                        case STRING:
1609                                qp = new StringParam();
1610                                break;
1611                        case TOKEN:
1612                                qp = new TokenParam();
1613                                break;
1614                        case COMPOSITE:
1615                                List<RuntimeSearchParam> compositeOf = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam);
1616                                if (compositeOf.size() != 2) {
1617                                        throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this.");
1618                                }
1619                                IQueryParameterType leftParam = toParameterType(compositeOf.get(0));
1620                                IQueryParameterType rightParam = toParameterType(compositeOf.get(1));
1621                                qp = new CompositeParam<>(leftParam, rightParam);
1622                                break;
1623                        case URI:
1624                                qp = new UriParam();
1625                                break;
1626                        case HAS:
1627                        case REFERENCE:
1628                        case SPECIAL:
1629                        default:
1630                                throw new InvalidRequestException(Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported.");
1631                }
1632                return qp;
1633        }
1634
1635        private enum PredicateBuilderTypeEnum {
1636                DATE, COORDS, NUMBER, QUANTITY, REFERENCE, SOURCE, STRING, TOKEN, TAG
1637        }
1638
1639        private static class PredicateBuilderCacheLookupResult<T extends BaseJoiningPredicateBuilder> {
1640                private final boolean myCacheHit;
1641                private final T myResult;
1642
1643                private PredicateBuilderCacheLookupResult(boolean theCacheHit, T theResult) {
1644                        myCacheHit = theCacheHit;
1645                        myResult = theResult;
1646                }
1647
1648                public boolean isCacheHit() {
1649                        return myCacheHit;
1650                }
1651
1652                public T getResult() {
1653                        return myResult;
1654                }
1655        }
1656
1657        private static class PredicateBuilderCacheKey {
1658                private final DbColumn myDbColumn;
1659                private final PredicateBuilderTypeEnum myType;
1660                private final String myParamName;
1661                private final int myHashCode;
1662
1663                private PredicateBuilderCacheKey(DbColumn theDbColumn, PredicateBuilderTypeEnum theType, String theParamName) {
1664                        myDbColumn = theDbColumn;
1665                        myType = theType;
1666                        myParamName = theParamName;
1667                        myHashCode = new HashCodeBuilder().append(myDbColumn).append(myType).append(myParamName).toHashCode();
1668                }
1669
1670                @Override
1671                public boolean equals(Object theO) {
1672                        if (this == theO) {
1673                                return true;
1674                        }
1675
1676                        if (theO == null || getClass() != theO.getClass()) {
1677                                return false;
1678                        }
1679
1680                        PredicateBuilderCacheKey that = (PredicateBuilderCacheKey) theO;
1681
1682                        return new EqualsBuilder()
1683                                .append(myDbColumn, that.myDbColumn)
1684                                .append(myType, that.myType)
1685                                .append(myParamName, that.myParamName)
1686                                .isEquals();
1687                }
1688
1689                @Override
1690                public int hashCode() {
1691                        return myHashCode;
1692                }
1693        }
1694
1695        @Nullable
1696        public static Condition toAndPredicate(List<Condition> theAndPredicates) {
1697                List<Condition> andPredicates = theAndPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
1698                if (andPredicates.size() == 0) {
1699                        return null;
1700                } else if (andPredicates.size() == 1) {
1701                        return andPredicates.get(0);
1702                } else {
1703                        return ComboCondition.and(andPredicates.toArray(new Condition[0]));
1704                }
1705        }
1706
1707        @Nullable
1708        public static Condition toOrPredicate(List<Condition> theOrPredicates) {
1709                List<Condition> orPredicates = theOrPredicates.stream().filter(t -> t != null).collect(Collectors.toList());
1710                if (orPredicates.size() == 0) {
1711                        return null;
1712                } else if (orPredicates.size() == 1) {
1713                        return orPredicates.get(0);
1714                } else {
1715                        return ComboCondition.or(orPredicates.toArray(new Condition[0]));
1716                }
1717        }
1718
1719        @Nullable
1720        public static Condition toOrPredicate(Condition... theOrPredicates) {
1721                return toOrPredicate(Arrays.asList(theOrPredicates));
1722        }
1723
1724        @Nullable
1725        public static Condition toAndPredicate(Condition... theAndPredicates) {
1726                return toAndPredicate(Arrays.asList(theAndPredicates));
1727        }
1728
1729        @Nonnull
1730        public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders, boolean theInverse) {
1731                if (theInverse) {
1732                        return toNotEqualToOrNotInPredicate(theColumn, theValuePlaceholders);
1733                } else {
1734                        return toEqualToOrInPredicate(theColumn, theValuePlaceholders);
1735                }
1736        }
1737
1738        @Nonnull
1739        public static Condition toEqualToOrInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
1740                if (theValuePlaceholders.size() == 1) {
1741                        return BinaryCondition.equalTo(theColumn, theValuePlaceholders.get(0));
1742                }
1743                return new InCondition(theColumn, theValuePlaceholders);
1744        }
1745
1746        @Nonnull
1747        public static Condition toNotEqualToOrNotInPredicate(DbColumn theColumn, List<String> theValuePlaceholders) {
1748                if (theValuePlaceholders.size() == 1) {
1749                        return BinaryCondition.notEqualTo(theColumn, theValuePlaceholders.get(0));
1750                }
1751                return new InCondition(theColumn, theValuePlaceholders).setNegate(true);
1752        }
1753
1754        public static SearchFilterParser.CompareOperation toOperation(ParamPrefixEnum thePrefix) {
1755                SearchFilterParser.CompareOperation retVal = null;
1756                if (thePrefix != null && ourCompareOperationToParamPrefix.containsValue(thePrefix)) {
1757                        retVal = ourCompareOperationToParamPrefix.getKey(thePrefix);
1758                }
1759                return defaultIfNull(retVal, SearchFilterParser.CompareOperation.eq);
1760        }
1761
1762        public static ParamPrefixEnum fromOperation(SearchFilterParser.CompareOperation thePrefix) {
1763                ParamPrefixEnum retVal = null;
1764                if (thePrefix != null && ourCompareOperationToParamPrefix.containsKey(thePrefix)) {
1765                        retVal = ourCompareOperationToParamPrefix.get(thePrefix);
1766                }
1767                return defaultIfNull(retVal, ParamPrefixEnum.EQUAL);
1768        }
1769
1770        private static String getChainedPart(String parameter) {
1771                return parameter.substring(parameter.indexOf(".") + 1);
1772        }
1773
1774        public static String getParamNameWithPrefix(String theSpnamePrefix, String theParamName) {
1775
1776                if (isBlank(theSpnamePrefix))
1777                        return theParamName;
1778
1779                return theSpnamePrefix + "." + theParamName;
1780        }
1781}