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