001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.search.builder;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeSearchParam;
024import ca.uhn.fhir.exception.TokenParamFormatInvalidRequestException;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
028import ca.uhn.fhir.jpa.dao.BaseStorageDao;
029import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
030import ca.uhn.fhir.jpa.model.config.PartitionSettings;
031import ca.uhn.fhir.jpa.model.dao.JpaPid;
032import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
033import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
034import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
035import ca.uhn.fhir.jpa.search.builder.models.MissingParameterQueryParams;
036import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
037import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheKey;
038import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheLookupResult;
039import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderTypeEnum;
040import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder;
041import ca.uhn.fhir.jpa.search.builder.predicate.BaseQuantityPredicateBuilder;
042import ca.uhn.fhir.jpa.search.builder.predicate.BaseSearchParamPredicateBuilder;
043import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder;
044import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder;
045import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
046import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder;
047import ca.uhn.fhir.jpa.search.builder.predicate.ICanMakeMissingParamPredicate;
048import ca.uhn.fhir.jpa.search.builder.predicate.ISourcePredicateBuilder;
049import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder;
050import ca.uhn.fhir.jpa.search.builder.predicate.ParsedLocationParam;
051import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder;
052import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
053import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder;
054import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder;
055import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder;
056import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder;
057import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder;
058import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder;
059import ca.uhn.fhir.jpa.search.builder.sql.ColumnTupleObject;
060import ca.uhn.fhir.jpa.search.builder.sql.PredicateBuilderFactory;
061import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
062import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
063import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
064import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
065import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
066import ca.uhn.fhir.model.api.IQueryParameterAnd;
067import ca.uhn.fhir.model.api.IQueryParameterOr;
068import ca.uhn.fhir.model.api.IQueryParameterType;
069import ca.uhn.fhir.parser.DataFormatException;
070import ca.uhn.fhir.rest.api.Constants;
071import ca.uhn.fhir.rest.api.QualifiedParamList;
072import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
073import ca.uhn.fhir.rest.api.server.RequestDetails;
074import ca.uhn.fhir.rest.param.CompositeParam;
075import ca.uhn.fhir.rest.param.DateParam;
076import ca.uhn.fhir.rest.param.DateRangeParam;
077import ca.uhn.fhir.rest.param.HasParam;
078import ca.uhn.fhir.rest.param.NumberParam;
079import ca.uhn.fhir.rest.param.QuantityParam;
080import ca.uhn.fhir.rest.param.ReferenceParam;
081import ca.uhn.fhir.rest.param.SpecialParam;
082import ca.uhn.fhir.rest.param.StringParam;
083import ca.uhn.fhir.rest.param.TokenParam;
084import ca.uhn.fhir.rest.param.TokenParamModifier;
085import ca.uhn.fhir.rest.param.UriParam;
086import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
087import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
088import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
089import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
090import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
091import com.google.common.collect.Lists;
092import com.google.common.collect.Maps;
093import com.google.common.collect.Sets;
094import com.healthmarketscience.sqlbuilder.BinaryCondition;
095import com.healthmarketscience.sqlbuilder.ComboCondition;
096import com.healthmarketscience.sqlbuilder.Condition;
097import com.healthmarketscience.sqlbuilder.Expression;
098import com.healthmarketscience.sqlbuilder.InCondition;
099import com.healthmarketscience.sqlbuilder.SelectQuery;
100import com.healthmarketscience.sqlbuilder.SetOperationQuery;
101import com.healthmarketscience.sqlbuilder.Subquery;
102import com.healthmarketscience.sqlbuilder.UnionQuery;
103import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
104import jakarta.annotation.Nullable;
105import org.apache.commons.lang3.StringUtils;
106import org.apache.commons.lang3.tuple.Triple;
107import org.hl7.fhir.instance.model.api.IAnyResource;
108import org.slf4j.Logger;
109import org.slf4j.LoggerFactory;
110import org.springframework.util.CollectionUtils;
111
112import java.math.BigDecimal;
113import java.util.ArrayList;
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.Optional;
122import java.util.Set;
123import java.util.function.Supplier;
124import java.util.regex.Pattern;
125import java.util.stream.Collectors;
126
127import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
128import static ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder.getResourceIdColumn;
129import static ca.uhn.fhir.jpa.util.QueryParameterUtils.fromOperation;
130import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getChainedPart;
131import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getParamNameWithPrefix;
132import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
133import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
134import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation;
135import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
136import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS;
137import static ca.uhn.fhir.rest.api.Constants.PARAM_ID;
138import static org.apache.commons.lang3.StringUtils.isBlank;
139import static org.apache.commons.lang3.StringUtils.isNotBlank;
140import static org.apache.commons.lang3.StringUtils.split;
141
142public class QueryStack {
143
144        private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class);
145        public static final String LOCATION_POSITION = "Location.position";
146        private static final Pattern PATTERN_DOT_AND_ALL_AFTER = Pattern.compile("\\..*");
147
148        private final FhirContext myFhirContext;
149        private final SearchQueryBuilder mySqlBuilder;
150        private final SearchParameterMap mySearchParameters;
151        private final ISearchParamRegistry mySearchParamRegistry;
152        private final PartitionSettings myPartitionSettings;
153        private final JpaStorageSettings myStorageSettings;
154        private final EnumSet<PredicateBuilderTypeEnum> myReusePredicateBuilderTypes;
155        private final RequestDetails myRequestDetails;
156        private Map<PredicateBuilderCacheKey, BaseJoiningPredicateBuilder> myJoinMap;
157        private Map<String, BaseJoiningPredicateBuilder> myParamNameToPredicateBuilderMap;
158        // used for _offset queries with sort, should be removed once the fix is applied to the async path too.
159        private boolean myUseAggregate;
160        private boolean myGroupingAdded;
161
162        /**
163         * Constructor
164         */
165        public QueryStack(
166                        RequestDetails theRequestDetails,
167                        SearchParameterMap theSearchParameters,
168                        JpaStorageSettings theStorageSettings,
169                        FhirContext theFhirContext,
170                        SearchQueryBuilder theSqlBuilder,
171                        ISearchParamRegistry theSearchParamRegistry,
172                        PartitionSettings thePartitionSettings) {
173                this(
174                                theRequestDetails,
175                                theSearchParameters,
176                                theStorageSettings,
177                                theFhirContext,
178                                theSqlBuilder,
179                                theSearchParamRegistry,
180                                thePartitionSettings,
181                                EnumSet.of(PredicateBuilderTypeEnum.DATE));
182        }
183
184        /**
185         * Constructor
186         */
187        private QueryStack(
188                        RequestDetails theRequestDetails,
189                        SearchParameterMap theSearchParameters,
190                        JpaStorageSettings theStorageSettings,
191                        FhirContext theFhirContext,
192                        SearchQueryBuilder theSqlBuilder,
193                        ISearchParamRegistry theSearchParamRegistry,
194                        PartitionSettings thePartitionSettings,
195                        EnumSet<PredicateBuilderTypeEnum> theReusePredicateBuilderTypes) {
196                myRequestDetails = theRequestDetails;
197                myPartitionSettings = thePartitionSettings;
198                assert theSearchParameters != null;
199                assert theStorageSettings != null;
200                assert theFhirContext != null;
201                assert theSqlBuilder != null;
202
203                mySearchParameters = theSearchParameters;
204                myStorageSettings = theStorageSettings;
205                myFhirContext = theFhirContext;
206                mySqlBuilder = theSqlBuilder;
207                mySearchParamRegistry = theSearchParamRegistry;
208                myReusePredicateBuilderTypes = theReusePredicateBuilderTypes;
209        }
210
211        public void addSortOnCoordsNear(String theParamName, boolean theAscending, SearchParameterMap theParams) {
212                boolean handled = false;
213                if (myParamNameToPredicateBuilderMap != null) {
214                        BaseJoiningPredicateBuilder builder = myParamNameToPredicateBuilderMap.get(theParamName);
215                        if (builder instanceof CoordsPredicateBuilder) {
216                                CoordsPredicateBuilder coordsBuilder = (CoordsPredicateBuilder) builder;
217
218                                List<List<IQueryParameterType>> params = theParams.get(theParamName);
219                                if (!params.isEmpty() && !params.get(0).isEmpty()) {
220                                        IQueryParameterType param = params.get(0).get(0);
221                                        ParsedLocationParam location = ParsedLocationParam.from(theParams, param);
222                                        double latitudeValue = location.getLatitudeValue();
223                                        double longitudeValue = location.getLongitudeValue();
224                                        mySqlBuilder.addSortCoordsNear(coordsBuilder, latitudeValue, longitudeValue, theAscending);
225                                        handled = true;
226                                }
227                        }
228                }
229
230                if (!handled) {
231                        String msg = myFhirContext
232                                        .getLocalizer()
233                                        .getMessageSanitized(QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName);
234                        throw new InvalidRequestException(Msg.code(2307) + msg);
235                }
236        }
237
238        public void addSortOnDate(String theResourceName, String theParamName, boolean theAscending) {
239                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
240                DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder();
241
242                Condition hashIdentityPredicate =
243                                datePredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
244
245                addSortCustomJoin(firstPredicateBuilder, datePredicateBuilder, hashIdentityPredicate);
246
247                mySqlBuilder.addSortDate(datePredicateBuilder.getColumnValueLow(), theAscending, myUseAggregate);
248        }
249
250        public void addSortOnLastUpdated(boolean theAscending) {
251                ResourceTablePredicateBuilder resourceTablePredicateBuilder;
252                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
253                if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) {
254                        resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder;
255                } else {
256                        resourceTablePredicateBuilder =
257                                        mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getJoinColumns());
258                }
259                mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending, myUseAggregate);
260        }
261
262        public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) {
263                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
264                NumberPredicateBuilder numberPredicateBuilder = mySqlBuilder.createNumberPredicateBuilder();
265
266                Condition hashIdentityPredicate =
267                                numberPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
268
269                addSortCustomJoin(firstPredicateBuilder, numberPredicateBuilder, hashIdentityPredicate);
270
271                mySqlBuilder.addSortNumeric(numberPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
272        }
273
274        public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) {
275                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
276
277                BaseQuantityPredicateBuilder quantityPredicateBuilder = mySqlBuilder.createQuantityPredicateBuilder();
278
279                Condition hashIdentityPredicate =
280                                quantityPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
281
282                addSortCustomJoin(firstPredicateBuilder, quantityPredicateBuilder, hashIdentityPredicate);
283
284                mySqlBuilder.addSortNumeric(quantityPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
285        }
286
287        public void addSortOnResourceId(boolean theAscending) {
288                ResourceTablePredicateBuilder resourceTablePredicateBuilder;
289                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
290                if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) {
291                        resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder;
292                } else {
293                        resourceTablePredicateBuilder =
294                                        mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getJoinColumns());
295                }
296                mySqlBuilder.addSortString(resourceTablePredicateBuilder.getColumnFhirId(), theAscending, myUseAggregate);
297        }
298
299        /** Sort on RES_ID -- used to break ties for reliable sort */
300        public void addSortOnResourcePID(boolean theAscending) {
301                BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
302                mySqlBuilder.addSortString(predicateBuilder.getResourceIdColumn(), theAscending);
303        }
304
305        public void addSortOnResourceLink(
306                        String theResourceName,
307                        String theReferenceTargetType,
308                        String theParamName,
309                        String theChain,
310                        boolean theAscending,
311                        SearchParameterMap theParams) {
312                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
313                ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this);
314
315                Condition pathPredicate =
316                                resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName);
317
318                addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate);
319
320                if (isBlank(theChain)) {
321                        mySqlBuilder.addSortNumeric(
322                                        resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate);
323                        return;
324                }
325
326                String targetType = null;
327                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
328                                theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
329                if (theReferenceTargetType != null) {
330                        targetType = theReferenceTargetType;
331                } else if (param.getTargets().size() > 1) {
332                        throw new InvalidRequestException(Msg.code(2287) + "Unable to sort on a chained parameter from '"
333                                        + theParamName + "' as this parameter has multiple target types. Please specify the target type.");
334                } else if (param.getTargets().size() == 1) {
335                        targetType = param.getTargets().iterator().next();
336                }
337
338                if (isBlank(targetType)) {
339                        throw new InvalidRequestException(
340                                        Msg.code(2288) + "Unable to sort on a chained parameter from '" + theParamName
341                                                        + "' as this parameter as this parameter does not define a target type. Please specify the target type.");
342                }
343
344                RuntimeSearchParam targetSearchParameter = mySearchParamRegistry.getActiveSearchParam(
345                                targetType, theChain, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
346                if (targetSearchParameter == null) {
347                        Collection<String> validSearchParameterNames = mySearchParamRegistry
348                                        .getActiveSearchParams(targetType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
349                                        .values()
350                                        .stream()
351                                        .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.STRING
352                                                        || t.getParamType() == RestSearchParameterTypeEnum.TOKEN
353                                                        || t.getParamType() == RestSearchParameterTypeEnum.DATE)
354                                        .map(RuntimeSearchParam::getName)
355                                        .sorted()
356                                        .distinct()
357                                        .collect(Collectors.toList());
358                        String msg = myFhirContext
359                                        .getLocalizer()
360                                        .getMessageSanitized(
361                                                        BaseStorageDao.class,
362                                                        "invalidSortParameter",
363                                                        theChain,
364                                                        targetType,
365                                                        validSearchParameterNames);
366                        throw new InvalidRequestException(Msg.code(2289) + msg);
367                }
368
369                // add a left-outer join to a predicate for the target type, then sort on value columns(s).
370                switch (targetSearchParameter.getParamType()) {
371                        case STRING:
372                                StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder();
373                                addSortCustomJoin(
374                                                resourceLinkPredicateBuilder.getJoinColumnsForTarget(),
375                                                stringPredicateBuilder,
376                                                stringPredicateBuilder.createHashIdentityPredicate(targetType, theChain));
377
378                                mySqlBuilder.addSortString(
379                                                stringPredicateBuilder.getColumnValueNormalized(), theAscending, myUseAggregate);
380                                return;
381
382                        case TOKEN:
383                                TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder();
384                                addSortCustomJoin(
385                                                resourceLinkPredicateBuilder.getJoinColumnsForTarget(),
386                                                tokenPredicateBuilder,
387                                                tokenPredicateBuilder.createHashIdentityPredicate(targetType, theChain));
388
389                                mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending, myUseAggregate);
390                                mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
391                                return;
392
393                        case DATE:
394                                DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder();
395                                addSortCustomJoin(
396                                                resourceLinkPredicateBuilder.getJoinColumnsForTarget(),
397                                                datePredicateBuilder,
398                                                datePredicateBuilder.createHashIdentityPredicate(targetType, theChain));
399
400                                mySqlBuilder.addSortDate(datePredicateBuilder.getColumnValueLow(), theAscending, myUseAggregate);
401                                return;
402
403                                /*
404                                 * Note that many of the options below aren't implemented because they
405                                 * don't seem useful to me, but they could theoretically be implemented
406                                 * if someone ever needed them. I'm not sure why you'd want to do a chained
407                                 * sort on a target that was a reference or a quantity, but if someone needed
408                                 * that we could implement it here.
409                                 */
410                        case SPECIAL: {
411                                if (LOCATION_POSITION.equals(targetSearchParameter.getPath())) {
412                                        List<List<IQueryParameterType>> params = theParams.get(theParamName);
413                                        if (params != null && !params.isEmpty() && !params.get(0).isEmpty()) {
414                                                IQueryParameterType locationParam = params.get(0).get(0);
415                                                final SpecialParam specialParam =
416                                                                new SpecialParam().setValue(locationParam.getValueAsQueryToken(myFhirContext));
417                                                ParsedLocationParam location = ParsedLocationParam.from(theParams, specialParam);
418                                                double latitudeValue = location.getLatitudeValue();
419                                                double longitudeValue = location.getLongitudeValue();
420                                                final CoordsPredicateBuilder coordsPredicateBuilder = mySqlBuilder.addCoordsPredicateBuilder(
421                                                                resourceLinkPredicateBuilder.getJoinColumnsForTarget());
422                                                mySqlBuilder.addSortCoordsNear(
423                                                                coordsPredicateBuilder, latitudeValue, longitudeValue, theAscending);
424                                        } else {
425                                                String msg = myFhirContext
426                                                                .getLocalizer()
427                                                                .getMessageSanitized(
428                                                                                QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName);
429                                                throw new InvalidRequestException(Msg.code(2497) + msg);
430                                        }
431                                        return;
432                                }
433                        }
434                                //noinspection fallthrough
435                        case NUMBER:
436                        case REFERENCE:
437                        case COMPOSITE:
438                        case QUANTITY:
439                        case URI:
440                        case HAS:
441
442                        default:
443                                throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter "
444                                                + theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: "
445                                                + targetSearchParameter.getParamType().name());
446                }
447        }
448
449        public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) {
450                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
451
452                StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder();
453                Condition hashIdentityPredicate =
454                                stringPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
455
456                addSortCustomJoin(firstPredicateBuilder, stringPredicateBuilder, hashIdentityPredicate);
457
458                mySqlBuilder.addSortString(stringPredicateBuilder.getColumnValueNormalized(), theAscending, myUseAggregate);
459        }
460
461        public void addSortOnToken(String theResourceName, String theParamName, boolean theAscending) {
462                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
463
464                TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder();
465                Condition hashIdentityPredicate =
466                                tokenPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
467
468                addSortCustomJoin(firstPredicateBuilder, tokenPredicateBuilder, hashIdentityPredicate);
469
470                mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending, myUseAggregate);
471                mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
472        }
473
474        public void addSortOnUri(String theResourceName, String theParamName, boolean theAscending) {
475                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
476
477                UriPredicateBuilder uriPredicateBuilder = mySqlBuilder.createUriPredicateBuilder();
478                Condition hashIdentityPredicate =
479                                uriPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
480
481                addSortCustomJoin(firstPredicateBuilder, uriPredicateBuilder, hashIdentityPredicate);
482
483                mySqlBuilder.addSortString(uriPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
484        }
485
486        private void addSortCustomJoin(
487                        BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder,
488                        BaseJoiningPredicateBuilder theToJoiningPredicateBuilder,
489                        Condition theCondition) {
490                addSortCustomJoin(theFromJoiningPredicateBuilder.getJoinColumns(), theToJoiningPredicateBuilder, theCondition);
491        }
492
493        private void addSortCustomJoin(
494                        DbColumn theFromDbColumn[],
495                        BaseJoiningPredicateBuilder theToJoiningPredicateBuilder,
496                        Condition theCondition) {
497
498                ComboCondition onCondition =
499                                mySqlBuilder.createOnCondition(theFromDbColumn, theToJoiningPredicateBuilder.getJoinColumns());
500
501                if (theCondition != null) {
502                        onCondition.addCondition(theCondition);
503                }
504
505                mySqlBuilder.addCustomJoin(
506                                SelectQuery.JoinType.LEFT_OUTER,
507                                theFromDbColumn[0].getTable(),
508                                theToJoiningPredicateBuilder.getTable(),
509                                onCondition);
510        }
511
512        public void setUseAggregate(boolean theUseAggregate) {
513                myUseAggregate = theUseAggregate;
514        }
515
516        @SuppressWarnings("unchecked")
517        private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder(
518                        PredicateBuilderTypeEnum theType,
519                        DbColumn[] theSourceJoinColumn,
520                        String theParamName,
521                        Supplier<T> theFactoryMethod) {
522                boolean cacheHit = false;
523                BaseJoiningPredicateBuilder retVal;
524                if (myReusePredicateBuilderTypes.contains(theType)) {
525                        PredicateBuilderCacheKey key = new PredicateBuilderCacheKey(theSourceJoinColumn, theType, theParamName);
526                        if (myJoinMap == null) {
527                                myJoinMap = new HashMap<>();
528                        }
529                        retVal = myJoinMap.get(key);
530                        if (retVal != null) {
531                                cacheHit = true;
532                        } else {
533                                retVal = theFactoryMethod.get();
534                                myJoinMap.put(key, retVal);
535                        }
536                } else {
537                        retVal = theFactoryMethod.get();
538                }
539
540                if (theType == PredicateBuilderTypeEnum.COORDS) {
541                        if (myParamNameToPredicateBuilderMap == null) {
542                                myParamNameToPredicateBuilderMap = new HashMap<>();
543                        }
544                        myParamNameToPredicateBuilderMap.put(theParamName, retVal);
545                }
546
547                return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal);
548        }
549
550        private Condition createPredicateComposite(
551                        @Nullable DbColumn[] theSourceJoinColumn,
552                        String theResourceName,
553                        String theSpnamePrefix,
554                        RuntimeSearchParam theParamDef,
555                        List<? extends IQueryParameterType> theNextAnd,
556                        RequestPartitionId theRequestPartitionId) {
557                return createPredicateComposite(
558                                theSourceJoinColumn,
559                                theResourceName,
560                                theSpnamePrefix,
561                                theParamDef,
562                                theNextAnd,
563                                theRequestPartitionId,
564                                mySqlBuilder);
565        }
566
567        private Condition createPredicateComposite(
568                        @Nullable DbColumn[] theSourceJoinColumn,
569                        String theResourceName,
570                        String theSpnamePrefix,
571                        RuntimeSearchParam theParamDef,
572                        List<? extends IQueryParameterType> theNextAnd,
573                        RequestPartitionId theRequestPartitionId,
574                        SearchQueryBuilder theSqlBuilder) {
575
576                Condition orCondidtion = null;
577                for (IQueryParameterType next : theNextAnd) {
578
579                        if (!(next instanceof CompositeParam<?, ?>)) {
580                                throw new InvalidRequestException(Msg.code(1203) + "Invalid type for composite param (must be "
581                                                + CompositeParam.class.getSimpleName() + ": " + next.getClass());
582                        }
583                        CompositeParam<?, ?> cp = (CompositeParam<?, ?>) next;
584
585                        List<RuntimeSearchParam> componentParams =
586                                        JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef);
587                        RuntimeSearchParam left = componentParams.get(0);
588                        IQueryParameterType leftValue = cp.getLeftValue();
589                        Condition leftPredicate = createPredicateCompositePart(
590                                        theSourceJoinColumn,
591                                        theResourceName,
592                                        theSpnamePrefix,
593                                        left,
594                                        leftValue,
595                                        theRequestPartitionId,
596                                        theSqlBuilder);
597
598                        RuntimeSearchParam right = componentParams.get(1);
599                        IQueryParameterType rightValue = cp.getRightValue();
600                        Condition rightPredicate = createPredicateCompositePart(
601                                        theSourceJoinColumn,
602                                        theResourceName,
603                                        theSpnamePrefix,
604                                        right,
605                                        rightValue,
606                                        theRequestPartitionId,
607                                        theSqlBuilder);
608
609                        Condition andCondition = toAndPredicate(leftPredicate, rightPredicate);
610
611                        if (orCondidtion == null) {
612                                orCondidtion = toOrPredicate(andCondition);
613                        } else {
614                                orCondidtion = toOrPredicate(orCondidtion, andCondition);
615                        }
616                }
617
618                return orCondidtion;
619        }
620
621        private Condition createPredicateCompositePart(
622                        @Nullable DbColumn[] theSourceJoinColumn,
623                        String theResourceName,
624                        String theSpnamePrefix,
625                        RuntimeSearchParam theParam,
626                        IQueryParameterType theParamValue,
627                        RequestPartitionId theRequestPartitionId,
628                        SearchQueryBuilder theSqlBuilder) {
629
630                switch (theParam.getParamType()) {
631                        case STRING: {
632                                return createPredicateString(
633                                                theSourceJoinColumn,
634                                                theResourceName,
635                                                theSpnamePrefix,
636                                                theParam,
637                                                Collections.singletonList(theParamValue),
638                                                null,
639                                                theRequestPartitionId,
640                                                theSqlBuilder);
641                        }
642                        case TOKEN: {
643                                return createPredicateToken(
644                                                theSourceJoinColumn,
645                                                theResourceName,
646                                                theSpnamePrefix,
647                                                theParam,
648                                                Collections.singletonList(theParamValue),
649                                                null,
650                                                theRequestPartitionId,
651                                                theSqlBuilder);
652                        }
653                        case DATE: {
654                                return createPredicateDate(
655                                                theSourceJoinColumn,
656                                                theResourceName,
657                                                theSpnamePrefix,
658                                                theParam,
659                                                Collections.singletonList(theParamValue),
660                                                toOperation(((DateParam) theParamValue).getPrefix()),
661                                                theRequestPartitionId,
662                                                theSqlBuilder);
663                        }
664                        case QUANTITY: {
665                                return createPredicateQuantity(
666                                                theSourceJoinColumn,
667                                                theResourceName,
668                                                theSpnamePrefix,
669                                                theParam,
670                                                Collections.singletonList(theParamValue),
671                                                null,
672                                                theRequestPartitionId,
673                                                theSqlBuilder);
674                        }
675                        case NUMBER:
676                        case REFERENCE:
677                        case COMPOSITE:
678                        case URI:
679                        case HAS:
680                        case SPECIAL:
681                        default:
682                                throw new InvalidRequestException(Msg.code(1204)
683                                                + "Don't know how to handle composite parameter with type of " + theParam.getParamType());
684                }
685        }
686
687        private Condition createMissingParameterQuery(MissingParameterQueryParams theParams) {
688                if (theParams.getParamType() == RestSearchParameterTypeEnum.COMPOSITE) {
689                        ourLog.error("Cannot create missing parameter query for a composite parameter.");
690                        return null;
691                } else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
692                        if (isEligibleForEmbeddedChainedResourceSearch(
693                                                        theParams.getResourceType(), theParams.getParamName(), theParams.getQueryParameterTypes())
694                                        .supportsUplifted()) {
695                                ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search.");
696                                return null;
697                        }
698                }
699
700                // TODO - Change this when we have HFJ_SPIDX_MISSING table
701                /**
702                 * How we search depends on if the
703                 * {@link JpaStorageSettings#getIndexMissingFields()} property
704                 * is Enabled or Disabled.
705                 *
706                 * If it is, we will use the SP_MISSING values set into the various
707                 * SP_INDX_X tables and search on those ("old" search).
708                 *
709                 * If it is not set, however, we will try and construct a query that
710                 * looks for missing SearchParameters in the SP_IDX_* tables ("new" search).
711                 *
712                 * You cannot mix and match, however (SP_MISSING is not in HASH_IDENTITY information).
713                 * So setting (or unsetting) the IndexMissingFields
714                 * property should always be followed up with a /$reindex call.
715                 *
716                 * ---
717                 *
718                 * Current limitations:
719                 * Checking if a row exists ("new" search) for a given missing field in an SP_INDX_* table
720                 * (ie, :missing=true) is slow when there are many resources in the table. (Defaults to
721                 * a table scan, since HASH_IDENTITY isn't part of the index).
722                 *
723                 * However, the "old" search method was slow for the reverse: when looking for resources
724                 * that do not have a missing field (:missing=false) for much the same reason.
725                 */
726                SearchQueryBuilder sqlBuilder = theParams.getSqlBuilder();
727                if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.DISABLED) {
728                        // new search
729                        return createMissingPredicateForUnindexedMissingFields(theParams, sqlBuilder);
730                } else {
731                        // old search
732                        return createMissingPredicateForIndexedMissingFields(theParams, sqlBuilder);
733                }
734        }
735
736        /**
737         * Old way of searching.
738         * Missing values must be indexed!
739         */
740        private Condition createMissingPredicateForIndexedMissingFields(
741                        MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) {
742                PredicateBuilderTypeEnum predicateType = null;
743                Supplier<? extends BaseJoiningPredicateBuilder> supplier = null;
744                switch (theParams.getParamType()) {
745                        case STRING:
746                                predicateType = PredicateBuilderTypeEnum.STRING;
747                                supplier = () -> sqlBuilder.addStringPredicateBuilder(theParams.getSourceJoinColumn());
748                                break;
749                        case NUMBER:
750                                predicateType = PredicateBuilderTypeEnum.NUMBER;
751                                supplier = () -> sqlBuilder.addNumberPredicateBuilder(theParams.getSourceJoinColumn());
752                                break;
753                        case DATE:
754                                predicateType = PredicateBuilderTypeEnum.DATE;
755                                supplier = () -> sqlBuilder.addDatePredicateBuilder(theParams.getSourceJoinColumn());
756                                break;
757                        case TOKEN:
758                                predicateType = PredicateBuilderTypeEnum.TOKEN;
759                                supplier = () -> sqlBuilder.addTokenPredicateBuilder(theParams.getSourceJoinColumn());
760                                break;
761                        case QUANTITY:
762                                predicateType = PredicateBuilderTypeEnum.QUANTITY;
763                                supplier = () -> sqlBuilder.addQuantityPredicateBuilder(theParams.getSourceJoinColumn());
764                                break;
765                        case REFERENCE:
766                        case URI:
767                                // we expect these values, but the pattern is slightly different;
768                                // see below
769                                break;
770                        case HAS:
771                        case SPECIAL:
772                                predicateType = PredicateBuilderTypeEnum.COORDS;
773                                supplier = () -> sqlBuilder.addCoordsPredicateBuilder(theParams.getSourceJoinColumn());
774                                break;
775                        case COMPOSITE:
776                        default:
777                                break;
778                }
779
780                if (supplier != null) {
781                        BaseSearchParamPredicateBuilder join = (BaseSearchParamPredicateBuilder) createOrReusePredicateBuilder(
782                                                        predicateType, theParams.getSourceJoinColumn(), theParams.getParamName(), supplier)
783                                        .getResult();
784
785                        return join.createPredicateParamMissingForNonReference(
786                                        theParams.getResourceType(),
787                                        theParams.getParamName(),
788                                        theParams.isMissing(),
789                                        theParams.getRequestPartitionId());
790                } else {
791                        if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
792                                SearchParamPresentPredicateBuilder join =
793                                                sqlBuilder.addSearchParamPresentPredicateBuilder(theParams.getSourceJoinColumn());
794                                return join.createPredicateParamMissingForReference(
795                                                theParams.getResourceType(),
796                                                theParams.getParamName(),
797                                                theParams.isMissing(),
798                                                theParams.getRequestPartitionId());
799                        } else if (theParams.getParamType() == RestSearchParameterTypeEnum.URI) {
800                                UriPredicateBuilder join = sqlBuilder.addUriPredicateBuilder(theParams.getSourceJoinColumn());
801                                return join.createPredicateParamMissingForNonReference(
802                                                theParams.getResourceType(),
803                                                theParams.getParamName(),
804                                                theParams.isMissing(),
805                                                theParams.getRequestPartitionId());
806                        } else {
807                                // we don't expect to see this
808                                ourLog.error("Invalid param type " + theParams.getParamType().name());
809                                return null;
810                        }
811                }
812        }
813
814        /**
815         * New way of searching for missing fields.
816         * Missing values must not indexed!
817         */
818        private Condition createMissingPredicateForUnindexedMissingFields(
819                        MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) {
820                ResourceTablePredicateBuilder table = sqlBuilder.getOrCreateResourceTablePredicateBuilder();
821
822                ICanMakeMissingParamPredicate innerQuery = PredicateBuilderFactory.createPredicateBuilderForParamType(
823                                theParams.getParamType(), theParams.getSqlBuilder(), this);
824
825                return innerQuery.createPredicateParamMissingValue(new MissingQueryParameterPredicateParams(
826                                table, theParams.isMissing(), theParams.getParamName(), theParams.getRequestPartitionId()));
827        }
828
829        public Condition createPredicateCoords(
830                        @Nullable DbColumn[] theSourceJoinColumn,
831                        String theResourceName,
832                        String theSpnamePrefix,
833                        RuntimeSearchParam theSearchParam,
834                        List<? extends IQueryParameterType> theList,
835                        RequestPartitionId theRequestPartitionId,
836                        SearchQueryBuilder theSqlBuilder) {
837                Boolean isMissing = theList.get(0).getMissing();
838                if (isMissing != null) {
839                        String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
840
841                        return createMissingParameterQuery(new MissingParameterQueryParams(
842                                        theSqlBuilder,
843                                        theSearchParam.getParamType(),
844                                        theList,
845                                        paramName,
846                                        theResourceName,
847                                        theSourceJoinColumn,
848                                        theRequestPartitionId));
849                } else {
850                        CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(
851                                                        PredicateBuilderTypeEnum.COORDS,
852                                                        theSourceJoinColumn,
853                                                        theSearchParam.getName(),
854                                                        () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn))
855                                        .getResult();
856
857                        List<Condition> codePredicates = new ArrayList<>();
858                        for (IQueryParameterType nextOr : theList) {
859                                Condition singleCode = predicateBuilder.createPredicateCoords(
860                                                mySearchParameters,
861                                                nextOr,
862                                                theResourceName,
863                                                theSearchParam,
864                                                predicateBuilder,
865                                                theRequestPartitionId);
866                                codePredicates.add(singleCode);
867                        }
868
869                        return predicateBuilder.combineWithRequestPartitionIdPredicate(
870                                        theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
871                }
872        }
873
874        public Condition createPredicateDate(
875                        @Nullable DbColumn[] theSourceJoinColumn,
876                        String theResourceName,
877                        String theSpnamePrefix,
878                        RuntimeSearchParam theSearchParam,
879                        List<? extends IQueryParameterType> theList,
880                        SearchFilterParser.CompareOperation theOperation,
881                        RequestPartitionId theRequestPartitionId) {
882                return createPredicateDate(
883                                theSourceJoinColumn,
884                                theResourceName,
885                                theSpnamePrefix,
886                                theSearchParam,
887                                theList,
888                                theOperation,
889                                theRequestPartitionId,
890                                mySqlBuilder);
891        }
892
893        public Condition createPredicateDate(
894                        @Nullable DbColumn[] theSourceJoinColumn,
895                        String theResourceName,
896                        String theSpnamePrefix,
897                        RuntimeSearchParam theSearchParam,
898                        List<? extends IQueryParameterType> theList,
899                        SearchFilterParser.CompareOperation theOperation,
900                        RequestPartitionId theRequestPartitionId,
901                        SearchQueryBuilder theSqlBuilder) {
902                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
903
904                Boolean isMissing = theList.get(0).getMissing();
905                if (isMissing != null) {
906                        return createMissingParameterQuery(new MissingParameterQueryParams(
907                                        theSqlBuilder,
908                                        theSearchParam.getParamType(),
909                                        theList,
910                                        paramName,
911                                        theResourceName,
912                                        theSourceJoinColumn,
913                                        theRequestPartitionId));
914                } else {
915                        PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult =
916                                        createOrReusePredicateBuilder(
917                                                        PredicateBuilderTypeEnum.DATE,
918                                                        theSourceJoinColumn,
919                                                        paramName,
920                                                        () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn));
921                        DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult();
922                        boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
923
924                        List<Condition> codePredicates = new ArrayList<>();
925
926                        for (IQueryParameterType nextOr : theList) {
927                                Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation);
928                                codePredicates.add(p);
929                        }
930
931                        Condition predicate = toOrPredicate(codePredicates);
932
933                        if (!cacheHit) {
934                                predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate);
935                                predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
936                        }
937
938                        return predicate;
939                }
940        }
941
942        private Condition createPredicateFilter(
943                        QueryStack theQueryStack3,
944                        SearchFilterParser.BaseFilter theFilter,
945                        String theResourceName,
946                        RequestDetails theRequest,
947                        RequestPartitionId theRequestPartitionId) {
948
949                if (theFilter instanceof SearchFilterParser.FilterParameter) {
950                        return createPredicateFilter(
951                                        theQueryStack3,
952                                        (SearchFilterParser.FilterParameter) theFilter,
953                                        theResourceName,
954                                        theRequest,
955                                        theRequestPartitionId);
956                } else if (theFilter instanceof SearchFilterParser.FilterLogical) {
957                        // Left side
958                        Condition xPredicate = createPredicateFilter(
959                                        theQueryStack3,
960                                        ((SearchFilterParser.FilterLogical) theFilter).getFilter1(),
961                                        theResourceName,
962                                        theRequest,
963                                        theRequestPartitionId);
964
965                        // Right side
966                        Condition yPredicate = createPredicateFilter(
967                                        theQueryStack3,
968                                        ((SearchFilterParser.FilterLogical) theFilter).getFilter2(),
969                                        theResourceName,
970                                        theRequest,
971                                        theRequestPartitionId);
972
973                        if (((SearchFilterParser.FilterLogical) theFilter).getOperation()
974                                        == SearchFilterParser.FilterLogicalOperation.and) {
975                                return ComboCondition.and(xPredicate, yPredicate);
976                        } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation()
977                                        == SearchFilterParser.FilterLogicalOperation.or) {
978                                return ComboCondition.or(xPredicate, yPredicate);
979                        } else {
980                                // Shouldn't happen
981                                throw new InvalidRequestException(Msg.code(1205) + "Don't know how to handle operation "
982                                                + ((SearchFilterParser.FilterLogical) theFilter).getOperation());
983                        }
984                } else {
985                        return createPredicateFilter(
986                                        theQueryStack3,
987                                        ((SearchFilterParser.FilterParameterGroup) theFilter).getContained(),
988                                        theResourceName,
989                                        theRequest,
990                                        theRequestPartitionId);
991                }
992        }
993
994        private Condition createPredicateFilter(
995                        QueryStack theQueryStack3,
996                        SearchFilterParser.FilterParameter theFilter,
997                        String theResourceName,
998                        RequestDetails theRequest,
999                        RequestPartitionId theRequestPartitionId) {
1000
1001                String paramName = theFilter.getParamPath().getName();
1002
1003                switch (paramName) {
1004                        case IAnyResource.SP_RES_ID: {
1005                                TokenParam param = new TokenParam();
1006                                param.setValueAsQueryToken(null, null, null, theFilter.getValue());
1007                                return theQueryStack3.createPredicateResourceId(
1008                                                null,
1009                                                Collections.singletonList(Collections.singletonList(param)),
1010                                                theResourceName,
1011                                                theFilter.getOperation(),
1012                                                theRequestPartitionId);
1013                        }
1014                        case Constants.PARAM_SOURCE: {
1015                                TokenParam param = new TokenParam();
1016                                param.setValueAsQueryToken(null, null, null, theFilter.getValue());
1017                                return createPredicateSource(null, Collections.singletonList(param));
1018                        }
1019                        default:
1020                                RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(
1021                                                theResourceName, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
1022                                if (searchParam == null) {
1023                                        Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(
1024                                                        theResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
1025                                        String msg = myFhirContext
1026                                                        .getLocalizer()
1027                                                        .getMessageSanitized(
1028                                                                        BaseStorageDao.class,
1029                                                                        "invalidSearchParameter",
1030                                                                        paramName,
1031                                                                        theResourceName,
1032                                                                        validNames);
1033                                        throw new InvalidRequestException(Msg.code(1206) + msg);
1034                                }
1035                                RestSearchParameterTypeEnum typeEnum = searchParam.getParamType();
1036                                if (typeEnum == RestSearchParameterTypeEnum.URI) {
1037                                        return theQueryStack3.createPredicateUri(
1038                                                        null,
1039                                                        theResourceName,
1040                                                        null,
1041                                                        searchParam,
1042                                                        Collections.singletonList(new UriParam(theFilter.getValue())),
1043                                                        theFilter.getOperation(),
1044                                                        theRequestPartitionId);
1045                                } else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
1046                                        return theQueryStack3.createPredicateString(
1047                                                        null,
1048                                                        theResourceName,
1049                                                        null,
1050                                                        searchParam,
1051                                                        Collections.singletonList(new StringParam(theFilter.getValue())),
1052                                                        theFilter.getOperation(),
1053                                                        theRequestPartitionId);
1054                                } else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
1055                                        return theQueryStack3.createPredicateDate(
1056                                                        null,
1057                                                        theResourceName,
1058                                                        null,
1059                                                        searchParam,
1060                                                        Collections.singletonList(
1061                                                                        new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())),
1062                                                        theFilter.getOperation(),
1063                                                        theRequestPartitionId);
1064                                } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
1065                                        return theQueryStack3.createPredicateNumber(
1066                                                        null,
1067                                                        theResourceName,
1068                                                        null,
1069                                                        searchParam,
1070                                                        Collections.singletonList(new NumberParam(theFilter.getValue())),
1071                                                        theFilter.getOperation(),
1072                                                        theRequestPartitionId);
1073                                } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
1074                                        SearchFilterParser.CompareOperation operation = theFilter.getOperation();
1075                                        String resourceType =
1076                                                        null; // The value can either have (Patient/123) or not have (123) a resource type, either
1077                                        // way it's not needed here
1078                                        String chain = (theFilter.getParamPath().getNext() != null)
1079                                                        ? theFilter.getParamPath().getNext().toString()
1080                                                        : null;
1081                                        String value = theFilter.getValue();
1082                                        ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
1083                                        return theQueryStack3.createPredicateReference(
1084                                                        null,
1085                                                        theResourceName,
1086                                                        paramName,
1087                                                        new ArrayList<>(),
1088                                                        Collections.singletonList(referenceParam),
1089                                                        operation,
1090                                                        theRequest,
1091                                                        theRequestPartitionId);
1092                                } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
1093                                        return theQueryStack3.createPredicateQuantity(
1094                                                        null,
1095                                                        theResourceName,
1096                                                        null,
1097                                                        searchParam,
1098                                                        Collections.singletonList(new QuantityParam(theFilter.getValue())),
1099                                                        theFilter.getOperation(),
1100                                                        theRequestPartitionId);
1101                                } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
1102                                        throw new InvalidRequestException(Msg.code(1207)
1103                                                        + "Composite search parameters not currently supported with _filter clauses");
1104                                } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) {
1105                                        TokenParam param = new TokenParam();
1106                                        param.setValueAsQueryToken(null, null, null, theFilter.getValue());
1107                                        return theQueryStack3.createPredicateToken(
1108                                                        null,
1109                                                        theResourceName,
1110                                                        null,
1111                                                        searchParam,
1112                                                        Collections.singletonList(param),
1113                                                        theFilter.getOperation(),
1114                                                        theRequestPartitionId);
1115                                }
1116                                break;
1117                }
1118                return null;
1119        }
1120
1121        private Condition createPredicateHas(
1122                        @Nullable DbColumn[] theSourceJoinColumn,
1123                        String theResourceType,
1124                        List<List<IQueryParameterType>> theHasParameters,
1125                        RequestDetails theRequest,
1126                        RequestPartitionId theRequestPartitionId) {
1127
1128                List<Condition> andPredicates = new ArrayList<>();
1129                for (List<? extends IQueryParameterType> nextOrList : theHasParameters) {
1130
1131                        String targetResourceType = null;
1132                        String paramReference = null;
1133                        String parameterName = null;
1134
1135                        String paramName = null;
1136                        List<QualifiedParamList> parameters = new ArrayList<>();
1137                        for (IQueryParameterType nextParam : nextOrList) {
1138                                HasParam next = (HasParam) nextParam;
1139                                targetResourceType = next.getTargetResourceType();
1140                                paramReference = next.getReferenceFieldName();
1141                                parameterName = next.getParameterName();
1142                                paramName = PATTERN_DOT_AND_ALL_AFTER.matcher(parameterName).replaceAll("");
1143                                parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myFhirContext)));
1144                        }
1145
1146                        if (paramName == null) {
1147                                continue;
1148                        }
1149
1150                        try {
1151                                myFhirContext.getResourceDefinition(targetResourceType);
1152                        } catch (DataFormatException e) {
1153                                throw new InvalidRequestException(Msg.code(1208) + "Invalid resource type: " + targetResourceType);
1154                        }
1155
1156                        ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
1157
1158                        if (paramName.startsWith("_has:")) {
1159
1160                                ourLog.trace("Handling double _has query: {}", paramName);
1161
1162                                String qualifier = paramName.substring(4);
1163                                for (IQueryParameterType next : nextOrList) {
1164                                        HasParam nextHasParam = new HasParam();
1165                                        nextHasParam.setValueAsQueryToken(
1166                                                        myFhirContext, PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext));
1167                                        orValues.add(nextHasParam);
1168                                }
1169
1170                        } else if (paramName.equals(PARAM_ID)) {
1171
1172                                for (IQueryParameterType next : nextOrList) {
1173                                        orValues.add(new TokenParam(next.getValueAsQueryToken(myFhirContext)));
1174                                }
1175
1176                        } else {
1177
1178                                // Ensure that the name of the search param
1179                                // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val)
1180                                // exists on the target resource type.
1181                                RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getRuntimeSearchParam(
1182                                                targetResourceType, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
1183
1184                                // Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in
1185                                // Patient?_has:Observation:subject:code=sys|val)
1186                                // exists on the target resource, or in the top-level Resource resource.
1187                                mySearchParamRegistry.getRuntimeSearchParam(
1188                                                targetResourceType, paramReference, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
1189
1190                                IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams(
1191                                                mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters);
1192
1193                                for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) {
1194                                        orValues.addAll(next.getValuesAsQueryTokens());
1195                                }
1196                        }
1197
1198                        // Handle internal chain inside the has.
1199                        if (parameterName.contains(".")) {
1200                                // Previously, for some unknown reason, we were calling getChainedPart() twice.  This broke the _has
1201                                // then chain, then _has use case by effectively cutting off the second part of the chain and
1202                                // missing one iteration of the recursive call to build the query.
1203                                // So, for example, for
1204                                // Practitioner?_has:ExplanationOfBenefit:care-team:coverage.payor._has:List:item:_id=list1
1205                                // instead of passing " payor._has:List:item:_id=list1" to the next recursion, the second call to
1206                                // getChainedPart() was wrongly removing "payor." and passing down "_has:List:item:_id=list1" instead.
1207                                // This resulted in running incorrect SQL with nonsensical join that resulted in 0 results.
1208                                // However, after running the pipeline,  I've concluded there's no use case at all for the
1209                                // double call to "getChainedPart()", which is why there's no conditional logic at all to make a double
1210                                // call to getChainedPart().
1211                                final String chainedPart = getChainedPart(parameterName);
1212
1213                                orValues.stream()
1214                                                .filter(qp -> qp instanceof ReferenceParam)
1215                                                .map(qp -> (ReferenceParam) qp)
1216                                                .forEach(rp -> rp.setChain(chainedPart));
1217
1218                                parameterName = parameterName.substring(0, parameterName.indexOf('.'));
1219                        }
1220
1221                        int colonIndex = parameterName.indexOf(':');
1222                        if (colonIndex != -1) {
1223                                parameterName = parameterName.substring(0, colonIndex);
1224                        }
1225
1226                        ResourceLinkPredicateBuilder resourceLinkTableJoin =
1227                                        mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
1228
1229                        List<String> paths = resourceLinkTableJoin.createResourceLinkPaths(
1230                                        targetResourceType, paramReference, new ArrayList<>());
1231                        if (CollectionUtils.isEmpty(paths)) {
1232                                throw new InvalidRequestException(Msg.code(2305) + "Reference field does not exist: " + paramReference);
1233                        }
1234
1235                        Condition typePredicate = BinaryCondition.equalTo(
1236                                        resourceLinkTableJoin.getColumnTargetResourceType(),
1237                                        mySqlBuilder.generatePlaceholder(theResourceType));
1238                        Condition pathPredicate = toEqualToOrInPredicate(
1239                                        resourceLinkTableJoin.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
1240
1241                        Condition linkedPredicate =
1242                                        searchForIdsWithAndOr(with().setSourceJoinColumn(resourceLinkTableJoin.getJoinColumnsForSource())
1243                                                        .setResourceName(targetResourceType)
1244                                                        .setParamName(parameterName)
1245                                                        .setAndOrParams(Collections.singletonList(orValues))
1246                                                        .setRequest(theRequest)
1247                                                        .setRequestPartitionId(theRequestPartitionId));
1248
1249                        if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1250                                andPredicates.add(toAndPredicate(pathPredicate, typePredicate, linkedPredicate));
1251                        } else {
1252                                Condition partitionPredicate = resourceLinkTableJoin.createPartitionIdPredicate(theRequestPartitionId);
1253                                andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate));
1254                        }
1255                }
1256
1257                return toAndPredicate(andPredicates);
1258        }
1259
1260        public Condition createPredicateNumber(
1261                        @Nullable DbColumn[] theSourceJoinColumn,
1262                        String theResourceName,
1263                        String theSpnamePrefix,
1264                        RuntimeSearchParam theSearchParam,
1265                        List<? extends IQueryParameterType> theList,
1266                        SearchFilterParser.CompareOperation theOperation,
1267                        RequestPartitionId theRequestPartitionId) {
1268                return createPredicateNumber(
1269                                theSourceJoinColumn,
1270                                theResourceName,
1271                                theSpnamePrefix,
1272                                theSearchParam,
1273                                theList,
1274                                theOperation,
1275                                theRequestPartitionId,
1276                                mySqlBuilder);
1277        }
1278
1279        public Condition createPredicateNumber(
1280                        @Nullable DbColumn[] theSourceJoinColumn,
1281                        String theResourceName,
1282                        String theSpnamePrefix,
1283                        RuntimeSearchParam theSearchParam,
1284                        List<? extends IQueryParameterType> theList,
1285                        SearchFilterParser.CompareOperation theOperation,
1286                        RequestPartitionId theRequestPartitionId,
1287                        SearchQueryBuilder theSqlBuilder) {
1288
1289                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1290
1291                Boolean isMissing = theList.get(0).getMissing();
1292                if (isMissing != null) {
1293                        return createMissingParameterQuery(new MissingParameterQueryParams(
1294                                        theSqlBuilder,
1295                                        theSearchParam.getParamType(),
1296                                        theList,
1297                                        paramName,
1298                                        theResourceName,
1299                                        theSourceJoinColumn,
1300                                        theRequestPartitionId));
1301                } else {
1302                        NumberPredicateBuilder join = createOrReusePredicateBuilder(
1303                                                        PredicateBuilderTypeEnum.NUMBER,
1304                                                        theSourceJoinColumn,
1305                                                        paramName,
1306                                                        () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn))
1307                                        .getResult();
1308
1309                        List<Condition> codePredicates = new ArrayList<>();
1310                        for (IQueryParameterType nextOr : theList) {
1311
1312                                if (nextOr instanceof NumberParam) {
1313                                        NumberParam param = (NumberParam) nextOr;
1314
1315                                        BigDecimal value = param.getValue();
1316                                        if (value == null) {
1317                                                continue;
1318                                        }
1319
1320                                        SearchFilterParser.CompareOperation operation = theOperation;
1321                                        if (operation == null) {
1322                                                operation = toOperation(param.getPrefix());
1323                                        }
1324
1325                                        Condition predicate = join.createPredicateNumeric(
1326                                                        theResourceName, paramName, operation, value, theRequestPartitionId, nextOr);
1327                                        codePredicates.add(predicate);
1328
1329                                } else {
1330                                        throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass());
1331                                }
1332                        }
1333
1334                        return join.combineWithRequestPartitionIdPredicate(
1335                                        theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
1336                }
1337        }
1338
1339        public Condition createPredicateQuantity(
1340                        @Nullable DbColumn[] theSourceJoinColumn,
1341                        String theResourceName,
1342                        String theSpnamePrefix,
1343                        RuntimeSearchParam theSearchParam,
1344                        List<? extends IQueryParameterType> theList,
1345                        SearchFilterParser.CompareOperation theOperation,
1346                        RequestPartitionId theRequestPartitionId) {
1347                return createPredicateQuantity(
1348                                theSourceJoinColumn,
1349                                theResourceName,
1350                                theSpnamePrefix,
1351                                theSearchParam,
1352                                theList,
1353                                theOperation,
1354                                theRequestPartitionId,
1355                                mySqlBuilder);
1356        }
1357
1358        public Condition createPredicateQuantity(
1359                        @Nullable DbColumn[] theSourceJoinColumn,
1360                        String theResourceName,
1361                        String theSpnamePrefix,
1362                        RuntimeSearchParam theSearchParam,
1363                        List<? extends IQueryParameterType> theList,
1364                        SearchFilterParser.CompareOperation theOperation,
1365                        RequestPartitionId theRequestPartitionId,
1366                        SearchQueryBuilder theSqlBuilder) {
1367
1368                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1369
1370                Boolean isMissing = theList.get(0).getMissing();
1371                if (isMissing != null) {
1372                        return createMissingParameterQuery(new MissingParameterQueryParams(
1373                                        theSqlBuilder,
1374                                        theSearchParam.getParamType(),
1375                                        theList,
1376                                        paramName,
1377                                        theResourceName,
1378                                        theSourceJoinColumn,
1379                                        theRequestPartitionId));
1380                } else {
1381                        List<QuantityParam> quantityParams =
1382                                        theList.stream().map(QuantityParam::toQuantityParam).collect(Collectors.toList());
1383
1384                        BaseQuantityPredicateBuilder join = null;
1385                        boolean normalizedSearchEnabled = myStorageSettings
1386                                        .getNormalizedQuantitySearchLevel()
1387                                        .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
1388                        if (normalizedSearchEnabled) {
1389                                List<QuantityParam> normalizedQuantityParams = quantityParams.stream()
1390                                                .map(UcumServiceUtil::toCanonicalQuantityOrNull)
1391                                                .filter(Objects::nonNull)
1392                                                .collect(Collectors.toList());
1393
1394                                if (normalizedQuantityParams.size() == quantityParams.size()) {
1395                                        join = createOrReusePredicateBuilder(
1396                                                                        PredicateBuilderTypeEnum.QUANTITY,
1397                                                                        theSourceJoinColumn,
1398                                                                        paramName,
1399                                                                        () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn))
1400                                                        .getResult();
1401                                        quantityParams = normalizedQuantityParams;
1402                                }
1403                        }
1404
1405                        if (join == null) {
1406                                join = createOrReusePredicateBuilder(
1407                                                                PredicateBuilderTypeEnum.QUANTITY,
1408                                                                theSourceJoinColumn,
1409                                                                paramName,
1410                                                                () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn))
1411                                                .getResult();
1412                        }
1413
1414                        List<Condition> codePredicates = new ArrayList<>();
1415                        for (QuantityParam nextOr : quantityParams) {
1416                                Condition singleCode = join.createPredicateQuantity(
1417                                                nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId);
1418                                codePredicates.add(singleCode);
1419                        }
1420
1421                        return join.combineWithRequestPartitionIdPredicate(
1422                                        theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
1423                }
1424        }
1425
1426        public Condition createPredicateReference(
1427                        @Nullable DbColumn[] theSourceJoinColumn,
1428                        String theResourceName,
1429                        String theParamName,
1430                        List<String> theQualifiers,
1431                        List<? extends IQueryParameterType> theList,
1432                        SearchFilterParser.CompareOperation theOperation,
1433                        RequestDetails theRequest,
1434                        RequestPartitionId theRequestPartitionId) {
1435                return createPredicateReference(
1436                                theSourceJoinColumn,
1437                                theResourceName,
1438                                theParamName,
1439                                theQualifiers,
1440                                theList,
1441                                theOperation,
1442                                theRequest,
1443                                theRequestPartitionId,
1444                                mySqlBuilder);
1445        }
1446
1447        public Condition createPredicateReference(
1448                        @Nullable DbColumn[] theSourceJoinColumn,
1449                        String theResourceName,
1450                        String theParamName,
1451                        List<String> theQualifiers,
1452                        List<? extends IQueryParameterType> theList,
1453                        SearchFilterParser.CompareOperation theOperation,
1454                        RequestDetails theRequest,
1455                        RequestPartitionId theRequestPartitionId,
1456                        SearchQueryBuilder theSqlBuilder) {
1457
1458                if ((theOperation != null)
1459                                && (theOperation != SearchFilterParser.CompareOperation.eq)
1460                                && (theOperation != SearchFilterParser.CompareOperation.ne)) {
1461                        throw new InvalidRequestException(
1462                                        Msg.code(1212)
1463                                                        + "Invalid operator specified for reference predicate.  Supported operators for reference predicate are \"eq\" and \"ne\".");
1464                }
1465
1466                Boolean isMissing = theList.get(0).getMissing();
1467                if (isMissing != null) {
1468                        return createMissingParameterQuery(new MissingParameterQueryParams(
1469                                        theSqlBuilder,
1470                                        RestSearchParameterTypeEnum.REFERENCE,
1471                                        theList,
1472                                        theParamName,
1473                                        theResourceName,
1474                                        theSourceJoinColumn,
1475                                        theRequestPartitionId));
1476                } else {
1477                        ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(
1478                                                        PredicateBuilderTypeEnum.REFERENCE,
1479                                                        theSourceJoinColumn,
1480                                                        theParamName,
1481                                                        () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn))
1482                                        .getResult();
1483                        return predicateBuilder.createPredicate(
1484                                        theRequest,
1485                                        theResourceName,
1486                                        theParamName,
1487                                        theQualifiers,
1488                                        theList,
1489                                        theOperation,
1490                                        theRequestPartitionId);
1491                }
1492        }
1493
1494        public void addGrouping() {
1495                if (!myGroupingAdded) {
1496                        BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
1497
1498                        /*
1499                         * Postgres and Oracle don't like it if we are doing a SELECT DISTINCT
1500                         * with multiple selected columns but no GROUP BY clause.
1501                         */
1502                        if (mySqlBuilder.isSelectPartitionId()) {
1503                                mySqlBuilder
1504                                                .getSelect()
1505                                                .addGroupings(
1506                                                                firstPredicateBuilder.getPartitionIdColumn(),
1507                                                                firstPredicateBuilder.getResourceIdColumn());
1508                        } else {
1509                                mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getJoinColumns());
1510                        }
1511                        myGroupingAdded = true;
1512                }
1513        }
1514
1515        public void addOrdering() {
1516                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
1517                mySqlBuilder.getSelect().addOrderings(firstPredicateBuilder.getJoinColumns());
1518        }
1519
1520        public Condition createPredicateReferenceForEmbeddedChainedSearchResource(
1521                        @Nullable DbColumn[] theSourceJoinColumn,
1522                        String theResourceName,
1523                        RuntimeSearchParam theSearchParam,
1524                        List<? extends IQueryParameterType> theList,
1525                        SearchFilterParser.CompareOperation theOperation,
1526                        RequestDetails theRequest,
1527                        RequestPartitionId theRequestPartitionId,
1528                        EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) {
1529
1530                boolean wantChainedAndNormal =
1531                                theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN;
1532
1533                // 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
1534                // builders across different subselects
1535                EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes =
1536                                EnumSet.copyOf(myReusePredicateBuilderTypes);
1537                if (wantChainedAndNormal) {
1538                        myReusePredicateBuilderTypes.clear();
1539                }
1540
1541                ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor();
1542                chainExtractor.deriveChains(theResourceName, theSearchParam, theList);
1543                Map<List<ChainElement>, Set<LeafNodeDefinition>> chains = chainExtractor.getChains();
1544
1545                Map<List<String>, Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap();
1546                for (List<ChainElement> nextChain : chains.keySet()) {
1547                        Set<LeafNodeDefinition> leafNodes = chains.get(nextChain);
1548
1549                        collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum);
1550                }
1551
1552                UnionQuery union = null;
1553                List<Condition> predicates = null;
1554                if (wantChainedAndNormal) {
1555                        union = new UnionQuery(SetOperationQuery.Type.UNION_ALL);
1556                } else {
1557                        predicates = new ArrayList<>();
1558                }
1559
1560                predicates = new ArrayList<>();
1561                for (List<String> nextReferenceLink : referenceLinks.keySet()) {
1562                        for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) {
1563                                SearchQueryBuilder builder;
1564                                if (wantChainedAndNormal) {
1565                                        builder = mySqlBuilder.newChildSqlBuilder(mySqlBuilder.isIncludePartitionIdInJoins());
1566                                } else {
1567                                        builder = mySqlBuilder;
1568                                }
1569
1570                                DbColumn[] previousJoinColumn = null;
1571
1572                                // Create a reference link predicates to the subselect for every link but the last one
1573                                for (String nextLink : nextReferenceLink) {
1574                                        // We don't want to call createPredicateReference() here, because the whole point is to avoid the
1575                                        // recursion.
1576                                        // TODO: Are we missing any important business logic from that method? All tests are passing.
1577                                        ResourceLinkPredicateBuilder resourceLinkPredicateBuilder =
1578                                                        builder.addReferencePredicateBuilder(this, previousJoinColumn);
1579                                        builder.addPredicate(
1580                                                        resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink)));
1581                                        previousJoinColumn = resourceLinkPredicateBuilder.getJoinColumnsForTarget();
1582                                }
1583
1584                                Condition containedCondition = createIndexPredicate(
1585                                                previousJoinColumn,
1586                                                leafNodeDefinition.getLeafTarget(),
1587                                                leafNodeDefinition.getLeafPathPrefix(),
1588                                                leafNodeDefinition.getLeafParamName(),
1589                                                leafNodeDefinition.getParamDefinition(),
1590                                                leafNodeDefinition.getOrValues(),
1591                                                theOperation,
1592                                                leafNodeDefinition.getQualifiers(),
1593                                                theRequest,
1594                                                theRequestPartitionId,
1595                                                builder);
1596
1597                                if (wantChainedAndNormal) {
1598                                        builder.addPredicate(containedCondition);
1599                                        union.addQueries(builder.getSelect());
1600                                } else {
1601                                        predicates.add(containedCondition);
1602                                }
1603                        }
1604                }
1605
1606                Condition retVal;
1607                if (wantChainedAndNormal) {
1608
1609                        if (theSourceJoinColumn == null) {
1610                                BaseJoiningPredicateBuilder root = mySqlBuilder.getOrCreateFirstPredicateBuilder(false);
1611                                DbColumn[] joinColumns = root.getJoinColumns();
1612                                Object joinColumnObject;
1613                                if (joinColumns.length == 1) {
1614                                        joinColumnObject = joinColumns[0];
1615                                } else {
1616                                        joinColumnObject = ColumnTupleObject.from(joinColumns);
1617                                }
1618                                retVal = new InCondition(joinColumnObject, union);
1619                        } else {
1620                                // -- for the resource link, need join with target_resource_id
1621                                retVal = new InCondition(theSourceJoinColumn, union);
1622                        }
1623
1624                } else {
1625
1626                        retVal = toOrPredicate(predicates);
1627                }
1628
1629                // restore the state of this collection to turn caching back on before we exit
1630                myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes);
1631                return retVal;
1632        }
1633
1634        private void collateChainedSearchOptions(
1635                        Map<List<String>, Set<LeafNodeDefinition>> referenceLinks,
1636                        List<ChainElement> nextChain,
1637                        Set<LeafNodeDefinition> leafNodes,
1638                        EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) {
1639                // Manually collapse the chain using all possible variants of contained resource patterns.
1640                // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this
1641                // someday?
1642                // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper
1643                // support for `_contained`
1644                if (nextChain.size() == 1) {
1645                        // discrete -> discrete
1646                        if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) {
1647                                // If !theWantChainedAndNormal that means we're only processing refchains
1648                                // so the discrete -> contained case is the only one that applies
1649                                updateMapOfReferenceLinks(
1650                                                referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes);
1651                        }
1652
1653                        // discrete -> contained
1654                        RuntimeSearchParam firstParamDefinition =
1655                                        leafNodes.iterator().next().getParamDefinition();
1656                        updateMapOfReferenceLinks(
1657                                        referenceLinks,
1658                                        Lists.newArrayList(),
1659                                        leafNodes.stream()
1660                                                        .map(t -> t.withPathPrefix(
1661                                                                        nextChain.get(0).getResourceType(),
1662                                                                        nextChain.get(0).getSearchParameterName()))
1663                                                        // When we're handling discrete->contained the differences between search
1664                                                        // parameters don't matter. E.g. if we're processing "subject.name=foo"
1665                                                        // the name could be Patient:name or Group:name but it doesn't actually
1666                                                        // matter that these are different since in this case both of these end
1667                                                        // up being an identical search in the string table for "subject.name".
1668                                                        .map(t -> t.withParam(firstParamDefinition))
1669                                                        .collect(Collectors.toSet()));
1670                } else if (nextChain.size() == 2) {
1671                        // discrete -> discrete -> discrete
1672                        updateMapOfReferenceLinks(
1673                                        referenceLinks,
1674                                        Lists.newArrayList(
1675                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1676                                        leafNodes);
1677                        // discrete -> discrete -> contained
1678                        updateMapOfReferenceLinks(
1679                                        referenceLinks,
1680                                        Lists.newArrayList(nextChain.get(0).getPath()),
1681                                        leafNodes.stream()
1682                                                        .map(t -> t.withPathPrefix(
1683                                                                        nextChain.get(1).getResourceType(),
1684                                                                        nextChain.get(1).getSearchParameterName()))
1685                                                        .collect(Collectors.toSet()));
1686                        // discrete -> contained -> discrete
1687                        updateMapOfReferenceLinks(
1688                                        referenceLinks,
1689                                        Lists.newArrayList(mergePaths(
1690                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath())),
1691                                        leafNodes);
1692                        if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
1693                                // discrete -> contained -> contained
1694                                updateMapOfReferenceLinks(
1695                                                referenceLinks,
1696                                                Lists.newArrayList(),
1697                                                leafNodes.stream()
1698                                                                .map(t -> t.withPathPrefix(
1699                                                                                nextChain.get(0).getResourceType(),
1700                                                                                nextChain.get(0).getSearchParameterName() + "."
1701                                                                                                + nextChain.get(1).getSearchParameterName()))
1702                                                                .collect(Collectors.toSet()));
1703                        }
1704                } else if (nextChain.size() == 3) {
1705                        // discrete -> discrete -> discrete -> discrete
1706                        updateMapOfReferenceLinks(
1707                                        referenceLinks,
1708                                        Lists.newArrayList(
1709                                                        nextChain.get(0).getPath(),
1710                                                        nextChain.get(1).getPath(),
1711                                                        nextChain.get(2).getPath()),
1712                                        leafNodes);
1713                        // discrete -> discrete -> discrete -> contained
1714                        updateMapOfReferenceLinks(
1715                                        referenceLinks,
1716                                        Lists.newArrayList(
1717                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1718                                        leafNodes.stream()
1719                                                        .map(t -> t.withPathPrefix(
1720                                                                        nextChain.get(2).getResourceType(),
1721                                                                        nextChain.get(2).getSearchParameterName()))
1722                                                        .collect(Collectors.toSet()));
1723                        // discrete -> discrete -> contained -> discrete
1724                        updateMapOfReferenceLinks(
1725                                        referenceLinks,
1726                                        Lists.newArrayList(
1727                                                        nextChain.get(0).getPath(),
1728                                                        mergePaths(
1729                                                                        nextChain.get(1).getPath(), nextChain.get(2).getPath())),
1730                                        leafNodes);
1731                        // discrete -> contained -> discrete -> discrete
1732                        updateMapOfReferenceLinks(
1733                                        referenceLinks,
1734                                        Lists.newArrayList(
1735                                                        mergePaths(
1736                                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1737                                                        nextChain.get(2).getPath()),
1738                                        leafNodes);
1739                        // discrete -> contained -> discrete -> contained
1740                        updateMapOfReferenceLinks(
1741                                        referenceLinks,
1742                                        Lists.newArrayList(mergePaths(
1743                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath())),
1744                                        leafNodes.stream()
1745                                                        .map(t -> t.withPathPrefix(
1746                                                                        nextChain.get(2).getResourceType(),
1747                                                                        nextChain.get(2).getSearchParameterName()))
1748                                                        .collect(Collectors.toSet()));
1749                        if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
1750                                // discrete -> contained -> contained -> discrete
1751                                updateMapOfReferenceLinks(
1752                                                referenceLinks,
1753                                                Lists.newArrayList(mergePaths(
1754                                                                nextChain.get(0).getPath(),
1755                                                                nextChain.get(1).getPath(),
1756                                                                nextChain.get(2).getPath())),
1757                                                leafNodes);
1758                                // discrete -> discrete -> contained -> contained
1759                                updateMapOfReferenceLinks(
1760                                                referenceLinks,
1761                                                Lists.newArrayList(nextChain.get(0).getPath()),
1762                                                leafNodes.stream()
1763                                                                .map(t -> t.withPathPrefix(
1764                                                                                nextChain.get(1).getResourceType(),
1765                                                                                nextChain.get(1).getSearchParameterName() + "."
1766                                                                                                + nextChain.get(2).getSearchParameterName()))
1767                                                                .collect(Collectors.toSet()));
1768                                // discrete -> contained -> contained -> contained
1769                                updateMapOfReferenceLinks(
1770                                                referenceLinks,
1771                                                Lists.newArrayList(),
1772                                                leafNodes.stream()
1773                                                                .map(t -> t.withPathPrefix(
1774                                                                                nextChain.get(0).getResourceType(),
1775                                                                                nextChain.get(0).getSearchParameterName() + "."
1776                                                                                                + nextChain.get(1).getSearchParameterName() + "."
1777                                                                                                + nextChain.get(2).getSearchParameterName()))
1778                                                                .collect(Collectors.toSet()));
1779                        }
1780                } else {
1781                        // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever
1782                        // needs this, we should revisit the approach
1783                        throw new InvalidRequestException(Msg.code(2011)
1784                                        + "The search chain is too long. Only chains of up to three references are supported.");
1785                }
1786        }
1787
1788        private void updateMapOfReferenceLinks(
1789                        Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap,
1790                        ArrayList<String> thePath,
1791                        Set<LeafNodeDefinition> theLeafNodesToAdd) {
1792                Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.get(thePath);
1793                if (leafNodes == null) {
1794                        leafNodes = Sets.newHashSet();
1795                        theReferenceLinksMap.put(thePath, leafNodes);
1796                }
1797                leafNodes.addAll(theLeafNodesToAdd);
1798        }
1799
1800        private String mergePaths(String... paths) {
1801                String result = "";
1802                for (String nextPath : paths) {
1803                        int separatorIndex = nextPath.indexOf('.');
1804                        if (StringUtils.isEmpty(result)) {
1805                                result = nextPath;
1806                        } else {
1807                                result = result + nextPath.substring(separatorIndex);
1808                        }
1809                }
1810                return result;
1811        }
1812
1813        private Condition createIndexPredicate(
1814                        DbColumn[] theSourceJoinColumn,
1815                        String theResourceName,
1816                        String theSpnamePrefix,
1817                        String theParamName,
1818                        RuntimeSearchParam theParamDefinition,
1819                        ArrayList<IQueryParameterType> theOrValues,
1820                        SearchFilterParser.CompareOperation theOperation,
1821                        List<String> theQualifiers,
1822                        RequestDetails theRequest,
1823                        RequestPartitionId theRequestPartitionId,
1824                        SearchQueryBuilder theSqlBuilder) {
1825                Condition containedCondition;
1826
1827                switch (theParamDefinition.getParamType()) {
1828                        case DATE:
1829                                containedCondition = createPredicateDate(
1830                                                theSourceJoinColumn,
1831                                                theResourceName,
1832                                                theSpnamePrefix,
1833                                                theParamDefinition,
1834                                                theOrValues,
1835                                                theOperation,
1836                                                theRequestPartitionId,
1837                                                theSqlBuilder);
1838                                break;
1839                        case NUMBER:
1840                                containedCondition = createPredicateNumber(
1841                                                theSourceJoinColumn,
1842                                                theResourceName,
1843                                                theSpnamePrefix,
1844                                                theParamDefinition,
1845                                                theOrValues,
1846                                                theOperation,
1847                                                theRequestPartitionId,
1848                                                theSqlBuilder);
1849                                break;
1850                        case QUANTITY:
1851                                containedCondition = createPredicateQuantity(
1852                                                theSourceJoinColumn,
1853                                                theResourceName,
1854                                                theSpnamePrefix,
1855                                                theParamDefinition,
1856                                                theOrValues,
1857                                                theOperation,
1858                                                theRequestPartitionId,
1859                                                theSqlBuilder);
1860                                break;
1861                        case STRING:
1862                                containedCondition = createPredicateString(
1863                                                theSourceJoinColumn,
1864                                                theResourceName,
1865                                                theSpnamePrefix,
1866                                                theParamDefinition,
1867                                                theOrValues,
1868                                                theOperation,
1869                                                theRequestPartitionId,
1870                                                theSqlBuilder);
1871                                break;
1872                        case TOKEN:
1873                                containedCondition = createPredicateToken(
1874                                                theSourceJoinColumn,
1875                                                theResourceName,
1876                                                theSpnamePrefix,
1877                                                theParamDefinition,
1878                                                theOrValues,
1879                                                theOperation,
1880                                                theRequestPartitionId,
1881                                                theSqlBuilder);
1882                                break;
1883                        case COMPOSITE:
1884                                containedCondition = createPredicateComposite(
1885                                                theSourceJoinColumn,
1886                                                theResourceName,
1887                                                theSpnamePrefix,
1888                                                theParamDefinition,
1889                                                theOrValues,
1890                                                theRequestPartitionId,
1891                                                theSqlBuilder);
1892                                break;
1893                        case URI:
1894                                containedCondition = createPredicateUri(
1895                                                theSourceJoinColumn,
1896                                                theResourceName,
1897                                                theSpnamePrefix,
1898                                                theParamDefinition,
1899                                                theOrValues,
1900                                                theOperation,
1901                                                theRequestPartitionId,
1902                                                theSqlBuilder);
1903                                break;
1904                        case REFERENCE:
1905                                containedCondition = createPredicateReference(
1906                                                theSourceJoinColumn,
1907                                                theResourceName,
1908                                                isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName,
1909                                                theQualifiers,
1910                                                theOrValues,
1911                                                theOperation,
1912                                                theRequest,
1913                                                theRequestPartitionId,
1914                                                theSqlBuilder);
1915                                break;
1916                        case HAS:
1917                        case SPECIAL:
1918                        default:
1919                                throw new InvalidRequestException(
1920                                                Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported.");
1921                }
1922                return containedCondition;
1923        }
1924
1925        @Nullable
1926        public Condition createPredicateResourceId(
1927                        @Nullable DbColumn[] theSourceJoinColumn,
1928                        List<List<IQueryParameterType>> theValues,
1929                        String theResourceName,
1930                        SearchFilterParser.CompareOperation theOperation,
1931                        RequestPartitionId theRequestPartitionId) {
1932                ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder();
1933                return builder.createPredicateResourceId(
1934                                theSourceJoinColumn, theResourceName, theValues, theOperation, theRequestPartitionId);
1935        }
1936
1937        private Condition createPredicateSourceForAndList(
1938                        @Nullable DbColumn[] theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) {
1939                mySqlBuilder.getOrCreateFirstPredicateBuilder();
1940
1941                List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size());
1942                for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1943                        andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd));
1944                }
1945                return toAndPredicate(andPredicates);
1946        }
1947
1948        private Condition createPredicateSource(
1949                        @Nullable DbColumn[] theSourceJoinColumn, List<? extends IQueryParameterType> theList) {
1950                if (myStorageSettings.getStoreMetaSourceInformation()
1951                                == JpaStorageSettings.StoreMetaSourceInformationEnum.NONE) {
1952                        String msg = myFhirContext.getLocalizer().getMessage(QueryStack.class, "sourceParamDisabled");
1953                        throw new InvalidRequestException(Msg.code(1216) + msg);
1954                }
1955
1956                List<Condition> orPredicates = new ArrayList<>();
1957
1958                // :missing=true modifier processing requires "LEFT JOIN" with HFJ_RESOURCE table to return correct results
1959                // if both sourceUri and requestId are not populated for the resource
1960                Optional<? extends IQueryParameterType> isMissingSourceOptional = theList.stream()
1961                                .filter(nextParameter -> nextParameter.getMissing() != null && nextParameter.getMissing())
1962                                .findFirst();
1963
1964                if (isMissingSourceOptional.isPresent()) {
1965                        ISourcePredicateBuilder join =
1966                                        getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER);
1967                        orPredicates.add(join.createPredicateMissingSourceUri());
1968                        return toOrPredicate(orPredicates);
1969                }
1970                // for all other cases we use "INNER JOIN" to match search parameters
1971                ISourcePredicateBuilder join = getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.INNER);
1972
1973                for (IQueryParameterType nextParameter : theList) {
1974                        SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext));
1975                        String sourceUri = sourceParameter.getSourceUri();
1976                        String requestId = sourceParameter.getRequestId();
1977                        if (isNotBlank(sourceUri) && isNotBlank(requestId)) {
1978                                orPredicates.add(toAndPredicate(
1979                                                join.createPredicateSourceUri(sourceUri), join.createPredicateRequestId(requestId)));
1980                        } else if (isNotBlank(sourceUri)) {
1981                                orPredicates.add(
1982                                                join.createPredicateSourceUriWithModifiers(nextParameter, myStorageSettings, sourceUri));
1983                        } else if (isNotBlank(requestId)) {
1984                                orPredicates.add(join.createPredicateRequestId(requestId));
1985                        }
1986                }
1987
1988                return toOrPredicate(orPredicates);
1989        }
1990
1991        private ISourcePredicateBuilder getSourcePredicateBuilder(
1992                        @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
1993                if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) {
1994                        return createOrReusePredicateBuilder(
1995                                                        PredicateBuilderTypeEnum.SOURCE,
1996                                                        theSourceJoinColumn,
1997                                                        Constants.PARAM_SOURCE,
1998                                                        () -> mySqlBuilder.addResourceHistoryProvenancePredicateBuilder(
1999                                                                        theSourceJoinColumn, theJoinType))
2000                                        .getResult();
2001                }
2002                return createOrReusePredicateBuilder(
2003                                                PredicateBuilderTypeEnum.SOURCE,
2004                                                theSourceJoinColumn,
2005                                                Constants.PARAM_SOURCE,
2006                                                () -> mySqlBuilder.addResourceHistoryPredicateBuilder(theSourceJoinColumn, theJoinType))
2007                                .getResult();
2008        }
2009
2010        public Condition createPredicateString(
2011                        @Nullable DbColumn[] theSourceJoinColumn,
2012                        String theResourceName,
2013                        String theSpnamePrefix,
2014                        RuntimeSearchParam theSearchParam,
2015                        List<? extends IQueryParameterType> theList,
2016                        SearchFilterParser.CompareOperation theOperation,
2017                        RequestPartitionId theRequestPartitionId) {
2018                return createPredicateString(
2019                                theSourceJoinColumn,
2020                                theResourceName,
2021                                theSpnamePrefix,
2022                                theSearchParam,
2023                                theList,
2024                                theOperation,
2025                                theRequestPartitionId,
2026                                mySqlBuilder);
2027        }
2028
2029        public Condition createPredicateString(
2030                        @Nullable DbColumn[] theSourceJoinColumn,
2031                        String theResourceName,
2032                        String theSpnamePrefix,
2033                        RuntimeSearchParam theSearchParam,
2034                        List<? extends IQueryParameterType> theList,
2035                        SearchFilterParser.CompareOperation theOperation,
2036                        RequestPartitionId theRequestPartitionId,
2037                        SearchQueryBuilder theSqlBuilder) {
2038                Boolean isMissing = theList.get(0).getMissing();
2039                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
2040
2041                if (isMissing != null) {
2042                        return createMissingParameterQuery(new MissingParameterQueryParams(
2043                                        theSqlBuilder,
2044                                        theSearchParam.getParamType(),
2045                                        theList,
2046                                        paramName,
2047                                        theResourceName,
2048                                        theSourceJoinColumn,
2049                                        theRequestPartitionId));
2050                }
2051
2052                StringPredicateBuilder join = createOrReusePredicateBuilder(
2053                                                PredicateBuilderTypeEnum.STRING,
2054                                                theSourceJoinColumn,
2055                                                paramName,
2056                                                () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn))
2057                                .getResult();
2058
2059                List<Condition> codePredicates = new ArrayList<>();
2060                for (IQueryParameterType nextOr : theList) {
2061                        Condition singleCode = join.createPredicateString(
2062                                        nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation);
2063                        codePredicates.add(singleCode);
2064                }
2065
2066                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates));
2067        }
2068
2069        public Condition createPredicateTag(
2070                        @Nullable DbColumn[] theSourceJoinColumn,
2071                        List<List<IQueryParameterType>> theList,
2072                        String theParamName,
2073                        RequestPartitionId theRequestPartitionId) {
2074                TagTypeEnum tagType;
2075                if (Constants.PARAM_TAG.equals(theParamName)) {
2076                        tagType = TagTypeEnum.TAG;
2077                } else if (Constants.PARAM_PROFILE.equals(theParamName)) {
2078                        tagType = TagTypeEnum.PROFILE;
2079                } else if (Constants.PARAM_SECURITY.equals(theParamName)) {
2080                        tagType = TagTypeEnum.SECURITY_LABEL;
2081                } else {
2082                        throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen
2083                }
2084
2085                List<Condition> andPredicates = new ArrayList<>();
2086                for (List<? extends IQueryParameterType> nextAndParams : theList) {
2087                        if (!checkHaveTags(nextAndParams, theParamName)) {
2088                                continue;
2089                        }
2090
2091                        List<Triple<String, String, String>> tokens = Lists.newArrayList();
2092                        boolean paramInverted = populateTokens(tokens, nextAndParams);
2093                        if (tokens.isEmpty()) {
2094                                continue;
2095                        }
2096
2097                        Condition tagPredicate;
2098                        BaseJoiningPredicateBuilder join;
2099                        if (paramInverted) {
2100
2101                                boolean selectPartitionId = myPartitionSettings.isPartitionIdsInPrimaryKeys();
2102                                SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder(selectPartitionId);
2103                                TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null);
2104                                sqlBuilder.addPredicate(
2105                                                tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId));
2106                                SelectQuery sql = sqlBuilder.getSelect();
2107
2108                                join = mySqlBuilder.getOrCreateFirstPredicateBuilder();
2109                                Expression subSelect = new Subquery(sql);
2110
2111                                Object left;
2112                                if (selectPartitionId) {
2113                                        left = new ColumnTupleObject(join.getJoinColumns());
2114                                } else {
2115                                        left = join.getResourceIdColumn();
2116                                }
2117                                tagPredicate = new InCondition(left, subSelect).setNegate(true);
2118
2119                        } else {
2120                                // Tag table can't be a query root because it will include deleted resources, and can't select by
2121                                // resource type
2122                                mySqlBuilder.getOrCreateFirstPredicateBuilder();
2123
2124                                TagPredicateBuilder tagJoin = createOrReusePredicateBuilder(
2125                                                                PredicateBuilderTypeEnum.TAG,
2126                                                                theSourceJoinColumn,
2127                                                                theParamName,
2128                                                                () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn))
2129                                                .getResult();
2130                                tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId);
2131                                join = tagJoin;
2132                        }
2133
2134                        andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate));
2135                }
2136
2137                return toAndPredicate(andPredicates);
2138        }
2139
2140        private boolean populateTokens(
2141                        List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) {
2142                boolean paramInverted = false;
2143
2144                for (IQueryParameterType nextOrParam : theAndParams) {
2145                        String code;
2146                        String system;
2147                        if (nextOrParam instanceof TokenParam) {
2148                                TokenParam nextParam = (TokenParam) nextOrParam;
2149                                code = nextParam.getValue();
2150                                system = nextParam.getSystem();
2151                                if (nextParam.getModifier() == TokenParamModifier.NOT) {
2152                                        paramInverted = true;
2153                                }
2154                        } else if (nextOrParam instanceof ReferenceParam) {
2155                                ReferenceParam nextParam = (ReferenceParam) nextOrParam;
2156                                code = nextParam.getValue();
2157                                system = null;
2158                        } else {
2159                                UriParam nextParam = (UriParam) nextOrParam;
2160                                code = nextParam.getValue();
2161                                system = null;
2162                        }
2163
2164                        if (isNotBlank(code)) {
2165                                theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code));
2166                        }
2167                }
2168                return paramInverted;
2169        }
2170
2171        private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) {
2172                for (IQueryParameterType nextParamUncasted : theParams) {
2173                        if (nextParamUncasted instanceof TokenParam) {
2174                                TokenParam nextParam = (TokenParam) nextParamUncasted;
2175                                if (isNotBlank(nextParam.getValue())) {
2176                                        return true;
2177                                }
2178                                if (isNotBlank(nextParam.getSystem())) {
2179                                        throw new TokenParamFormatInvalidRequestException(
2180                                                        Msg.code(1218), theParamName, nextParam.getValueAsQueryToken(myFhirContext));
2181                                }
2182                        }
2183
2184                        if (nextParamUncasted instanceof ReferenceParam
2185                                        && isNotBlank(((ReferenceParam) nextParamUncasted).getValue())) {
2186                                return true;
2187                        } else if (nextParamUncasted instanceof UriParam && isNotBlank(((UriParam) nextParamUncasted).getValue())) {
2188                                return true;
2189                        }
2190                }
2191
2192                return false;
2193        }
2194
2195        public Condition createPredicateToken(
2196                        @Nullable DbColumn[] theSourceJoinColumn,
2197                        String theResourceName,
2198                        String theSpnamePrefix,
2199                        RuntimeSearchParam theSearchParam,
2200                        List<? extends IQueryParameterType> theList,
2201                        SearchFilterParser.CompareOperation theOperation,
2202                        RequestPartitionId theRequestPartitionId) {
2203                return createPredicateToken(
2204                                theSourceJoinColumn,
2205                                theResourceName,
2206                                theSpnamePrefix,
2207                                theSearchParam,
2208                                theList,
2209                                theOperation,
2210                                theRequestPartitionId,
2211                                mySqlBuilder);
2212        }
2213
2214        public Condition createPredicateToken(
2215                        @Nullable DbColumn[] theSourceJoinColumn,
2216                        String theResourceName,
2217                        String theSpnamePrefix,
2218                        RuntimeSearchParam theSearchParam,
2219                        List<? extends IQueryParameterType> theList,
2220                        SearchFilterParser.CompareOperation theOperation,
2221                        RequestPartitionId theRequestPartitionId,
2222                        SearchQueryBuilder theSqlBuilder) {
2223
2224                List<IQueryParameterType> tokens = new ArrayList<>();
2225
2226                boolean paramInverted = false;
2227                TokenParamModifier modifier;
2228
2229                for (IQueryParameterType nextOr : theList) {
2230                        if (nextOr instanceof TokenParam) {
2231                                if (!((TokenParam) nextOr).isEmpty()) {
2232                                        TokenParam id = (TokenParam) nextOr;
2233                                        if (id.isText()) {
2234
2235                                                // Check whether the :text modifier is actually enabled here
2236                                                boolean tokenTextIndexingEnabled =
2237                                                                BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(
2238                                                                                myStorageSettings, theSearchParam);
2239                                                if (!tokenTextIndexingEnabled) {
2240                                                        String msg;
2241                                                        if (myStorageSettings.isSuppressStringIndexingInTokens()) {
2242                                                                msg = myFhirContext
2243                                                                                .getLocalizer()
2244                                                                                .getMessage(QueryStack.class, "textModifierDisabledForServer");
2245                                                        } else {
2246                                                                msg = myFhirContext
2247                                                                                .getLocalizer()
2248                                                                                .getMessage(QueryStack.class, "textModifierDisabledForSearchParam");
2249                                                        }
2250                                                        throw new MethodNotAllowedException(Msg.code(1219) + msg);
2251                                                }
2252                                                return createPredicateString(
2253                                                                theSourceJoinColumn,
2254                                                                theResourceName,
2255                                                                theSpnamePrefix,
2256                                                                theSearchParam,
2257                                                                theList,
2258                                                                null,
2259                                                                theRequestPartitionId,
2260                                                                theSqlBuilder);
2261                                        }
2262
2263                                        modifier = id.getModifier();
2264                                        // for :not modifier, create a token and remove the :not modifier
2265                                        if (modifier == TokenParamModifier.NOT) {
2266                                                tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue()));
2267                                                paramInverted = true;
2268                                        } else {
2269                                                tokens.add(nextOr);
2270                                        }
2271                                }
2272                        } else {
2273                                tokens.add(nextOr);
2274                        }
2275                }
2276
2277                if (tokens.isEmpty()) {
2278                        return null;
2279                }
2280
2281                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
2282                Condition predicate;
2283                BaseJoiningPredicateBuilder join;
2284
2285                if (paramInverted) {
2286                        boolean selectPartitionId = myPartitionSettings.isPartitionIdsInPrimaryKeys();
2287                        SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder(selectPartitionId);
2288                        TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null);
2289                        sqlBuilder.addPredicate(tokenSelector.createPredicateToken(
2290                                        tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId));
2291                        SelectQuery sql = sqlBuilder.getSelect();
2292                        Expression subSelect = new Subquery(sql);
2293
2294                        join = theSqlBuilder.getOrCreateFirstPredicateBuilder();
2295
2296                        DbColumn[] leftColumns;
2297                        if (theSourceJoinColumn == null) {
2298                                leftColumns = join.getJoinColumns();
2299                        } else {
2300                                leftColumns = theSourceJoinColumn;
2301                        }
2302
2303                        Object left = new ColumnTupleObject(leftColumns);
2304                        predicate = new InCondition(left, subSelect).setNegate(true);
2305
2306                } else {
2307                        Boolean isMissing = theList.get(0).getMissing();
2308                        if (isMissing != null) {
2309                                return createMissingParameterQuery(new MissingParameterQueryParams(
2310                                                theSqlBuilder,
2311                                                theSearchParam.getParamType(),
2312                                                theList,
2313                                                paramName,
2314                                                theResourceName,
2315                                                theSourceJoinColumn,
2316                                                theRequestPartitionId));
2317                        }
2318
2319                        TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(
2320                                                        PredicateBuilderTypeEnum.TOKEN,
2321                                                        theSourceJoinColumn,
2322                                                        paramName,
2323                                                        () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn))
2324                                        .getResult();
2325
2326                        predicate = tokenJoin.createPredicateToken(
2327                                        tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
2328                        join = tokenJoin;
2329                }
2330
2331                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
2332        }
2333
2334        public Condition createPredicateUri(
2335                        @Nullable DbColumn[] theSourceJoinColumn,
2336                        String theResourceName,
2337                        String theSpnamePrefix,
2338                        RuntimeSearchParam theSearchParam,
2339                        List<? extends IQueryParameterType> theList,
2340                        SearchFilterParser.CompareOperation theOperation,
2341                        RequestPartitionId theRequestPartitionId) {
2342                return createPredicateUri(
2343                                theSourceJoinColumn,
2344                                theResourceName,
2345                                theSpnamePrefix,
2346                                theSearchParam,
2347                                theList,
2348                                theOperation,
2349                                theRequestPartitionId,
2350                                mySqlBuilder);
2351        }
2352
2353        public Condition createPredicateUri(
2354                        @Nullable DbColumn[] theSourceJoinColumn,
2355                        String theResourceName,
2356                        String theSpnamePrefix,
2357                        RuntimeSearchParam theSearchParam,
2358                        List<? extends IQueryParameterType> theList,
2359                        SearchFilterParser.CompareOperation theOperation,
2360                        RequestPartitionId theRequestPartitionId,
2361                        SearchQueryBuilder theSqlBuilder) {
2362
2363                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
2364
2365                Boolean isMissing = theList.get(0).getMissing();
2366                if (isMissing != null) {
2367                        return createMissingParameterQuery(new MissingParameterQueryParams(
2368                                        theSqlBuilder,
2369                                        theSearchParam.getParamType(),
2370                                        theList,
2371                                        paramName,
2372                                        theResourceName,
2373                                        theSourceJoinColumn,
2374                                        theRequestPartitionId));
2375                } else {
2376                        UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn);
2377
2378                        Condition predicate = join.addPredicate(theList, paramName, theOperation, myRequestDetails);
2379                        return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
2380                }
2381        }
2382
2383        public QueryStack newChildQueryFactoryWithFullBuilderReuse() {
2384                return new QueryStack(
2385                                myRequestDetails,
2386                                mySearchParameters,
2387                                myStorageSettings,
2388                                myFhirContext,
2389                                mySqlBuilder,
2390                                mySearchParamRegistry,
2391                                myPartitionSettings,
2392                                EnumSet.allOf(PredicateBuilderTypeEnum.class));
2393        }
2394
2395        @Nullable
2396        public Condition searchForIdsWithAndOr(SearchForIdsParams theSearchForIdsParams) {
2397
2398                if (theSearchForIdsParams.myAndOrParams.isEmpty()) {
2399                        return null;
2400                }
2401
2402                switch (theSearchForIdsParams.myParamName) {
2403                        case IAnyResource.SP_RES_ID:
2404                                return createPredicateResourceId(
2405                                                theSearchForIdsParams.mySourceJoinColumn,
2406                                                theSearchForIdsParams.myAndOrParams,
2407                                                theSearchForIdsParams.myResourceName,
2408                                                null,
2409                                                theSearchForIdsParams.myRequestPartitionId);
2410
2411                        case Constants.PARAM_PID:
2412                                return createPredicateResourcePID(
2413                                                theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams);
2414
2415                        case PARAM_HAS:
2416                                return createPredicateHas(
2417                                                theSearchForIdsParams.mySourceJoinColumn,
2418                                                theSearchForIdsParams.myResourceName,
2419                                                theSearchForIdsParams.myAndOrParams,
2420                                                theSearchForIdsParams.myRequest,
2421                                                theSearchForIdsParams.myRequestPartitionId);
2422
2423                        case Constants.PARAM_TAG:
2424                        case Constants.PARAM_PROFILE:
2425                        case Constants.PARAM_SECURITY:
2426                                if (myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE) {
2427                                        return createPredicateSearchParameter(
2428                                                        theSearchForIdsParams.mySourceJoinColumn,
2429                                                        theSearchForIdsParams.myResourceName,
2430                                                        theSearchForIdsParams.myParamName,
2431                                                        theSearchForIdsParams.myAndOrParams,
2432                                                        theSearchForIdsParams.myRequest,
2433                                                        theSearchForIdsParams.myRequestPartitionId);
2434                                } else {
2435                                        return createPredicateTag(
2436                                                        theSearchForIdsParams.mySourceJoinColumn,
2437                                                        theSearchForIdsParams.myAndOrParams,
2438                                                        theSearchForIdsParams.myParamName,
2439                                                        theSearchForIdsParams.myRequestPartitionId);
2440                                }
2441
2442                        case Constants.PARAM_SOURCE:
2443                                return createPredicateSourceForAndList(
2444                                                theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams);
2445
2446                        case Constants.PARAM_LASTUPDATED:
2447                                // this case statement handles a _lastUpdated query as part of a reverse search
2448                                // only (/Patient?_has:Encounter:patient:_lastUpdated=ge2023-10-24).
2449                                // performing a _lastUpdated query on a resource (/Patient?_lastUpdated=eq2023-10-24)
2450                                // is handled in {@link SearchBuilder#createChunkedQuery}.
2451                                return createReverseSearchPredicateLastUpdated(
2452                                                theSearchForIdsParams.myAndOrParams, theSearchForIdsParams.mySourceJoinColumn);
2453
2454                        default:
2455                                return createPredicateSearchParameter(
2456                                                theSearchForIdsParams.mySourceJoinColumn,
2457                                                theSearchForIdsParams.myResourceName,
2458                                                theSearchForIdsParams.myParamName,
2459                                                theSearchForIdsParams.myAndOrParams,
2460                                                theSearchForIdsParams.myRequest,
2461                                                theSearchForIdsParams.myRequestPartitionId);
2462                }
2463        }
2464
2465        /**
2466         * Raw match on RES_ID
2467         */
2468        private Condition createPredicateResourcePID(
2469                        DbColumn[] theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) {
2470                DbColumn pidColumn = getResourceIdColumn(theSourceJoinColumn);
2471
2472                if (pidColumn == null) {
2473                        BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
2474                        pidColumn = predicateBuilder.getResourceIdColumn();
2475                }
2476
2477                // we don't support any modifiers for now
2478                Set<Long> pids = theAndOrParams.stream()
2479                                .map(orList -> orList.stream()
2480                                                .map(v -> v.getValueAsQueryToken(myFhirContext))
2481                                                .map(Long::valueOf)
2482                                                .collect(Collectors.toSet()))
2483                                .reduce(Sets::intersection)
2484                                .orElse(Set.of());
2485
2486                if (pids.isEmpty()) {
2487                        mySqlBuilder.setMatchNothing();
2488                        return null;
2489                }
2490
2491                return toEqualToOrInPredicate(pidColumn, mySqlBuilder.generatePlaceholders(pids));
2492        }
2493
2494        private Condition createReverseSearchPredicateLastUpdated(
2495                        List<List<IQueryParameterType>> theAndOrParams, DbColumn[] theSourceColumn) {
2496
2497                ResourceTablePredicateBuilder resourceTableJoin =
2498                                mySqlBuilder.addResourceTablePredicateBuilder(theSourceColumn);
2499
2500                List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size());
2501
2502                for (List<IQueryParameterType> aList : theAndOrParams) {
2503                        if (!aList.isEmpty()) {
2504                                DateParam dateParam = (DateParam) aList.get(0);
2505                                DateRangeParam dateRangeParam = new DateRangeParam(dateParam);
2506                                Condition aCondition = mySqlBuilder.addPredicateLastUpdated(dateRangeParam, resourceTableJoin);
2507                                andPredicates.add(aCondition);
2508                        }
2509                }
2510
2511                return toAndPredicate(andPredicates);
2512        }
2513
2514        @Nullable
2515        private Condition createPredicateSearchParameter(
2516                        @Nullable DbColumn[] theSourceJoinColumn,
2517                        String theResourceName,
2518                        String theParamName,
2519                        List<List<IQueryParameterType>> theAndOrParams,
2520                        RequestDetails theRequest,
2521                        RequestPartitionId theRequestPartitionId) {
2522                List<Condition> andPredicates = new ArrayList<>();
2523                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(
2524                                theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2525                if (nextParamDef != null) {
2526
2527                        if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) {
2528                                if (theRequestPartitionId.isAllPartitions()) {
2529                                        throw new PreconditionFailedException(
2530                                                        Msg.code(1220) + "This server is not configured to support search against all partitions");
2531                                }
2532                        }
2533
2534                        switch (nextParamDef.getParamType()) {
2535                                case DATE:
2536                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2537                                                // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt'
2538                                                // to create the predicateDate instead of generic one with operation = null
2539                                                SearchFilterParser.CompareOperation operation = null;
2540                                                if (nextAnd.size() > 0) {
2541                                                        DateParam param = (DateParam) nextAnd.get(0);
2542                                                        operation = toOperation(param.getPrefix());
2543                                                }
2544                                                andPredicates.add(createPredicateDate(
2545                                                                theSourceJoinColumn,
2546                                                                theResourceName,
2547                                                                null,
2548                                                                nextParamDef,
2549                                                                nextAnd,
2550                                                                operation,
2551                                                                theRequestPartitionId));
2552                                        }
2553                                        break;
2554                                case QUANTITY:
2555                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2556                                                SearchFilterParser.CompareOperation operation = null;
2557                                                if (nextAnd.size() > 0) {
2558                                                        QuantityParam param = (QuantityParam) nextAnd.get(0);
2559                                                        operation = toOperation(param.getPrefix());
2560                                                }
2561                                                andPredicates.add(createPredicateQuantity(
2562                                                                theSourceJoinColumn,
2563                                                                theResourceName,
2564                                                                null,
2565                                                                nextParamDef,
2566                                                                nextAnd,
2567                                                                operation,
2568                                                                theRequestPartitionId));
2569                                        }
2570                                        break;
2571                                case REFERENCE:
2572                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2573
2574                                                // Handle Search Parameters where the name is a full chain
2575                                                // (e.g. SearchParameter with name=composition.patient.identifier)
2576                                                if (handleFullyChainedParameter(
2577                                                                theSourceJoinColumn,
2578                                                                theResourceName,
2579                                                                theParamName,
2580                                                                theRequest,
2581                                                                theRequestPartitionId,
2582                                                                andPredicates,
2583                                                                nextAnd)) {
2584                                                        continue;
2585                                                }
2586
2587                                                EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum =
2588                                                                isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd);
2589                                                if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) {
2590                                                        andPredicates.add(createPredicateReference(
2591                                                                        theSourceJoinColumn,
2592                                                                        theResourceName,
2593                                                                        theParamName,
2594                                                                        new ArrayList<>(),
2595                                                                        nextAnd,
2596                                                                        null,
2597                                                                        theRequest,
2598                                                                        theRequestPartitionId));
2599                                                } else {
2600                                                        andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource(
2601                                                                        theSourceJoinColumn,
2602                                                                        theResourceName,
2603                                                                        nextParamDef,
2604                                                                        nextAnd,
2605                                                                        null,
2606                                                                        theRequest,
2607                                                                        theRequestPartitionId,
2608                                                                        embeddedChainedSearchModeEnum));
2609                                                }
2610                                        }
2611                                        break;
2612                                case STRING:
2613                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2614                                                andPredicates.add(createPredicateString(
2615                                                                theSourceJoinColumn,
2616                                                                theResourceName,
2617                                                                null,
2618                                                                nextParamDef,
2619                                                                nextAnd,
2620                                                                SearchFilterParser.CompareOperation.sw,
2621                                                                theRequestPartitionId));
2622                                        }
2623                                        break;
2624                                case TOKEN:
2625                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2626                                                if (LOCATION_POSITION.equals(nextParamDef.getPath())) {
2627                                                        andPredicates.add(createPredicateCoords(
2628                                                                        theSourceJoinColumn,
2629                                                                        theResourceName,
2630                                                                        null,
2631                                                                        nextParamDef,
2632                                                                        nextAnd,
2633                                                                        theRequestPartitionId,
2634                                                                        mySqlBuilder));
2635                                                } else {
2636                                                        andPredicates.add(createPredicateToken(
2637                                                                        theSourceJoinColumn,
2638                                                                        theResourceName,
2639                                                                        null,
2640                                                                        nextParamDef,
2641                                                                        nextAnd,
2642                                                                        null,
2643                                                                        theRequestPartitionId));
2644                                                }
2645                                        }
2646                                        break;
2647                                case NUMBER:
2648                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2649                                                andPredicates.add(createPredicateNumber(
2650                                                                theSourceJoinColumn,
2651                                                                theResourceName,
2652                                                                null,
2653                                                                nextParamDef,
2654                                                                nextAnd,
2655                                                                null,
2656                                                                theRequestPartitionId));
2657                                        }
2658                                        break;
2659                                case COMPOSITE:
2660                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2661                                                andPredicates.add(createPredicateComposite(
2662                                                                theSourceJoinColumn,
2663                                                                theResourceName,
2664                                                                null,
2665                                                                nextParamDef,
2666                                                                nextAnd,
2667                                                                theRequestPartitionId));
2668                                        }
2669                                        break;
2670                                case URI:
2671                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2672                                                andPredicates.add(createPredicateUri(
2673                                                                theSourceJoinColumn,
2674                                                                theResourceName,
2675                                                                null,
2676                                                                nextParamDef,
2677                                                                nextAnd,
2678                                                                SearchFilterParser.CompareOperation.eq,
2679                                                                theRequestPartitionId));
2680                                        }
2681                                        break;
2682                                case HAS:
2683                                case SPECIAL:
2684                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2685                                                if (LOCATION_POSITION.equals(nextParamDef.getPath())) {
2686                                                        andPredicates.add(createPredicateCoords(
2687                                                                        theSourceJoinColumn,
2688                                                                        theResourceName,
2689                                                                        null,
2690                                                                        nextParamDef,
2691                                                                        nextAnd,
2692                                                                        theRequestPartitionId,
2693                                                                        mySqlBuilder));
2694                                                }
2695                                        }
2696                                        break;
2697                        }
2698                } else {
2699                        // These are handled later
2700                        if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) {
2701                                if (Constants.PARAM_FILTER.equals(theParamName)) {
2702
2703                                        // Parse the predicates enumerated in the _filter separated by AND or OR...
2704                                        if (theAndOrParams.get(0).get(0) instanceof StringParam) {
2705                                                String filterString =
2706                                                                ((StringParam) theAndOrParams.get(0).get(0)).getValue();
2707                                                SearchFilterParser.BaseFilter filter;
2708                                                try {
2709                                                        filter = SearchFilterParser.parse(filterString);
2710                                                } catch (SearchFilterParser.FilterSyntaxException theE) {
2711                                                        throw new InvalidRequestException(
2712                                                                        Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage());
2713                                                }
2714                                                if (filter != null) {
2715
2716                                                        if (!myStorageSettings.isFilterParameterEnabled()) {
2717                                                                throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER
2718                                                                                + " parameter is disabled on this server");
2719                                                        }
2720
2721                                                        Condition predicate = createPredicateFilter(
2722                                                                        this, filter, theResourceName, theRequest, theRequestPartitionId);
2723                                                        if (predicate != null) {
2724                                                                mySqlBuilder.addPredicate(predicate);
2725                                                        }
2726                                                }
2727                                        }
2728
2729                                } else {
2730                                        RuntimeSearchParam notEnabledForSearchParam = mySearchParamRegistry.getActiveSearchParam(
2731                                                        theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.ALL);
2732                                        if (notEnabledForSearchParam == null) {
2733                                                String msg = myFhirContext
2734                                                                .getLocalizer()
2735                                                                .getMessageSanitized(
2736                                                                                BaseStorageDao.class,
2737                                                                                "invalidSearchParameter",
2738                                                                                theParamName,
2739                                                                                theResourceName,
2740                                                                                mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(
2741                                                                                                theResourceName,
2742                                                                                                ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH));
2743                                                throw new InvalidRequestException(Msg.code(1223) + msg);
2744                                        } else {
2745                                                String msg = myFhirContext
2746                                                                .getLocalizer()
2747                                                                .getMessageSanitized(
2748                                                                                BaseStorageDao.class,
2749                                                                                "invalidSearchParameterNotEnabledForSearch",
2750                                                                                theParamName,
2751                                                                                theResourceName,
2752                                                                                mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(
2753                                                                                                theResourceName,
2754                                                                                                ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH));
2755                                                throw new InvalidRequestException(Msg.code(2540) + msg);
2756                                        }
2757                                }
2758                        }
2759                }
2760
2761                return toAndPredicate(andPredicates);
2762        }
2763
2764        /**
2765         * This method handles the case of Search Parameters where the name/code
2766         * in the SP is a full chain expression. Normally to handle an expression
2767         * like <code>Observation?subject.name=foo</code> are handled by a SP
2768         * with a type of REFERENCE where the name is "subject". That is not
2769         * handled here. On the other hand, if the SP has a name value containing
2770         * the full chain (e.g. "subject.name") we handle that here.
2771         *
2772         * @return Returns {@literal true} if the search parameter was handled
2773         * by this method
2774         */
2775        private boolean handleFullyChainedParameter(
2776                        @Nullable DbColumn[] theSourceJoinColumn,
2777                        String theResourceName,
2778                        String theParamName,
2779                        RequestDetails theRequest,
2780                        RequestPartitionId theRequestPartitionId,
2781                        List<Condition> andPredicates,
2782                        List<? extends IQueryParameterType> nextAnd) {
2783                if (!nextAnd.isEmpty() && nextAnd.get(0) instanceof ReferenceParam) {
2784                        ReferenceParam param = (ReferenceParam) nextAnd.get(0);
2785                        if (isNotBlank(param.getChain())) {
2786                                String fullName = theParamName + "." + param.getChain();
2787                                RuntimeSearchParam fullChainParam = mySearchParamRegistry.getActiveSearchParam(
2788                                                theResourceName, fullName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2789                                if (fullChainParam != null) {
2790                                        List<IQueryParameterType> swappedParamTypes = nextAnd.stream()
2791                                                        .map(t -> newParameterInstance(fullChainParam, null, t.getValueAsQueryToken(myFhirContext)))
2792                                                        .collect(Collectors.toList());
2793                                        List<List<IQueryParameterType>> params = List.of(swappedParamTypes);
2794                                        Condition predicate = createPredicateSearchParameter(
2795                                                        theSourceJoinColumn, theResourceName, fullName, params, theRequest, theRequestPartitionId);
2796                                        andPredicates.add(predicate);
2797                                        return true;
2798                                }
2799                        }
2800                }
2801                return false;
2802        }
2803
2804        /**
2805         * When searching using a chained search expression (e.g. "Patient?organization.name=foo")
2806         * we have a few options:
2807         * <ul>
2808         * <li>
2809         *    A. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceLink} for
2810         *    paramName="organization" with a join on {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString}
2811         *    with paramName="name", that's {@link EmbeddedChainedSearchModeEnum#REF_JOIN_ONLY}
2812         *    which is the standard searching case. Let's guess that 99.9% of all searches work
2813         *    this way.
2814         * </ul>
2815         * <li>
2816         *    B. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString}
2817         *    with paramName="organization.name", that's {@link EmbeddedChainedSearchModeEnum#UPLIFTED_ONLY}.
2818         *    We only do this if there is an uplifted refchain declared on the "organization"
2819         *    search parameter for the "name" search parameter, and contained indexing is disabled.
2820         *    This kind of index can come from indexing normal references where the search parameter
2821         *      has an uplifted refchain declared, and it can also come from indexing contained resources.
2822         *      For both of these cases, the actual index in the database is identical. But the important
2823         *    difference is that when you're searching for contained resources you also want to
2824         *    search for normal references. When you're searching for explicit refchains, no normal
2825         *    indexes matter because they'd be a duplicate of the uplifted refchain.
2826         * </li>
2827         * <li>
2828         *    C. We can also do both and return a union of the two, using
2829         *    {@link EmbeddedChainedSearchModeEnum#UPLIFTED_AND_REF_JOIN}. We do that if contained
2830         *    resource indexing is enabled since we have to assume there may be indexes
2831         *    on "organization" for both contained and non-contained Organization.
2832         *    resources.
2833         * </li>
2834         */
2835        private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch(
2836                        String theResourceType, String theParameterName, List<? extends IQueryParameterType> theParameter) {
2837                boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources();
2838                boolean indexOnUpliftedRefchains = myStorageSettings.isIndexOnUpliftedRefchains();
2839
2840                if (!indexOnContainedResources && !indexOnUpliftedRefchains) {
2841                        return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY;
2842                }
2843
2844                boolean haveUpliftCandidates = theParameter.stream()
2845                                .filter(t -> t instanceof ReferenceParam)
2846                                .map(t -> ((ReferenceParam) t).getChain())
2847                                .filter(StringUtils::isNotBlank)
2848                                // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we
2849                                // ever want to support this, it would be really hard to do.
2850                                .filter(t -> !t.startsWith(PARAM_HAS + ":"))
2851                                .anyMatch(t -> {
2852                                        if (indexOnContainedResources) {
2853                                                return true;
2854                                        }
2855                                        RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
2856                                                        theResourceType,
2857                                                        theParameterName,
2858                                                        ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2859                                        return param != null && param.hasUpliftRefchain(t);
2860                                });
2861
2862                if (haveUpliftCandidates) {
2863                        if (indexOnContainedResources) {
2864                                return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN;
2865                        }
2866                        return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY;
2867                } else {
2868                        return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY;
2869                }
2870        }
2871
2872        public void addPredicateCompositeUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) {
2873                ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
2874                Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexStrings);
2875                mySqlBuilder.addPredicate(predicate);
2876        }
2877
2878        public void addPredicateCompositeNonUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) {
2879                ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder =
2880                                mySqlBuilder.addComboNonUniquePredicateBuilder();
2881                Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexStrings);
2882                mySqlBuilder.addPredicate(predicate);
2883        }
2884
2885        // expand out the pids
2886        public void addPredicateEverythingOperation(
2887                        String theResourceName, List<String> theTypeSourceResourceNames, JpaPid... theTargetPids) {
2888                ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null);
2889                Condition predicate =
2890                                table.createEverythingPredicate(theResourceName, theTypeSourceResourceNames, theTargetPids);
2891                mySqlBuilder.addPredicate(predicate);
2892                mySqlBuilder.getSelect().setIsDistinct(true);
2893                addGrouping();
2894        }
2895
2896        public IQueryParameterType newParameterInstance(
2897                        RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) {
2898                IQueryParameterType qp = newParameterInstance(theParam);
2899
2900                qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken);
2901                return qp;
2902        }
2903
2904        private IQueryParameterType newParameterInstance(RuntimeSearchParam theParam) {
2905
2906                IQueryParameterType qp;
2907                switch (theParam.getParamType()) {
2908                        case DATE:
2909                                qp = new DateParam();
2910                                break;
2911                        case NUMBER:
2912                                qp = new NumberParam();
2913                                break;
2914                        case QUANTITY:
2915                                qp = new QuantityParam();
2916                                break;
2917                        case STRING:
2918                                qp = new StringParam();
2919                                break;
2920                        case TOKEN:
2921                                qp = new TokenParam();
2922                                break;
2923                        case COMPOSITE:
2924                                List<RuntimeSearchParam> compositeOf =
2925                                                JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam);
2926                                if (compositeOf.size() != 2) {
2927                                        throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has "
2928                                                        + compositeOf.size() + " composite parts. Don't know how handlt this.");
2929                                }
2930                                IQueryParameterType leftParam = newParameterInstance(compositeOf.get(0));
2931                                IQueryParameterType rightParam = newParameterInstance(compositeOf.get(1));
2932                                qp = new CompositeParam<>(leftParam, rightParam);
2933                                break;
2934                        case URI:
2935                                qp = new UriParam();
2936                                break;
2937                        case REFERENCE:
2938                                qp = new ReferenceParam();
2939                                break;
2940                        case SPECIAL:
2941                                qp = new SpecialParam();
2942                                break;
2943                        case HAS:
2944                        default:
2945                                throw new InvalidRequestException(
2946                                                Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported.");
2947                }
2948                return qp;
2949        }
2950
2951        /**
2952         * @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum
2953         */
2954        enum EmbeddedChainedSearchModeEnum {
2955                UPLIFTED_ONLY(true),
2956                UPLIFTED_AND_REF_JOIN(true),
2957                REF_JOIN_ONLY(false);
2958
2959                private final boolean mySupportsUplifted;
2960
2961                EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) {
2962                        mySupportsUplifted = theSupportsUplifted;
2963                }
2964
2965                public boolean supportsUplifted() {
2966                        return mySupportsUplifted;
2967                }
2968        }
2969
2970        private static final class ChainElement {
2971                private final String myResourceType;
2972                private final String mySearchParameterName;
2973                private final String myPath;
2974
2975                public ChainElement(String theResourceType, String theSearchParameterName, String thePath) {
2976                        this.myResourceType = theResourceType;
2977                        this.mySearchParameterName = theSearchParameterName;
2978                        this.myPath = thePath;
2979                }
2980
2981                public String getResourceType() {
2982                        return myResourceType;
2983                }
2984
2985                public String getPath() {
2986                        return myPath;
2987                }
2988
2989                public String getSearchParameterName() {
2990                        return mySearchParameterName;
2991                }
2992
2993                @Override
2994                public boolean equals(Object o) {
2995                        if (this == o) return true;
2996                        if (o == null || getClass() != o.getClass()) return false;
2997                        ChainElement that = (ChainElement) o;
2998                        return myResourceType.equals(that.myResourceType)
2999                                        && mySearchParameterName.equals(that.mySearchParameterName)
3000                                        && myPath.equals(that.myPath);
3001                }
3002
3003                @Override
3004                public int hashCode() {
3005                        return Objects.hash(myResourceType, mySearchParameterName, myPath);
3006                }
3007        }
3008
3009        private class ReferenceChainExtractor {
3010                private final Map<List<ChainElement>, Set<LeafNodeDefinition>> myChains = Maps.newHashMap();
3011
3012                public Map<List<ChainElement>, Set<LeafNodeDefinition>> getChains() {
3013                        return myChains;
3014                }
3015
3016                private boolean isReferenceParamValid(ReferenceParam theReferenceParam) {
3017                        return split(theReferenceParam.getChain(), '.').length <= 3;
3018                }
3019
3020                private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) {
3021                        List<String> pathsForType = theSearchParam.getPathsSplit().stream()
3022                                        .map(String::trim)
3023                                        .filter(t -> (t.startsWith(theResourceType) || t.startsWith("(" + theResourceType)))
3024                                        .collect(Collectors.toList());
3025                        if (pathsForType.isEmpty()) {
3026                                ourLog.warn(
3027                                                "Search parameter {} does not have a path for resource type {}.",
3028                                                theSearchParam.getName(),
3029                                                theResourceType);
3030                        }
3031
3032                        return pathsForType;
3033                }
3034
3035                public void deriveChains(
3036                                String theResourceType,
3037                                RuntimeSearchParam theSearchParam,
3038                                List<? extends IQueryParameterType> theList) {
3039                        List<String> paths = extractPaths(theResourceType, theSearchParam);
3040                        for (String path : paths) {
3041                                List<ChainElement> searchParams = Lists.newArrayList();
3042                                searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path));
3043                                for (IQueryParameterType nextOr : theList) {
3044                                        String targetValue = nextOr.getValueAsQueryToken(myFhirContext);
3045                                        if (nextOr instanceof ReferenceParam) {
3046                                                ReferenceParam referenceParam = (ReferenceParam) nextOr;
3047                                                if (!isReferenceParamValid(referenceParam)) {
3048                                                        throw new InvalidRequestException(Msg.code(2007) + "The search chain "
3049                                                                        + theSearchParam.getName() + "." + referenceParam.getChain()
3050                                                                        + " is too long. Only chains up to three references are supported.");
3051                                                }
3052
3053                                                String targetChain = referenceParam.getChain();
3054                                                List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType());
3055
3056                                                processNextLinkInChain(
3057                                                                searchParams,
3058                                                                theSearchParam,
3059                                                                targetChain,
3060                                                                targetValue,
3061                                                                qualifiers,
3062                                                                referenceParam.getResourceType());
3063                                        }
3064                                }
3065                        }
3066                }
3067
3068                private void processNextLinkInChain(
3069                                List<ChainElement> theSearchParams,
3070                                RuntimeSearchParam thePreviousSearchParam,
3071                                String theChain,
3072                                String theTargetValue,
3073                                List<String> theQualifiers,
3074                                String theResourceType) {
3075
3076                        String nextParamName = theChain;
3077                        String nextChain = null;
3078                        String nextQualifier = null;
3079                        int linkIndex = theChain.indexOf('.');
3080                        if (linkIndex != -1) {
3081                                nextParamName = theChain.substring(0, linkIndex);
3082                                nextChain = theChain.substring(linkIndex + 1);
3083                        }
3084
3085                        int qualifierIndex = nextParamName.indexOf(':');
3086                        if (qualifierIndex != -1) {
3087                                nextParamName = nextParamName.substring(0, qualifierIndex);
3088                                nextQualifier = nextParamName.substring(qualifierIndex);
3089                        }
3090
3091                        List<String> qualifiersBranch = Lists.newArrayList();
3092                        qualifiersBranch.addAll(theQualifiers);
3093                        qualifiersBranch.add(nextQualifier);
3094
3095                        boolean searchParamFound = false;
3096                        for (String nextTarget : thePreviousSearchParam.getTargets()) {
3097                                RuntimeSearchParam nextSearchParam = null;
3098                                if (isBlank(theResourceType) || theResourceType.equals(nextTarget)) {
3099                                        nextSearchParam = mySearchParamRegistry.getActiveSearchParam(
3100                                                        nextTarget, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
3101                                }
3102                                if (nextSearchParam != null) {
3103                                        searchParamFound = true;
3104                                        // If we find a search param on this resource type for this parameter name, keep iterating
3105                                        //  Otherwise, abandon this branch and carry on to the next one
3106                                        if (StringUtils.isEmpty(nextChain)) {
3107                                                // We've reached the end of the chain
3108                                                ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
3109
3110                                                if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) {
3111                                                        orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue));
3112                                                } else {
3113                                                        IQueryParameterType qp = newParameterInstance(nextSearchParam);
3114                                                        qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue);
3115                                                        orValues.add(qp);
3116                                                }
3117
3118                                                Set<LeafNodeDefinition> leafNodes = myChains.get(theSearchParams);
3119                                                if (leafNodes == null) {
3120                                                        leafNodes = Sets.newHashSet();
3121                                                        myChains.put(theSearchParams, leafNodes);
3122                                                }
3123                                                leafNodes.add(new LeafNodeDefinition(
3124                                                                nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch));
3125                                        } else {
3126                                                List<String> nextPaths = extractPaths(nextTarget, nextSearchParam);
3127                                                for (String nextPath : nextPaths) {
3128                                                        List<ChainElement> searchParamBranch = Lists.newArrayList();
3129                                                        searchParamBranch.addAll(theSearchParams);
3130
3131                                                        searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath));
3132                                                        processNextLinkInChain(
3133                                                                        searchParamBranch,
3134                                                                        nextSearchParam,
3135                                                                        nextChain,
3136                                                                        theTargetValue,
3137                                                                        qualifiersBranch,
3138                                                                        nextQualifier);
3139                                                }
3140                                        }
3141                                }
3142                        }
3143                        if (!searchParamFound) {
3144                                throw new InvalidRequestException(Msg.code(1214)
3145                                                + myFhirContext
3146                                                                .getLocalizer()
3147                                                                .getMessage(
3148                                                                                BaseStorageDao.class,
3149                                                                                "invalidParameterChain",
3150                                                                                thePreviousSearchParam.getName() + '.' + theChain));
3151                        }
3152                }
3153        }
3154
3155        private static class LeafNodeDefinition {
3156                private final RuntimeSearchParam myParamDefinition;
3157                private final ArrayList<IQueryParameterType> myOrValues;
3158                private final String myLeafTarget;
3159                private final String myLeafParamName;
3160                private final String myLeafPathPrefix;
3161                private final List<String> myQualifiers;
3162
3163                public LeafNodeDefinition(
3164                                RuntimeSearchParam theParamDefinition,
3165                                ArrayList<IQueryParameterType> theOrValues,
3166                                String theLeafTarget,
3167                                String theLeafParamName,
3168                                String theLeafPathPrefix,
3169                                List<String> theQualifiers) {
3170                        myParamDefinition = theParamDefinition;
3171                        myOrValues = theOrValues;
3172                        myLeafTarget = theLeafTarget;
3173                        myLeafParamName = theLeafParamName;
3174                        myLeafPathPrefix = theLeafPathPrefix;
3175                        myQualifiers = theQualifiers;
3176                }
3177
3178                public RuntimeSearchParam getParamDefinition() {
3179                        return myParamDefinition;
3180                }
3181
3182                public ArrayList<IQueryParameterType> getOrValues() {
3183                        return myOrValues;
3184                }
3185
3186                public String getLeafTarget() {
3187                        return myLeafTarget;
3188                }
3189
3190                public String getLeafParamName() {
3191                        return myLeafParamName;
3192                }
3193
3194                public String getLeafPathPrefix() {
3195                        return myLeafPathPrefix;
3196                }
3197
3198                public List<String> getQualifiers() {
3199                        return myQualifiers;
3200                }
3201
3202                public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) {
3203                        return new LeafNodeDefinition(
3204                                        myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers);
3205                }
3206
3207                @Override
3208                public boolean equals(Object o) {
3209                        if (this == o) return true;
3210                        if (o == null || getClass() != o.getClass()) return false;
3211                        LeafNodeDefinition that = (LeafNodeDefinition) o;
3212                        return Objects.equals(myParamDefinition, that.myParamDefinition)
3213                                        && Objects.equals(myOrValues, that.myOrValues)
3214                                        && Objects.equals(myLeafTarget, that.myLeafTarget)
3215                                        && Objects.equals(myLeafParamName, that.myLeafParamName)
3216                                        && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix)
3217                                        && Objects.equals(myQualifiers, that.myQualifiers);
3218                }
3219
3220                @Override
3221                public int hashCode() {
3222                        return Objects.hash(
3223                                        myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers);
3224                }
3225
3226                /**
3227                 * Return a copy of this object with the given {@link RuntimeSearchParam}
3228                 * but all other values unchanged.
3229                 */
3230                public LeafNodeDefinition withParam(RuntimeSearchParam theParamDefinition) {
3231                        return new LeafNodeDefinition(
3232                                        theParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers);
3233                }
3234        }
3235
3236        public static class SearchForIdsParams {
3237                DbColumn[] mySourceJoinColumn;
3238                String myResourceName;
3239                String myParamName;
3240                List<List<IQueryParameterType>> myAndOrParams;
3241                RequestDetails myRequest;
3242                RequestPartitionId myRequestPartitionId;
3243                ResourceTablePredicateBuilder myResourceTablePredicateBuilder;
3244
3245                public static SearchForIdsParams with() {
3246                        return new SearchForIdsParams();
3247                }
3248
3249                public DbColumn[] getSourceJoinColumn() {
3250                        return mySourceJoinColumn;
3251                }
3252
3253                public SearchForIdsParams setSourceJoinColumn(DbColumn[] theSourceJoinColumn) {
3254                        mySourceJoinColumn = theSourceJoinColumn;
3255                        return this;
3256                }
3257
3258                public String getResourceName() {
3259                        return myResourceName;
3260                }
3261
3262                public SearchForIdsParams setResourceName(String theResourceName) {
3263                        myResourceName = theResourceName;
3264                        return this;
3265                }
3266
3267                public String getParamName() {
3268                        return myParamName;
3269                }
3270
3271                public SearchForIdsParams setParamName(String theParamName) {
3272                        myParamName = theParamName;
3273                        return this;
3274                }
3275
3276                public List<List<IQueryParameterType>> getAndOrParams() {
3277                        return myAndOrParams;
3278                }
3279
3280                public SearchForIdsParams setAndOrParams(List<List<IQueryParameterType>> theAndOrParams) {
3281                        myAndOrParams = theAndOrParams;
3282                        return this;
3283                }
3284
3285                public RequestDetails getRequest() {
3286                        return myRequest;
3287                }
3288
3289                public SearchForIdsParams setRequest(RequestDetails theRequest) {
3290                        myRequest = theRequest;
3291                        return this;
3292                }
3293
3294                public RequestPartitionId getRequestPartitionId() {
3295                        return myRequestPartitionId;
3296                }
3297
3298                public SearchForIdsParams setRequestPartitionId(RequestPartitionId theRequestPartitionId) {
3299                        myRequestPartitionId = theRequestPartitionId;
3300                        return this;
3301                }
3302
3303                public ResourceTablePredicateBuilder getResourceTablePredicateBuilder() {
3304                        return myResourceTablePredicateBuilder;
3305                }
3306
3307                public SearchForIdsParams setResourceTablePredicateBuilder(
3308                                ResourceTablePredicateBuilder theResourceTablePredicateBuilder) {
3309                        myResourceTablePredicateBuilder = theResourceTablePredicateBuilder;
3310                        return this;
3311                }
3312        }
3313}