001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.search.builder.predicate;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
024import ca.uhn.fhir.context.ConfigurationException;
025import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
026import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
027import ca.uhn.fhir.context.RuntimeResourceDefinition;
028import ca.uhn.fhir.context.RuntimeSearchParam;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.interceptor.api.HookParams;
031import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
032import ca.uhn.fhir.interceptor.api.Pointcut;
033import ca.uhn.fhir.interceptor.model.RequestPartitionId;
034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
035import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
036import ca.uhn.fhir.jpa.api.dao.IDao;
037import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
038import ca.uhn.fhir.jpa.dao.BaseStorageDao;
039import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
040import ca.uhn.fhir.jpa.model.dao.JpaPid;
041import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
042import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
043import ca.uhn.fhir.jpa.search.builder.QueryStack;
044import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
045import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
046import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
047import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
048import ca.uhn.fhir.jpa.util.QueryParameterUtils;
049import ca.uhn.fhir.model.api.IQueryParameterType;
050import ca.uhn.fhir.model.primitive.IdDt;
051import ca.uhn.fhir.parser.DataFormatException;
052import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
053import ca.uhn.fhir.rest.api.server.RequestDetails;
054import ca.uhn.fhir.rest.param.ReferenceParam;
055import ca.uhn.fhir.rest.param.TokenParam;
056import ca.uhn.fhir.rest.param.TokenParamModifier;
057import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
058import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
059import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
060import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
061import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
062import com.google.common.annotations.VisibleForTesting;
063import com.google.common.collect.Lists;
064import com.healthmarketscience.sqlbuilder.BinaryCondition;
065import com.healthmarketscience.sqlbuilder.ComboCondition;
066import com.healthmarketscience.sqlbuilder.Condition;
067import com.healthmarketscience.sqlbuilder.NotCondition;
068import com.healthmarketscience.sqlbuilder.SelectQuery;
069import com.healthmarketscience.sqlbuilder.UnaryCondition;
070import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
071import jakarta.annotation.Nonnull;
072import jakarta.annotation.Nullable;
073import org.apache.commons.lang3.StringUtils;
074import org.hl7.fhir.instance.model.api.IBaseResource;
075import org.hl7.fhir.instance.model.api.IIdType;
076import org.slf4j.Logger;
077import org.slf4j.LoggerFactory;
078import org.springframework.beans.factory.annotation.Autowired;
079
080import java.util.ArrayList;
081import java.util.Arrays;
082import java.util.Collection;
083import java.util.Collections;
084import java.util.HashSet;
085import java.util.List;
086import java.util.ListIterator;
087import java.util.Optional;
088import java.util.Set;
089import java.util.regex.Pattern;
090import java.util.stream.Collectors;
091
092import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
093import static ca.uhn.fhir.rest.api.Constants.PARAM_TYPE;
094import static ca.uhn.fhir.rest.api.Constants.VALID_MODIFIERS;
095import static org.apache.commons.lang3.StringUtils.isBlank;
096import static org.apache.commons.lang3.StringUtils.trim;
097
098public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder implements ICanMakeMissingParamPredicate {
099
100        private static final Logger ourLog = LoggerFactory.getLogger(ResourceLinkPredicateBuilder.class);
101        private static final Pattern MODIFIER_REPLACE_PATTERN = Pattern.compile(".*:");
102        private final DbColumn myColumnSrcType;
103        private final DbColumn myColumnSrcPath;
104        private final DbColumn myColumnTargetResourceId;
105        private final DbColumn myColumnTargetResourceUrl;
106        private final DbColumn myColumnSrcResourceId;
107        private final DbColumn myColumnTargetResourceType;
108        private final QueryStack myQueryStack;
109        private final boolean myReversed;
110
111        private final DbColumn myColumnTargetPartitionId;
112        private final DbColumn myColumnSrcPartitionId;
113
114        @Autowired
115        private JpaStorageSettings myStorageSettings;
116
117        @Autowired
118        private IInterceptorBroadcaster myInterceptorBroadcaster;
119
120        @Autowired
121        private ISearchParamRegistry mySearchParamRegistry;
122
123        @Autowired
124        private IIdHelperService myIdHelperService;
125
126        @Autowired
127        private DaoRegistry myDaoRegistry;
128
129        @Autowired
130        private MatchUrlService myMatchUrlService;
131
132        /**
133         * Constructor
134         */
135        public ResourceLinkPredicateBuilder(
136                        QueryStack theQueryStack, SearchQueryBuilder theSearchSqlBuilder, boolean theReversed) {
137                super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_RES_LINK"));
138                myColumnSrcResourceId = getTable().addColumn("SRC_RESOURCE_ID");
139                myColumnSrcPartitionId = getTable().addColumn("PARTITION_ID");
140                myColumnSrcType = getTable().addColumn("SOURCE_RESOURCE_TYPE");
141                myColumnSrcPath = getTable().addColumn("SRC_PATH");
142                myColumnTargetResourceId = getTable().addColumn("TARGET_RESOURCE_ID");
143                myColumnTargetPartitionId = getTable().addColumn("TARGET_RES_PARTITION_ID");
144                myColumnTargetResourceUrl = getTable().addColumn("TARGET_RESOURCE_URL");
145                myColumnTargetResourceType = getTable().addColumn("TARGET_RESOURCE_TYPE");
146
147                myReversed = theReversed;
148                myQueryStack = theQueryStack;
149        }
150
151        private DbColumn getResourceTypeColumn() {
152                if (myReversed) {
153                        return myColumnTargetResourceType;
154                } else {
155                        return myColumnSrcType;
156                }
157        }
158
159        public DbColumn getColumnSourcePath() {
160                return myColumnSrcPath;
161        }
162
163        public DbColumn getColumnTargetResourceId() {
164                return myColumnTargetResourceId;
165        }
166
167        public DbColumn getColumnTargetPartitionId() {
168                return myColumnTargetPartitionId;
169        }
170
171        public DbColumn[] getJoinColumnsForTarget() {
172                return getSearchQueryBuilder().toJoinColumns(getColumnTargetPartitionId(), getColumnTargetResourceId());
173        }
174
175        public DbColumn[] getJoinColumnsForSource() {
176                return getSearchQueryBuilder().toJoinColumns(getPartitionIdColumn(), myColumnSrcResourceId);
177        }
178
179        /**
180         * Note that this may return the SRC_RESOURCE_ID or TGT_RESOURCE_ID depending
181         * on whether we're building a forward or reverse link. If you need a specific
182         * one of these, use {@link #getJoinColumnsForSource()} or {@link #getJoinColumnsForTarget()}.
183         */
184        @Override
185        public DbColumn[] getJoinColumns() {
186                return super.getJoinColumns();
187        }
188
189        public DbColumn getColumnSrcResourceId() {
190                return myColumnSrcResourceId;
191        }
192
193        public DbColumn getColumnSrcPartitionId() {
194                return myColumnSrcPartitionId;
195        }
196
197        public DbColumn getColumnTargetResourceType() {
198                return myColumnTargetResourceType;
199        }
200
201        @Override
202        public DbColumn getResourceIdColumn() {
203                if (myReversed) {
204                        return myColumnTargetResourceId;
205                } else {
206                        return myColumnSrcResourceId;
207                }
208        }
209
210        public Condition createPredicate(
211                        RequestDetails theRequest,
212                        String theResourceType,
213                        String theParamName,
214                        List<String> theQualifiers,
215                        List<? extends IQueryParameterType> theReferenceOrParamList,
216                        SearchFilterParser.CompareOperation theOperation,
217                        RequestPartitionId theRequestPartitionId) {
218
219                List<IIdType> targetIds = new ArrayList<>();
220                List<String> targetQualifiedUrls = new ArrayList<>();
221
222                for (int orIdx = 0; orIdx < theReferenceOrParamList.size(); orIdx++) {
223                        IQueryParameterType nextOr = theReferenceOrParamList.get(orIdx);
224
225                        if (nextOr instanceof ReferenceParam) {
226                                ReferenceParam ref = (ReferenceParam) nextOr;
227
228                                if (isBlank(ref.getChain())) {
229
230                                        /*
231                                         * Handle non-chained search, e.g. Patient?organization=Organization/123
232                                         */
233
234                                        IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
235
236                                        if (dt.hasBaseUrl()) {
237                                                if (myStorageSettings.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
238                                                        dt = dt.toUnqualified();
239                                                        targetIds.add(dt);
240                                                } else {
241                                                        targetQualifiedUrls.add(dt.getValue());
242                                                }
243                                        } else {
244                                                validateModifierUse(theRequest, theResourceType, ref);
245                                                validateResourceTypeInReferenceParam(ref.getResourceType());
246                                                targetIds.add(dt);
247                                        }
248
249                                } else {
250
251                                        /*
252                                         * Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
253                                         */
254
255                                        return addPredicateReferenceWithChain(
256                                                        theResourceType,
257                                                        theParamName,
258                                                        theQualifiers,
259                                                        theReferenceOrParamList,
260                                                        ref,
261                                                        theRequest,
262                                                        theRequestPartitionId);
263                                }
264
265                        } else {
266                                throw new IllegalArgumentException(
267                                                Msg.code(1241) + "Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
268                        }
269                }
270
271                for (IIdType next : targetIds) {
272                        if (!next.hasResourceType()) {
273                                warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, null);
274                        }
275                }
276
277                List<String> pathsToMatch = createResourceLinkPaths(theResourceType, theParamName, theQualifiers);
278                boolean inverse;
279                if ((theOperation == null) || (theOperation == SearchFilterParser.CompareOperation.eq)) {
280                        inverse = false;
281                } else {
282                        inverse = true;
283                }
284
285                List<JpaPid> targetPids =
286                                myIdHelperService.resolveResourcePersistentIdsWithCache(theRequestPartitionId, targetIds);
287                List<Long> targetPidList = JpaPid.toLongList(targetPids);
288
289                if (targetPidList.isEmpty() && targetQualifiedUrls.isEmpty()) {
290                        setMatchNothing();
291                        return null;
292                } else {
293                        Condition retVal = createPredicateReference(inverse, pathsToMatch, targetPidList, targetQualifiedUrls);
294                        return combineWithRequestPartitionIdPredicate(getRequestPartitionId(), retVal);
295                }
296        }
297
298        private void validateModifierUse(RequestDetails theRequest, String theResourceType, ReferenceParam theRef) {
299                try {
300                        final String resourceTypeFromRef = theRef.getResourceType();
301                        if (StringUtils.isEmpty(resourceTypeFromRef)) {
302                                return;
303                        }
304                        // TODO: LD: unless we do this, ResourceProviderR4Test#testSearchWithSlashes will fail due to its
305                        // derived-from: syntax
306                        getFhirContext().getResourceDefinition(resourceTypeFromRef);
307                } catch (DataFormatException e) {
308                        final List<String> nonMatching = Optional.ofNullable(theRequest)
309                                        .map(RequestDetails::getParameters)
310                                        .map(params -> params.keySet().stream()
311                                                        .filter(mod -> mod.contains(":"))
312                                                        .map(MODIFIER_REPLACE_PATTERN::matcher)
313                                                        .map(pattern -> pattern.replaceAll(":"))
314                                                        .filter(mod -> !VALID_MODIFIERS.contains(mod))
315                                                        .distinct()
316                                                        .collect(Collectors.toUnmodifiableList()))
317                                        .orElse(Collections.emptyList());
318
319                        if (!nonMatching.isEmpty()) {
320                                final String msg = getFhirContext()
321                                                .getLocalizer()
322                                                .getMessageSanitized(
323                                                                SearchCoordinatorSvcImpl.class,
324                                                                "invalidUseOfSearchIdentifier",
325                                                                nonMatching,
326                                                                theResourceType,
327                                                                VALID_MODIFIERS);
328                                throw new InvalidRequestException(Msg.code(2498) + msg);
329                        }
330                }
331        }
332
333        private void validateResourceTypeInReferenceParam(final String theResourceType) {
334                if (StringUtils.isEmpty(theResourceType)) {
335                        return;
336                }
337
338                try {
339                        getFhirContext().getResourceDefinition(theResourceType);
340                } catch (DataFormatException e) {
341                        throw newInvalidResourceTypeException(theResourceType);
342                }
343        }
344
345        private Condition createPredicateReference(
346                        boolean theInverse,
347                        List<String> thePathsToMatch,
348                        List<Long> theTargetPidList,
349                        List<String> theTargetQualifiedUrls) {
350
351                Condition targetPidCondition = null;
352                if (!theTargetPidList.isEmpty()) {
353                        List<String> placeholders = generatePlaceholders(theTargetPidList);
354                        targetPidCondition =
355                                        QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceId, placeholders, theInverse);
356                }
357
358                Condition targetUrlsCondition = null;
359                if (!theTargetQualifiedUrls.isEmpty()) {
360                        List<String> placeholders = generatePlaceholders(theTargetQualifiedUrls);
361                        targetUrlsCondition =
362                                        QueryParameterUtils.toEqualToOrInPredicate(myColumnTargetResourceUrl, placeholders, theInverse);
363                }
364
365                Condition joinedCondition;
366                if (targetPidCondition != null && targetUrlsCondition != null) {
367                        joinedCondition = ComboCondition.or(targetPidCondition, targetUrlsCondition);
368                } else if (targetPidCondition != null) {
369                        joinedCondition = targetPidCondition;
370                } else {
371                        joinedCondition = targetUrlsCondition;
372                }
373
374                Condition pathPredicate = createPredicateSourcePaths(thePathsToMatch);
375                joinedCondition = ComboCondition.and(pathPredicate, joinedCondition);
376
377                return joinedCondition;
378        }
379
380        @Nonnull
381        public Condition createPredicateSourcePaths(List<String> thePathsToMatch) {
382                return QueryParameterUtils.toEqualToOrInPredicate(myColumnSrcPath, generatePlaceholders(thePathsToMatch));
383        }
384
385        public Condition createPredicateSourcePaths(String theResourceName, String theParamName) {
386                List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, Collections.emptyList());
387                return createPredicateSourcePaths(pathsToMatch);
388        }
389
390        private void warnAboutPerformanceOnUnqualifiedResources(
391                        String theParamName, RequestDetails theRequest, @Nullable List<String> theCandidateTargetTypes) {
392                StringBuilder builder = new StringBuilder();
393                builder.append("This search uses an unqualified resource(a parameter in a chain without a resource type). ");
394                builder.append("This is less efficient than using a qualified type. ");
395                if (theCandidateTargetTypes != null) {
396                        builder.append("[" + theParamName + "] resolves to ["
397                                        + theCandidateTargetTypes.stream().collect(Collectors.joining(",")) + "].");
398                        builder.append("If you know what you're looking for, try qualifying it using the form ");
399                        builder.append(theCandidateTargetTypes.stream()
400                                        .map(cls -> "[" + cls + ":" + theParamName + "]")
401                                        .collect(Collectors.joining(" or ")));
402                } else {
403                        builder.append("If you know what you're looking for, try qualifying it using the form: '");
404                        builder.append(theParamName).append(":[resourceType]=[id] or ");
405                        builder.append(theParamName).append("=[resourceType]/[id]");
406                        builder.append("'");
407                }
408                String message = builder.toString();
409                StorageProcessingMessage msg = new StorageProcessingMessage().setMessage(message);
410                HookParams params = new HookParams()
411                                .add(RequestDetails.class, theRequest)
412                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
413                                .add(StorageProcessingMessage.class, msg);
414                CompositeInterceptorBroadcaster.doCallHooks(
415                                myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
416        }
417
418        /**
419         * This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain
420         * on the device.
421         */
422        private Condition addPredicateReferenceWithChain(
423                        String theResourceName,
424                        String theParamName,
425                        List<String> theQualifiers,
426                        List<? extends IQueryParameterType> theList,
427                        ReferenceParam theReferenceParam,
428                        RequestDetails theRequest,
429                        RequestPartitionId theRequestPartitionId) {
430
431                /*
432                 * Which resource types can the given chained parameter actually link to? This might be a list
433                 * where the chain is unqualified, as in: Observation?subject.identifier=(...)
434                 * since subject can link to several possible target types.
435                 *
436                 * If the user has qualified the chain, as in: Observation?subject:Patient.identifier=(...)
437                 * this is just a simple 1-entry list.
438                 */
439                final List<String> resourceTypes =
440                                determineCandidateResourceTypesForChain(theResourceName, theParamName, theReferenceParam);
441
442                /*
443                 * Handle chain on _type
444                 */
445                if (PARAM_TYPE.equals(theReferenceParam.getChain())) {
446
447                        List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
448                        Condition typeCondition = createPredicateSourcePaths(pathsToMatch);
449
450                        String typeValue = theReferenceParam.getValue();
451
452                        validateResourceTypeInReferenceParam(typeValue);
453                        if (!resourceTypes.contains(typeValue)) {
454                                throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue);
455                        }
456
457                        Condition condition = BinaryCondition.equalTo(
458                                        myColumnTargetResourceType, generatePlaceholder(theReferenceParam.getValue()));
459
460                        return QueryParameterUtils.toAndPredicate(typeCondition, condition);
461                }
462
463                boolean foundChainMatch = false;
464                List<String> candidateTargetTypes = new ArrayList<>();
465                List<Condition> orPredicates = new ArrayList<>();
466                boolean paramInverted = false;
467                QueryStack childQueryFactory = myQueryStack.newChildQueryFactoryWithFullBuilderReuse();
468
469                String chain = theReferenceParam.getChain();
470
471                String remainingChain = null;
472                int chainDotIndex = chain.indexOf('.');
473                if (chainDotIndex != -1) {
474                        remainingChain = chain.substring(chainDotIndex + 1);
475                        chain = chain.substring(0, chainDotIndex);
476                }
477
478                int qualifierIndex = chain.indexOf(':');
479                String qualifier = null;
480                if (qualifierIndex != -1) {
481                        qualifier = chain.substring(qualifierIndex);
482                        chain = chain.substring(0, qualifierIndex);
483                }
484
485                boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
486
487                for (String nextType : resourceTypes) {
488
489                        RuntimeResourceDefinition typeDef = getFhirContext().getResourceDefinition(nextType);
490                        String subResourceName = typeDef.getName();
491
492                        IDao dao = myDaoRegistry.getResourceDao(nextType);
493                        if (dao == null) {
494                                ourLog.debug("Don't have a DAO for type {}", nextType);
495                                continue;
496                        }
497
498                        RuntimeSearchParam param = null;
499                        if (!isMeta) {
500                                param = mySearchParamRegistry.getActiveSearchParam(
501                                                nextType, chain, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
502                                if (param == null) {
503                                        ourLog.debug("Type {} doesn't have search param {}", nextType, param);
504                                        continue;
505                                }
506                        }
507
508                        ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
509
510                        for (IQueryParameterType next : theList) {
511                                String nextValue = next.getValueAsQueryToken(getFhirContext());
512                                IQueryParameterType chainValue = mapReferenceChainToRawParamType(
513                                                remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue);
514                                if (chainValue == null) {
515                                        continue;
516                                }
517
518                                // For the token param, if it's a :not modifier, need switch OR to AND
519                                if (!paramInverted && chainValue instanceof TokenParam) {
520                                        if (((TokenParam) chainValue).getModifier() == TokenParamModifier.NOT) {
521                                                paramInverted = true;
522                                        }
523                                }
524                                foundChainMatch = true;
525                                orValues.add(chainValue);
526                        }
527
528                        if (!foundChainMatch) {
529                                throw new InvalidRequestException(Msg.code(1242)
530                                                + getFhirContext()
531                                                                .getLocalizer()
532                                                                .getMessage(
533                                                                                BaseStorageDao.class,
534                                                                                "invalidParameterChain",
535                                                                                theParamName + '.' + theReferenceParam.getChain()));
536                        }
537
538                        candidateTargetTypes.add(nextType);
539
540                        List<Condition> andPredicates = new ArrayList<>();
541
542                        List<List<IQueryParameterType>> chainParamValues = Collections.singletonList(orValues);
543                        andPredicates.add(
544                                        childQueryFactory.searchForIdsWithAndOr(with().setSourceJoinColumn(getJoinColumnsForTarget())
545                                                        .setResourceName(subResourceName)
546                                                        .setParamName(chain)
547                                                        .setAndOrParams(chainParamValues)
548                                                        .setRequest(theRequest)
549                                                        .setRequestPartitionId(theRequestPartitionId)));
550
551                        orPredicates.add(QueryParameterUtils.toAndPredicate(andPredicates));
552                }
553
554                if (candidateTargetTypes.isEmpty()) {
555                        throw new InvalidRequestException(Msg.code(1243)
556                                        + getFhirContext()
557                                                        .getLocalizer()
558                                                        .getMessage(
559                                                                        BaseStorageDao.class,
560                                                                        "invalidParameterChain",
561                                                                        theParamName + '.' + theReferenceParam.getChain()));
562                }
563
564                if (candidateTargetTypes.size() > 1) {
565                        warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes);
566                }
567
568                // If :not modifier for a token, switch OR with AND in the multi-type case
569                Condition multiTypePredicate;
570                if (paramInverted) {
571                        multiTypePredicate = QueryParameterUtils.toAndPredicate(orPredicates);
572                } else {
573                        multiTypePredicate = QueryParameterUtils.toOrPredicate(orPredicates);
574                }
575
576                List<String> pathsToMatch = createResourceLinkPaths(theResourceName, theParamName, theQualifiers);
577                Condition pathPredicate = createPredicateSourcePaths(pathsToMatch);
578                return QueryParameterUtils.toAndPredicate(pathPredicate, multiTypePredicate);
579        }
580
581        @Nonnull
582        private List<String> determineCandidateResourceTypesForChain(
583                        String theResourceName, String theParamName, ReferenceParam theReferenceParam) {
584                final List<Class<? extends IBaseResource>> resourceTypes;
585                if (!theReferenceParam.hasResourceType()) {
586
587                        resourceTypes = determineResourceTypes(Collections.singleton(theResourceName), theParamName);
588
589                        if (resourceTypes.isEmpty()) {
590                                RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(
591                                                theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
592                                if (searchParamByName == null) {
593                                        throw new InternalErrorException(Msg.code(1244) + "Could not find parameter " + theParamName);
594                                }
595                                String paramPath = searchParamByName.getPath();
596                                if (paramPath.endsWith(".as(Reference)")) {
597                                        paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
598                                }
599
600                                if (paramPath.contains(".extension(")) {
601                                        int startIdx = paramPath.indexOf(".extension(");
602                                        int endIdx = paramPath.indexOf(')', startIdx);
603                                        if (startIdx != -1 && endIdx != -1) {
604                                                paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
605                                        }
606                                }
607
608                                Class<? extends IBaseResource> resourceType =
609                                                getFhirContext().getResourceDefinition(theResourceName).getImplementingClass();
610                                BaseRuntimeChildDefinition def = getFhirContext().newTerser().getDefinition(resourceType, paramPath);
611                                if (def instanceof RuntimeChildChoiceDefinition) {
612                                        RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
613                                        resourceTypes.addAll(choiceDef.getResourceTypes());
614                                } else if (def instanceof RuntimeChildResourceDefinition) {
615                                        RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
616                                        resourceTypes.addAll(resDef.getResourceTypes());
617                                        if (resourceTypes.size() == 1) {
618                                                if (resourceTypes.get(0).isInterface()) {
619                                                        throw new InvalidRequestException(
620                                                                        Msg.code(1245) + "Unable to perform search for unqualified chain '" + theParamName
621                                                                                        + "' as this SearchParameter does not declare any target types. Add a qualifier of the form '"
622                                                                                        + theParamName + ":[ResourceType]' to perform this search.");
623                                                }
624                                        }
625                                } else {
626                                        throw new ConfigurationException(Msg.code(1246) + "Property " + paramPath + " of type "
627                                                        + getResourceType() + " is not a resource: " + def.getClass());
628                                }
629                        }
630
631                        if (resourceTypes.isEmpty()) {
632                                for (BaseRuntimeElementDefinition<?> next : getFhirContext().getElementDefinitions()) {
633                                        if (next instanceof RuntimeResourceDefinition) {
634                                                RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
635                                                resourceTypes.add(nextResDef.getImplementingClass());
636                                        }
637                                }
638                        }
639
640                } else {
641
642                        try {
643                                RuntimeResourceDefinition resDef =
644                                                getFhirContext().getResourceDefinition(theReferenceParam.getResourceType());
645                                resourceTypes = new ArrayList<>(1);
646                                resourceTypes.add(resDef.getImplementingClass());
647                        } catch (DataFormatException e) {
648                                throw newInvalidResourceTypeException(theReferenceParam.getResourceType());
649                        }
650                }
651
652                return resourceTypes.stream()
653                                .map(t -> getFhirContext().getResourceType(t))
654                                .collect(Collectors.toList());
655        }
656
657        private List<Class<? extends IBaseResource>> determineResourceTypes(
658                        Set<String> theResourceNames, String theParamNameChain) {
659                int linkIndex = theParamNameChain.indexOf('.');
660                if (linkIndex == -1) {
661                        Set<Class<? extends IBaseResource>> resourceTypes = new HashSet<>();
662                        for (String resourceName : theResourceNames) {
663                                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
664                                                resourceName, theParamNameChain, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
665
666                                if (param != null && param.hasTargets()) {
667                                        Set<String> targetTypes = param.getTargets();
668                                        for (String next : targetTypes) {
669                                                resourceTypes.add(
670                                                                getFhirContext().getResourceDefinition(next).getImplementingClass());
671                                        }
672                                }
673                        }
674                        return new ArrayList<>(resourceTypes);
675                } else {
676                        String paramNameHead = theParamNameChain.substring(0, linkIndex);
677                        String paramNameTail = theParamNameChain.substring(linkIndex + 1);
678                        Set<String> targetResourceTypeNames = new HashSet<>();
679                        for (String resourceName : theResourceNames) {
680                                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
681                                                resourceName, paramNameHead, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
682
683                                if (param != null && param.hasTargets()) {
684                                        targetResourceTypeNames.addAll(param.getTargets());
685                                }
686                        }
687                        return determineResourceTypes(targetResourceTypeNames, paramNameTail);
688                }
689        }
690
691        public List<String> createResourceLinkPaths(
692                        String theResourceName, String theParamName, List<String> theParamQualifiers) {
693                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
694                                theResourceName, theParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
695                if (param != null) {
696                        List<String> path = param.getPathsSplit();
697
698                        /*
699                         * SearchParameters can declare paths on multiple resource
700                         * types. Here we only want the ones that actually apply.
701                         */
702                        path = new ArrayList<>(path);
703
704                        ListIterator<String> iter = path.listIterator();
705                        while (iter.hasNext()) {
706                                String nextPath = trim(iter.next());
707                                if (!nextPath.contains(theResourceName + ".")) {
708                                        iter.remove();
709                                }
710                        }
711
712                        return path;
713                }
714
715                boolean containsChain = theParamName.contains(".");
716                if (containsChain) {
717                        int linkIndex = theParamName.indexOf('.');
718                        String paramNameHead = theParamName.substring(0, linkIndex);
719                        String paramNameTail = theParamName.substring(linkIndex + 1);
720                        String qualifier = !theParamQualifiers.isEmpty() ? theParamQualifiers.get(0) : null;
721                        List<String> nextQualifiersList = !theParamQualifiers.isEmpty()
722                                        ? theParamQualifiers.subList(1, theParamQualifiers.size())
723                                        : List.of();
724
725                        param = mySearchParamRegistry.getActiveSearchParam(
726                                        theResourceName, paramNameHead, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
727                        if (param != null) {
728                                Set<String> tailPaths = param.getTargets().stream()
729                                                .filter(t -> isBlank(qualifier) || qualifier.equals(t))
730                                                .map(t -> createResourceLinkPaths(t, paramNameTail, nextQualifiersList))
731                                                .flatMap(Collection::stream)
732                                                .map(t -> t.substring(t.indexOf('.') + 1))
733                                                .collect(Collectors.toSet());
734
735                                List<String> path = param.getPathsSplit();
736
737                                /*
738                                 * SearchParameters can declare paths on multiple resource
739                                 * types. Here we only want the ones that actually apply.
740                                 * Then append all the tail paths to each of the applicable head paths
741                                 */
742                                return path.stream()
743                                                .map(String::trim)
744                                                .filter(t -> t.startsWith(theResourceName + "."))
745                                                .map(head -> tailPaths.stream()
746                                                                .map(tail -> head + "." + tail)
747                                                                .collect(Collectors.toSet()))
748                                                .flatMap(Collection::stream)
749                                                .collect(Collectors.toList());
750                        }
751                }
752
753                // This can happen during recursion, if not all the possible target types of one link in the chain
754                // support the next link
755                return new ArrayList<>();
756        }
757
758        private IQueryParameterType mapReferenceChainToRawParamType(
759                        String remainingChain,
760                        RuntimeSearchParam param,
761                        String theParamName,
762                        String qualifier,
763                        String nextType,
764                        String chain,
765                        boolean isMeta,
766                        String resourceId) {
767                IQueryParameterType chainValue;
768                if (remainingChain != null) {
769                        if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
770                                ourLog.debug(
771                                                "Type {} parameter {} is not a reference, can not chain {}", nextType, chain, remainingChain);
772                                return null;
773                        }
774
775                        chainValue = new ReferenceParam();
776                        chainValue.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId);
777                        ((ReferenceParam) chainValue).setChain(remainingChain);
778                } else if (isMeta) {
779                        IQueryParameterType type = myMatchUrlService.newInstanceType(chain);
780                        type.setValueAsQueryToken(getFhirContext(), theParamName, qualifier, resourceId);
781                        chainValue = type;
782                } else {
783                        chainValue = myQueryStack.newParameterInstance(param, qualifier, resourceId);
784                }
785
786                return chainValue;
787        }
788
789        @Nonnull
790        private InvalidRequestException newInvalidTargetTypeForChainException(
791                        String theResourceName, String theParamName, String theTypeValue) {
792                String searchParamName = theResourceName + ":" + theParamName;
793                String msg = getFhirContext()
794                                .getLocalizer()
795                                .getMessage(
796                                                ResourceLinkPredicateBuilder.class, "invalidTargetTypeForChain", theTypeValue, searchParamName);
797                return new InvalidRequestException(Msg.code(2495) + msg);
798        }
799
800        @Nonnull
801        private InvalidRequestException newInvalidResourceTypeException(String theResourceType) {
802                String msg = getFhirContext()
803                                .getLocalizer()
804                                .getMessageSanitized(SearchCoordinatorSvcImpl.class, "invalidResourceType", theResourceType);
805                throw new InvalidRequestException(Msg.code(1250) + msg);
806        }
807
808        @Nonnull
809        public Condition createEverythingPredicate(
810                        String theResourceName, List<String> theSourceResourceNames, Long... theTargetPids) {
811                Condition condition;
812
813                if (theTargetPids != null && theTargetPids.length >= 1) {
814                        // if resource ids are provided, we'll create the predicate
815                        // with ids in or equal to this value
816                        condition = QueryParameterUtils.toEqualToOrInPredicate(
817                                        myColumnTargetResourceId, generatePlaceholders(Arrays.asList(theTargetPids)));
818                } else {
819                        // ... otherwise we look for resource types
820                        condition = BinaryCondition.equalTo(myColumnTargetResourceType, generatePlaceholder(theResourceName));
821                }
822
823                if (!theSourceResourceNames.isEmpty()) {
824                        // if source resources are provided, add on predicate for _type operation
825                        Condition typeCondition = QueryParameterUtils.toEqualToOrInPredicate(
826                                        myColumnSrcType, generatePlaceholders(theSourceResourceNames));
827                        condition = QueryParameterUtils.toAndPredicate(List.of(condition, typeCondition));
828                }
829
830                return condition;
831        }
832
833        @Override
834        public Condition createPredicateParamMissingValue(MissingQueryParameterPredicateParams theParams) {
835                SelectQuery subquery = new SelectQuery();
836                subquery.addCustomColumns(1);
837                subquery.addFromTable(getTable());
838
839                String resourceType = theParams.getResourceTablePredicateBuilder().getResourceType();
840                RuntimeSearchParam paramDefinition = mySearchParamRegistry.getRuntimeSearchParam(
841                                resourceType, theParams.getParamName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
842                List<String> pathList = paramDefinition.getPathsSplitForResourceType(resourceType);
843
844                Condition subQueryCondition = ComboCondition.and(
845                                BinaryCondition.equalTo(
846                                                getResourceIdColumn(),
847                                                theParams.getResourceTablePredicateBuilder().getResourceIdColumn()),
848                                BinaryCondition.equalTo(getResourceTypeColumn(), generatePlaceholder(resourceType)),
849                                ComboCondition.or(pathList.stream()
850                                                .map(path -> BinaryCondition.equalTo(getColumnSourcePath(), generatePlaceholder(path)))
851                                                .toArray(BinaryCondition[]::new)));
852
853                subquery.addCondition(subQueryCondition);
854
855                Condition unaryCondition = UnaryCondition.exists(subquery);
856                if (theParams.isMissing()) {
857                        unaryCondition = new NotCondition(unaryCondition);
858                }
859
860                return combineWithRequestPartitionIdPredicate(theParams.getRequestPartitionId(), unaryCondition);
861        }
862
863        @VisibleForTesting
864        void setSearchParamRegistryForUnitTest(ISearchParamRegistry theSearchParamRegistry) {
865                mySearchParamRegistry = theSearchParamRegistry;
866        }
867
868        @VisibleForTesting
869        void setIdHelperServiceForUnitTest(IIdHelperService theIdHelperService) {
870                myIdHelperService = theIdHelperService;
871        }
872}