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