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