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