001package ca.uhn.fhir.jpa.dao.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.context.BaseRuntimeChildDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.ConfigurationException;
026import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
027import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
028import ca.uhn.fhir.context.RuntimeResourceDefinition;
029import ca.uhn.fhir.context.RuntimeSearchParam;
030import ca.uhn.fhir.i18n.Msg;
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.LegacySearchBuilder;
041import ca.uhn.fhir.jpa.model.config.PartitionSettings;
042import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity;
043import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
044import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
045import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
046import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
047import ca.uhn.fhir.jpa.model.entity.ResourceLink;
048import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
049import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
050import ca.uhn.fhir.jpa.searchparam.ResourceMetaParams;
051import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
052import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
053import ca.uhn.fhir.model.api.IQueryParameterAnd;
054import ca.uhn.fhir.model.api.IQueryParameterOr;
055import ca.uhn.fhir.model.api.IQueryParameterType;
056import ca.uhn.fhir.model.primitive.IdDt;
057import ca.uhn.fhir.parser.DataFormatException;
058import ca.uhn.fhir.rest.api.Constants;
059import ca.uhn.fhir.rest.api.QualifiedParamList;
060import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
061import ca.uhn.fhir.rest.api.server.RequestDetails;
062import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
063import ca.uhn.fhir.rest.param.CompositeParam;
064import ca.uhn.fhir.rest.param.DateParam;
065import ca.uhn.fhir.rest.param.HasParam;
066import ca.uhn.fhir.rest.param.NumberParam;
067import ca.uhn.fhir.rest.param.QuantityParam;
068import ca.uhn.fhir.rest.param.ReferenceParam;
069import ca.uhn.fhir.rest.param.SpecialParam;
070import ca.uhn.fhir.rest.param.StringParam;
071import ca.uhn.fhir.rest.param.TokenParam;
072import ca.uhn.fhir.rest.param.UriParam;
073import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
074import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
075import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
076import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
077import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
078import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
079import com.google.common.collect.Lists;
080import org.hl7.fhir.instance.model.api.IAnyResource;
081import org.hl7.fhir.instance.model.api.IBaseResource;
082import org.hl7.fhir.instance.model.api.IIdType;
083import org.slf4j.Logger;
084import org.slf4j.LoggerFactory;
085import org.springframework.beans.factory.annotation.Autowired;
086import org.springframework.context.annotation.Scope;
087import org.springframework.stereotype.Component;
088
089import javax.annotation.Nonnull;
090import javax.annotation.Nullable;
091import javax.persistence.criteria.From;
092import javax.persistence.criteria.Join;
093import javax.persistence.criteria.JoinType;
094import javax.persistence.criteria.Predicate;
095import javax.persistence.criteria.Root;
096import javax.persistence.criteria.Subquery;
097import java.util.ArrayList;
098import java.util.Collection;
099import java.util.Collections;
100import java.util.List;
101import java.util.ListIterator;
102import java.util.Set;
103import java.util.stream.Collectors;
104
105import static ca.uhn.fhir.jpa.search.builder.QueryStack.fromOperation;
106import static org.apache.commons.lang3.StringUtils.isBlank;
107import static org.apache.commons.lang3.StringUtils.isNotBlank;
108import static org.apache.commons.lang3.StringUtils.trim;
109
110@Component
111@Scope("prototype")
112public
113class PredicateBuilderReference extends BasePredicateBuilder {
114        private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderReference.class);
115        private final PredicateBuilder myPredicateBuilder;
116        @Autowired
117        IIdHelperService myIdHelperService;
118        @Autowired
119        ISearchParamRegistry mySearchParamRegistry;
120        @Autowired
121        MatchUrlService myMatchUrlService;
122        @Autowired
123        DaoRegistry myDaoRegistry;
124        @Autowired
125        PartitionSettings myPartitionSettings;
126        @Autowired
127        private IInterceptorBroadcaster myInterceptorBroadcaster;
128
129        public PredicateBuilderReference(LegacySearchBuilder theSearchBuilder, PredicateBuilder thePredicateBuilder) {
130                super(theSearchBuilder);
131                myPredicateBuilder = thePredicateBuilder;
132        }
133
134        /**
135         * Add reference predicate to the current search
136         */
137
138        public Predicate addPredicate(String theResourceName,
139                                                                                        String theParamName,
140                                                                                        List<? extends IQueryParameterType> theList,
141                                                                                        SearchFilterParser.CompareOperation operation,
142                                                                                        RequestDetails theRequest,
143                                                                                        RequestPartitionId theRequestPartitionId) {
144
145                // This just to ensure the chain has been split correctly
146                assert theParamName.contains(".") == false;
147
148                if ((operation != null) &&
149                        (operation != SearchFilterParser.CompareOperation.eq) &&
150                        (operation != SearchFilterParser.CompareOperation.ne)) {
151                        throw new InvalidRequestException(Msg.code(1008) + "Invalid operator specified for reference predicate.  Supported operators for reference predicate are \"eq\" and \"ne\".");
152                }
153
154                if (theList.get(0).getMissing() != null) {
155                        addPredicateParamMissingForReference(theResourceName, theParamName, theList.get(0).getMissing(), theRequestPartitionId);
156                        return null;
157                }
158
159                From<?, ResourceLink> join = myQueryStack.createJoin(SearchBuilderJoinEnum.REFERENCE, theParamName);
160
161                List<IIdType> targetIds = new ArrayList<>();
162                List<String> targetQualifiedUrls = new ArrayList<>();
163
164                for (int orIdx = 0; orIdx < theList.size(); orIdx++) {
165                        IQueryParameterType nextOr = theList.get(orIdx);
166
167                        if (nextOr instanceof ReferenceParam) {
168                                ReferenceParam ref = (ReferenceParam) nextOr;
169
170                                if (isBlank(ref.getChain())) {
171
172                                        /*
173                                         * Handle non-chained search, e.g. Patient?organization=Organization/123
174                                         */
175
176                                        IIdType dt = new IdDt(ref.getBaseUrl(), ref.getResourceType(), ref.getIdPart(), null);
177
178                                        if (dt.hasBaseUrl()) {
179                                                if (myDaoConfig.getTreatBaseUrlsAsLocal().contains(dt.getBaseUrl())) {
180                                                        dt = dt.toUnqualified();
181                                                        targetIds.add(dt);
182                                                } else {
183                                                        targetQualifiedUrls.add(dt.getValue());
184                                                }
185                                        } else {
186                                                targetIds.add(dt);
187                                        }
188
189                                } else {
190
191                                        /*
192                                         * Handle chained search, e.g. Patient?organization.name=Kwik-e-mart
193                                         */
194
195                                        return addPredicateReferenceWithChain(theResourceName, theParamName, theList, join, new ArrayList<>(), ref, theRequest, theRequestPartitionId);
196
197                                }
198
199                        } else {
200                                throw new IllegalArgumentException(Msg.code(1009) + "Invalid token type (expecting ReferenceParam): " + nextOr.getClass());
201                        }
202
203                }
204
205                List<Predicate> codePredicates = new ArrayList<>();
206                addPartitionIdPredicate(theRequestPartitionId, join, codePredicates);
207
208                for (IIdType next : targetIds) {
209                        if (!next.hasResourceType()) {
210                                warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, null);
211                        }
212                }
213
214                // Resources by ID
215                List<ResourcePersistentId> targetPids = myIdHelperService.resolveResourcePersistentIdsWithCache(theRequestPartitionId, targetIds);
216                if (!targetPids.isEmpty()) {
217                        ourLog.debug("Searching for resource link with target PIDs: {}", targetPids);
218                        Predicate pathPredicate;
219                        if ((operation == null) || (operation == SearchFilterParser.CompareOperation.eq)) {
220                                pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
221                        } else {
222                                pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join).not();
223                        }
224                        Predicate pidPredicate;
225                        if ((operation == null) || (operation == SearchFilterParser.CompareOperation.eq)) {
226                                if (targetPids.size() == 1) {
227                                        pidPredicate = myCriteriaBuilder.equal(join.get("myTargetResourcePid").as(Long.class), targetPids.get(0).getIdAsLong());
228                                } else {
229                                        pidPredicate = join.get("myTargetResourcePid").in(ResourcePersistentId.toLongList(targetPids));
230                                }
231                        } else {
232                                pidPredicate = join.get("myTargetResourcePid").in(ResourcePersistentId.toLongList(targetPids)).not();
233                        }
234                        codePredicates.add(myCriteriaBuilder.and(pathPredicate, pidPredicate));
235                }
236
237                // Resources by fully qualified URL
238                if (!targetQualifiedUrls.isEmpty()) {
239                        ourLog.debug("Searching for resource link with target URLs: {}", targetQualifiedUrls);
240                        Predicate pathPredicate;
241                        if ((operation == null) || (operation == SearchFilterParser.CompareOperation.eq)) {
242                                pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join);
243                        } else {
244                                pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, join).not();
245                        }
246                        Predicate pidPredicate;
247                        if ((operation == null) || (operation == SearchFilterParser.CompareOperation.eq)) {
248                                pidPredicate = join.get("myTargetResourceUrl").in(targetQualifiedUrls);
249                        } else {
250                                pidPredicate = join.get("myTargetResourceUrl").in(targetQualifiedUrls).not();
251                        }
252                        codePredicates.add(myCriteriaBuilder.and(pathPredicate, pidPredicate));
253                }
254
255                if (codePredicates.size() > 0) {
256                        Predicate predicate = myCriteriaBuilder.or(toArray(codePredicates));
257                        myQueryStack.addPredicateWithImplicitTypeSelection(predicate);
258                        return predicate;
259                } else {
260                        return myQueryStack.addNeverMatchingPredicate();
261                }
262        }
263
264        /**
265         * This is for handling queries like the following: /Observation?device.identifier=urn:system|foo in which we use a chain
266         * on the device.
267         */
268        private Predicate addPredicateReferenceWithChain(String theResourceName, String theParamName, List<? extends IQueryParameterType> theList, From<?, ResourceLink> theJoin, List<Predicate> theCodePredicates, ReferenceParam theReferenceParam, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
269
270                /*
271                 * Which resource types can the given chained parameter actually link to? This might be a list
272                 * where the chain is unqualified, as in: Observation?subject.identifier=(...)
273                 * since subject can link to several possible target types.
274                 *
275                 * If the user has qualified the chain, as in: Observation?subject:Patient.identifier=(...)
276                 * this is just a simple 1-entry list.
277                 */
278                final List<Class<? extends IBaseResource>> resourceTypes = determineCandidateResourceTypesForChain(theResourceName, theParamName, theReferenceParam);
279
280                /*
281                 * Handle chain on _type
282                 */
283                if (Constants.PARAM_TYPE.equals(theReferenceParam.getChain())) {
284                        return createChainPredicateOnType(theResourceName, theParamName, theJoin, theReferenceParam, resourceTypes);
285                }
286
287                boolean foundChainMatch = false;
288                List<Class<? extends IBaseResource>> candidateTargetTypes = new ArrayList<>();
289                for (Class<? extends IBaseResource> nextType : resourceTypes) {
290                        String chain = theReferenceParam.getChain();
291
292                        String remainingChain = null;
293                        int chainDotIndex = chain.indexOf('.');
294                        if (chainDotIndex != -1) {
295                                remainingChain = chain.substring(chainDotIndex + 1);
296                                chain = chain.substring(0, chainDotIndex);
297                        }
298
299                        RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(nextType);
300                        String subResourceName = typeDef.getName();
301
302                        IDao dao = myDaoRegistry.getResourceDao(nextType);
303                        if (dao == null) {
304                                ourLog.debug("Don't have a DAO for type {}", nextType.getSimpleName());
305                                continue;
306                        }
307
308                        int qualifierIndex = chain.indexOf(':');
309                        String qualifier = null;
310                        if (qualifierIndex != -1) {
311                                qualifier = chain.substring(qualifierIndex);
312                                chain = chain.substring(0, qualifierIndex);
313                        }
314
315                        boolean isMeta = ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(chain);
316                        RuntimeSearchParam param = null;
317                        if (!isMeta) {
318                                param = mySearchParamRegistry.getActiveSearchParam(subResourceName, chain);
319                                if (param == null) {
320                                        ourLog.debug("Type {} doesn't have search param {}", subResourceName, param);
321                                        continue;
322                                }
323                        }
324
325                        ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
326
327                        for (IQueryParameterType next : theList) {
328                                String nextValue = next.getValueAsQueryToken(myContext);
329                                IQueryParameterType chainValue = mapReferenceChainToRawParamType(remainingChain, param, theParamName, qualifier, nextType, chain, isMeta, nextValue);
330                                if (chainValue == null) {
331                                        continue;
332                                }
333                                foundChainMatch = true;
334                                orValues.add(chainValue);
335                        }
336
337                        // If this is false, we throw an exception below so no sense doing any further processing
338                        if (foundChainMatch) {
339                                Subquery<Long> subQ = createLinkSubquery(chain, subResourceName, orValues, theRequest, theRequestPartitionId);
340
341                                Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin);
342                                Predicate pidPredicate = theJoin.get("myTargetResourcePid").in(subQ);
343                                Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, pidPredicate);
344                                theCodePredicates.add(andPredicate);
345                                candidateTargetTypes.add(nextType);
346                        }
347                }
348
349                if (!foundChainMatch) {
350                        throw new InvalidRequestException(Msg.code(1010) + myContext.getLocalizer().getMessage(BaseStorageDao.class, "invalidParameterChain", theParamName + '.' + theReferenceParam.getChain()));
351                }
352
353                if (candidateTargetTypes.size() > 1) {
354                        warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, candidateTargetTypes);
355                }
356
357                Predicate predicate = myCriteriaBuilder.or(toArray(theCodePredicates));
358                myQueryStack.addPredicateWithImplicitTypeSelection(predicate);
359                return predicate;
360        }
361
362        private Predicate createChainPredicateOnType(String theResourceName, String theParamName, From<?, ResourceLink> theJoin, ReferenceParam theReferenceParam, List<Class<? extends IBaseResource>> theResourceTypes) {
363                String typeValue = theReferenceParam.getValue();
364
365                Class<? extends IBaseResource> wantedType;
366                try {
367                        wantedType = myContext.getResourceDefinition(typeValue).getImplementingClass();
368                } catch (DataFormatException e) {
369                        throw newInvalidResourceTypeException(typeValue);
370                }
371                if (!theResourceTypes.contains(wantedType)) {
372                        throw newInvalidTargetTypeForChainException(theResourceName, theParamName, typeValue);
373                }
374
375                Predicate pathPredicate = createResourceLinkPathPredicate(theResourceName, theParamName, theJoin);
376                Predicate sourceTypeParameter = myCriteriaBuilder.equal(theJoin.get("mySourceResourceType"), myResourceName);
377                Predicate targetTypeParameter = myCriteriaBuilder.equal(theJoin.get("myTargetResourceType"), typeValue);
378
379                Predicate composite = myCriteriaBuilder.and(pathPredicate, sourceTypeParameter, targetTypeParameter);
380                myQueryStack.addPredicate(composite);
381                return composite;
382        }
383
384        @Nonnull
385        private List<Class<? extends IBaseResource>> determineCandidateResourceTypesForChain(String theResourceName, String theParamName, ReferenceParam theReferenceParam) {
386                final List<Class<? extends IBaseResource>> resourceTypes;
387                if (!theReferenceParam.hasResourceType()) {
388
389                        RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
390                        resourceTypes = new ArrayList<>();
391
392                        if (param.hasTargets()) {
393                                Set<String> targetTypes = param.getTargets();
394                                for (String next : targetTypes) {
395                                        resourceTypes.add(myContext.getResourceDefinition(next).getImplementingClass());
396                                }
397                        }
398
399                        if (resourceTypes.isEmpty()) {
400                                RuntimeSearchParam searchParamByName = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
401                                if (searchParamByName == null) {
402                                        throw new InternalErrorException(Msg.code(1011) + "Could not find parameter " + theParamName);
403                                }
404                                String paramPath = searchParamByName.getPath();
405                                if (paramPath.endsWith(".as(Reference)")) {
406                                        paramPath = paramPath.substring(0, paramPath.length() - ".as(Reference)".length()) + "Reference";
407                                }
408
409                                if (paramPath.contains(".extension(")) {
410                                        int startIdx = paramPath.indexOf(".extension(");
411                                        int endIdx = paramPath.indexOf(')', startIdx);
412                                        if (startIdx != -1 && endIdx != -1) {
413                                                paramPath = paramPath.substring(0, startIdx + 10) + paramPath.substring(endIdx + 1);
414                                        }
415                                }
416
417                                BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, paramPath);
418                                if (def instanceof RuntimeChildChoiceDefinition) {
419                                        RuntimeChildChoiceDefinition choiceDef = (RuntimeChildChoiceDefinition) def;
420                                        resourceTypes.addAll(choiceDef.getResourceTypes());
421                                } else if (def instanceof RuntimeChildResourceDefinition) {
422                                        RuntimeChildResourceDefinition resDef = (RuntimeChildResourceDefinition) def;
423                                        resourceTypes.addAll(resDef.getResourceTypes());
424                                        if (resourceTypes.size() == 1) {
425                                                if (resourceTypes.get(0).isInterface()) {
426                                                        throw new InvalidRequestException(Msg.code(1012) + "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.");
427                                                }
428                                        }
429                                } else {
430                                        throw new ConfigurationException(Msg.code(1013) + "Property " + paramPath + " of type " + myResourceName + " is not a resource: " + def.getClass());
431                                }
432                        }
433
434                        if (resourceTypes.isEmpty()) {
435                                for (BaseRuntimeElementDefinition<?> next : myContext.getElementDefinitions()) {
436                                        if (next instanceof RuntimeResourceDefinition) {
437                                                RuntimeResourceDefinition nextResDef = (RuntimeResourceDefinition) next;
438                                                resourceTypes.add(nextResDef.getImplementingClass());
439                                        }
440                                }
441                        }
442
443                } else {
444
445                        try {
446                                RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theReferenceParam.getResourceType());
447                                resourceTypes = new ArrayList<>(1);
448                                resourceTypes.add(resDef.getImplementingClass());
449                        } catch (DataFormatException e) {
450                                throw newInvalidResourceTypeException(theReferenceParam.getResourceType());
451                        }
452
453                }
454                return resourceTypes;
455        }
456
457        private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, @Nullable List<Class<? extends IBaseResource>> theCandidateTargetTypes) {
458                StringBuilder builder = new StringBuilder();
459                builder.append("This search uses an unqualified resource(a parameter in a chain without a resource type). ");
460                builder.append("This is less efficient than using a qualified type. ");
461                if (theCandidateTargetTypes != null) {
462                        builder.append("[" + theParamName + "] resolves to [" + theCandidateTargetTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(",")) + "].");
463                        builder.append("If you know what you're looking for, try qualifying it using the form ");
464                        builder.append(theCandidateTargetTypes.stream().map(cls -> "[" + cls.getSimpleName() + ":" + theParamName + "]").collect(Collectors.joining(" or ")));
465                } else {
466                        builder.append("If you know what you're looking for, try qualifying it using the form: '");
467                        builder.append(theParamName).append(":[resourceType]");
468                        builder.append("'");
469                }
470                String message = builder
471                        .toString();
472                StorageProcessingMessage msg = new StorageProcessingMessage()
473                        .setMessage(message);
474                HookParams params = new HookParams()
475                        .add(RequestDetails.class, theRequest)
476                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
477                        .add(StorageProcessingMessage.class, msg);
478                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
479        }
480
481        Predicate createResourceLinkPathPredicate(String theResourceName, String theParamName, From<?, ? extends ResourceLink> from) {
482                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
483                List<String> path = param.getPathsSplit();
484
485                /*
486                 * SearchParameters can declare paths on multiple resource
487                 * types. Here we only want the ones that actually apply.
488                 */
489                path = new ArrayList<>(path);
490
491                ListIterator<String> iter = path.listIterator();
492                while (iter.hasNext()) {
493                        String nextPath = trim(iter.next());
494                        if (!nextPath.contains(theResourceName + ".")) {
495                                iter.remove();
496                        }
497                }
498
499                // multiple values
500                return from.get("mySourcePath").in(path);
501        }
502
503        private IQueryParameterType mapReferenceChainToRawParamType(String remainingChain, RuntimeSearchParam param, String theParamName, String qualifier, Class<? extends IBaseResource> nextType, String chain, boolean isMeta, String resourceId) {
504                IQueryParameterType chainValue;
505                if (remainingChain != null) {
506                        if (param == null || param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
507                                ourLog.debug("Type {} parameter {} is not a reference, can not chain {}", nextType.getSimpleName(), chain, remainingChain);
508                                return null;
509                        }
510
511                        chainValue = new ReferenceParam();
512                        chainValue.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
513                        ((ReferenceParam) chainValue).setChain(remainingChain);
514                } else if (isMeta) {
515                        IQueryParameterType type = myMatchUrlService.newInstanceType(chain);
516                        type.setValueAsQueryToken(myContext, theParamName, qualifier, resourceId);
517                        chainValue = type;
518                } else {
519                        chainValue = toParameterType(param, qualifier, resourceId);
520                }
521
522                return chainValue;
523        }
524
525        Subquery<Long> createLinkSubquery(String theChain, String theSubResourceName, List<IQueryParameterType> theOrValues, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
526
527                /*
528                 * We're doing a chain call, so push the current query root
529                 * and predicate list down and put new ones at the top of the
530                 * stack and run a subquery
531                 */
532                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theSubResourceName, theChain);
533                if (nextParamDef != null && !theChain.startsWith("_")) {
534                        myQueryStack.pushIndexTableSubQuery();
535                } else {
536                        myQueryStack.pushResourceTableSubQuery(theSubResourceName);
537                }
538
539                List<List<IQueryParameterType>> andOrParams = new ArrayList<>();
540                andOrParams.add(theOrValues);
541
542                searchForIdsWithAndOr(theSubResourceName, theChain, andOrParams, theRequest, theRequestPartitionId);
543
544                /*
545                 * Pop the old query root and predicate list back
546                 */
547                return (Subquery<Long>) myQueryStack.pop();
548
549        }
550
551        void searchForIdsWithAndOr(String theResourceName, String theParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
552
553                if (theAndOrParams.isEmpty()) {
554                        return;
555                }
556
557                switch (theParamName) {
558                        case IAnyResource.SP_RES_ID:
559                                myPredicateBuilder.addPredicateResourceId(theAndOrParams, theResourceName, theRequestPartitionId);
560                                break;
561
562                        case Constants.PARAM_HAS:
563                                addPredicateHas(theResourceName, theAndOrParams, theRequest, theRequestPartitionId);
564                                break;
565
566                        case Constants.PARAM_TAG:
567                        case Constants.PARAM_PROFILE:
568                        case Constants.PARAM_SECURITY:
569                                myPredicateBuilder.addPredicateTag(theAndOrParams, theParamName, theRequestPartitionId);
570                                break;
571
572                        case Constants.PARAM_SOURCE:
573                                addPredicateSource(theAndOrParams, theRequest);
574                                break;
575
576                        default:
577
578                                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
579                                if (nextParamDef != null) {
580
581                                        if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) {
582                                                if (theRequestPartitionId.isAllPartitions()) {
583                                                        throw new PreconditionFailedException(Msg.code(1014) + "This server is not configured to support search against all partitions");
584                                                }
585                                        }
586
587                                        switch (nextParamDef.getParamType()) {
588                                                case DATE:
589                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
590                                                                // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt' 
591                                                                // to create the predicateDate instead of generic one with operation = null 
592                                                                SearchFilterParser.CompareOperation operation = null;
593                                                                if (nextAnd.size() > 0) {
594                                                                        DateParam param = (DateParam) nextAnd.get(0);
595                                                                        operation = ca.uhn.fhir.jpa.search.builder.QueryStack.toOperation(param.getPrefix());
596                                                                }
597                                                                myPredicateBuilder.addPredicateDate(theResourceName, nextParamDef, nextAnd, operation, theRequestPartitionId);
598                                                        }
599                                                        break;
600                                                case QUANTITY:
601                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
602                                                                myPredicateBuilder.addPredicateQuantity(theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId);
603                                                        }
604                                                        break;
605                                                case REFERENCE:
606                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
607                                                                addPredicate(theResourceName, theParamName, nextAnd, null, theRequest, theRequestPartitionId);
608                                                        }
609                                                        break;
610                                                case STRING:
611                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
612                                                                myPredicateBuilder.addPredicateString(theResourceName, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.sw, theRequestPartitionId);
613                                                        }
614                                                        break;
615                                                case TOKEN:
616                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
617                                                                if ("Location.position".equals(nextParamDef.getPath())) {
618                                                                        myPredicateBuilder.addPredicateCoords(theResourceName, nextParamDef, nextAnd, theRequestPartitionId);
619                                                                } else {
620                                                                        myPredicateBuilder.addPredicateToken(theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId);
621                                                                }
622                                                        }
623                                                        break;
624                                                case NUMBER:
625                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
626                                                                myPredicateBuilder.addPredicateNumber(theResourceName, nextParamDef, nextAnd, null, theRequestPartitionId);
627                                                        }
628                                                        break;
629                                                case COMPOSITE:
630                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
631                                                                addPredicateComposite(theResourceName, nextParamDef, nextAnd, theRequestPartitionId);
632                                                        }
633                                                        break;
634                                                case URI:
635                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
636                                                                myPredicateBuilder.addPredicateUri(theResourceName, nextParamDef, nextAnd, SearchFilterParser.CompareOperation.eq, theRequestPartitionId);
637                                                        }
638                                                        break;
639                                                case HAS:
640                                                case SPECIAL:
641                                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
642                                                                if ("Location.position".equals(nextParamDef.getPath())) {
643                                                                        myPredicateBuilder.addPredicateCoords(theResourceName, nextParamDef, nextAnd, theRequestPartitionId);
644                                                                }
645                                                        }
646                                                        break;
647                                        }
648                                } else {
649                                        if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
650                                                // These are handled later
651                                        } else if (Constants.PARAM_FILTER.equals(theParamName)) {
652                                                // Parse the predicates enumerated in the _filter separated by AND or OR...
653                                                if (theAndOrParams.get(0).get(0) instanceof StringParam) {
654                                                        String filterString = ((StringParam) theAndOrParams.get(0).get(0)).getValue();
655                                                        SearchFilterParser.Filter filter;
656                                                        try {
657                                                                filter = SearchFilterParser.parse(filterString);
658                                                        } catch (SearchFilterParser.FilterSyntaxException theE) {
659                                                                throw new InvalidRequestException(Msg.code(1015) + "Error parsing _filter syntax: " + theE.getMessage());
660                                                        }
661                                                        if (filter != null) {
662
663                                                                if (!myDaoConfig.isFilterParameterEnabled()) {
664                                                                        throw new InvalidRequestException(Msg.code(1016) + Constants.PARAM_FILTER + " parameter is disabled on this server");
665                                                                }
666
667                                                                // TODO: we clear the predicates below because the filter builds up
668                                                                // its own collection of predicates. It'd probably be good at some
669                                                                // point to do something more fancy...
670                                                                ArrayList<Predicate> holdPredicates = new ArrayList<>(myQueryStack.getPredicates());
671
672                                                                Predicate filterPredicate = processFilter(filter, theResourceName, theRequest, theRequestPartitionId);
673                                                                myQueryStack.clearPredicates();
674                                                                myQueryStack.addPredicates(holdPredicates);
675                                                                myQueryStack.addPredicate(filterPredicate);
676
677                                                                // Because filters can have an OR at the root, we never know for sure that we haven't done an optimized
678                                                                // search that doesn't check the resource type. This could be improved in the future, but for now it's
679                                                                // safest to just clear this flag. The test "testRetrieveDifferentTypeEq" will fail if we don't clear
680                                                                // this here.
681                                                                myQueryStack.clearHasImplicitTypeSelection();
682                                                        }
683                                                }
684
685                                        } else {
686                                                Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName);
687                                                String msg = myContext.getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidSearchParameter", theParamName, theResourceName, validNames);
688                                                throw new InvalidRequestException(Msg.code(1017) + msg);
689                                        }
690                                }
691                                break;
692                }
693        }
694
695        private Predicate processFilter(SearchFilterParser.Filter theFilter, String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
696
697                if (theFilter instanceof SearchFilterParser.FilterParameter) {
698                        return processFilterParameter((SearchFilterParser.FilterParameter) theFilter, theResourceName, theRequest, theRequestPartitionId);
699                } else if (theFilter instanceof SearchFilterParser.FilterLogical) {
700                        // Left side
701                        Predicate xPredicate = processFilter(((SearchFilterParser.FilterLogical) theFilter).getFilter1(), theResourceName, theRequest, theRequestPartitionId);
702
703                        // Right side
704                        Predicate yPredicate = processFilter(((SearchFilterParser.FilterLogical) theFilter).getFilter2(), theResourceName, theRequest, theRequestPartitionId);
705
706                        if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.and) {
707                                return myCriteriaBuilder.and(xPredicate, yPredicate);
708                        } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation() == SearchFilterParser.FilterLogicalOperation.or) {
709                                return myCriteriaBuilder.or(xPredicate, yPredicate);
710                        }
711                } else if (theFilter instanceof SearchFilterParser.FilterParameterGroup) {
712                        return processFilter(((SearchFilterParser.FilterParameterGroup) theFilter).getContained(), theResourceName, theRequest, theRequestPartitionId);
713                }
714                return null;
715        }
716
717        private Predicate processFilterParameter(SearchFilterParser.FilterParameter theFilter,
718                                                                                                                  String theResourceName, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
719
720                if (theFilter.getParamPath().getName().equals(Constants.PARAM_SOURCE)) {
721                        TokenParam param = new TokenParam();
722                        param.setValueAsQueryToken(null, null, null, theFilter.getValue());
723                        return addPredicateSource(Collections.singletonList(param), theFilter.getOperation(), theRequest);
724                } else if (theFilter.getParamPath().getName().equals(IAnyResource.SP_RES_ID)) {
725                        TokenParam param = new TokenParam();
726                        param.setValueAsQueryToken(null,
727                                null,
728                                null,
729                                theFilter.getValue());
730                        return myPredicateBuilder.addPredicateResourceId(Collections.singletonList(Collections.singletonList(param)), myResourceName, theFilter.getOperation(), theRequestPartitionId);
731                }
732
733                RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, theFilter.getParamPath().getName());
734
735                if (searchParam == null) {
736                        throw new InvalidRequestException(Msg.code(1018) + "Invalid search parameter specified, " + theFilter.getParamPath().getName() + ", for resource type " + theResourceName);
737                }
738
739                RestSearchParameterTypeEnum typeEnum = searchParam.getParamType();
740                if (typeEnum == RestSearchParameterTypeEnum.URI) {
741                        return myPredicateBuilder.addPredicateUri(theResourceName, searchParam, Collections.singletonList(new UriParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
742                } else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
743                        return myPredicateBuilder.addPredicateString(theResourceName, searchParam, Collections.singletonList(new StringParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
744                } else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
745                        return myPredicateBuilder.addPredicateDate(theResourceName, searchParam, Collections.singletonList(new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
746                } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
747                        return myPredicateBuilder.addPredicateNumber(theResourceName, searchParam, Collections.singletonList(new NumberParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
748                } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
749                        String paramName = theFilter.getParamPath().getName();
750                        SearchFilterParser.CompareOperation operation = theFilter.getOperation();
751                        String resourceType = null; // The value can either have (Patient/123) or not have (123) a resource type, either way it's not needed here
752                        String chain = (theFilter.getParamPath().getNext() != null) ? theFilter.getParamPath().getNext().toString() : null;
753                        String value = theFilter.getValue();
754                        ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
755                        return addPredicate(theResourceName, paramName, Collections.singletonList(referenceParam), operation, theRequest, theRequestPartitionId);
756                } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
757                        return myPredicateBuilder.addPredicateQuantity(theResourceName, searchParam, Collections.singletonList(new QuantityParam(theFilter.getValue())), theFilter.getOperation(), theRequestPartitionId);
758                } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
759                        throw new InvalidRequestException(Msg.code(1019) + "Composite search parameters not currently supported with _filter clauses");
760                } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) {
761                        TokenParam param = new TokenParam();
762                        param.setValueAsQueryToken(null,
763                                null,
764                                null,
765                                theFilter.getValue());
766                        return myPredicateBuilder.addPredicateToken(theResourceName, searchParam, Collections.singletonList(param), theFilter.getOperation(), theRequestPartitionId);
767                }
768
769                return null;
770        }
771
772        private IQueryParameterType toParameterType(RuntimeSearchParam theParam) {
773                IQueryParameterType qp;
774                switch (theParam.getParamType()) {
775                        case DATE:
776                                qp = new DateParam();
777                                break;
778                        case NUMBER:
779                                qp = new NumberParam();
780                                break;
781                        case QUANTITY:
782                                qp = new QuantityParam();
783                                break;
784                        case STRING:
785                                qp = new StringParam();
786                                break;
787                        case TOKEN:
788                                qp = new TokenParam();
789                                break;
790                        case COMPOSITE:
791                                List<RuntimeSearchParam> compositeOf = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam);
792                                if (compositeOf.size() != 2) {
793                                        throw new InternalErrorException(Msg.code(1020) + "Parameter " + theParam.getName() + " has " + compositeOf.size() + " composite parts. Don't know how handlt this.");
794                                }
795                                IQueryParameterType leftParam = toParameterType(compositeOf.get(0));
796                                IQueryParameterType rightParam = toParameterType(compositeOf.get(1));
797                                qp = new CompositeParam<>(leftParam, rightParam);
798                                break;
799                        case REFERENCE:
800                                qp = new ReferenceParam();
801                                break;
802                        case SPECIAL:
803                                if ("Location.position".equals(theParam.getPath())) {
804                                        qp = new SpecialParam();
805                                        break;
806                                }
807                                throw new InternalErrorException(Msg.code(1021) + "Don't know how to convert param type: " + theParam.getParamType());
808                        case URI:
809                        case HAS:
810                        default:
811                                throw new InternalErrorException(Msg.code(1022) + "Don't know how to convert param type: " + theParam.getParamType());
812                }
813                return qp;
814        }
815
816        private IQueryParameterType toParameterType(RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) {
817                IQueryParameterType qp = toParameterType(theParam);
818
819                qp.setValueAsQueryToken(myContext, theParam.getName(), theQualifier, theValueAsQueryToken);
820                return qp;
821        }
822
823        private void addPredicateSource(List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest) {
824                for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
825                        addPredicateSource(nextAnd, SearchFilterParser.CompareOperation.eq, theRequest);
826                }
827        }
828
829        private Predicate addPredicateSource(List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest) {
830                // Required for now
831                assert theOperation == SearchFilterParser.CompareOperation.eq;
832
833                if (myDaoConfig.getStoreMetaSourceInformation() == DaoConfig.StoreMetaSourceInformationEnum.NONE) {
834                        String msg = myContext.getLocalizer().getMessage(LegacySearchBuilder.class, "sourceParamDisabled");
835                        throw new InvalidRequestException(Msg.code(1023) + msg);
836                }
837
838                From<?, ResourceHistoryProvenanceEntity> join = myQueryStack.createJoin(SearchBuilderJoinEnum.PROVENANCE, Constants.PARAM_SOURCE);
839
840                List<Predicate> codePredicates = new ArrayList<>();
841
842                for (IQueryParameterType nextParameter : theList) {
843                        SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myContext));
844                        String sourceUri = sourceParameter.getSourceUri();
845                        String requestId = sourceParameter.getRequestId();
846                        Predicate sourceUriPredicate = myCriteriaBuilder.equal(join.get("mySourceUri"), sourceUri);
847                        Predicate requestIdPredicate = myCriteriaBuilder.equal(join.get("myRequestId"), requestId);
848                        if (isNotBlank(sourceUri) && isNotBlank(requestId)) {
849                                codePredicates.add(myCriteriaBuilder.and(sourceUriPredicate, requestIdPredicate));
850                        } else if (isNotBlank(sourceUri)) {
851                                codePredicates.add(sourceUriPredicate);
852                        } else if (isNotBlank(requestId)) {
853                                codePredicates.add(requestIdPredicate);
854                        }
855                }
856
857                Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates));
858                myQueryStack.addPredicate(retVal);
859                return retVal;
860        }
861
862        private void addPredicateHas(String theResourceType, List<List<IQueryParameterType>> theHasParameters, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
863
864                for (List<? extends IQueryParameterType> nextOrList : theHasParameters) {
865
866                        String targetResourceType = null;
867                        String paramReference = null;
868                        String parameterName = null;
869
870                        String paramName = null;
871                        List<QualifiedParamList> parameters = new ArrayList<>();
872                        for (IQueryParameterType nextParam : nextOrList) {
873                                HasParam next = (HasParam) nextParam;
874                                targetResourceType = next.getTargetResourceType();
875                                paramReference = next.getReferenceFieldName();
876                                parameterName = next.getParameterName();
877                                paramName = parameterName.replaceAll("\\..*", "");
878                                parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myContext)));
879                        }
880
881                        if (paramName == null) {
882                                continue;
883                        }
884
885                        RuntimeResourceDefinition targetResourceDefinition;
886                        try {
887                                targetResourceDefinition = myContext.getResourceDefinition(targetResourceType);
888                        } catch (DataFormatException e) {
889                                throw new InvalidRequestException(Msg.code(1024) + "Invalid resource type: " + targetResourceType);
890                        }
891
892                        ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
893
894                        if (paramName.startsWith("_has:")) {
895
896                                ourLog.trace("Handing double _has query: {}", paramName);
897
898                                String qualifier = paramName.substring(4);
899                                paramName = Constants.PARAM_HAS;
900                                for (IQueryParameterType next : nextOrList) {
901                                        HasParam nextHasParam = new HasParam();
902                                        nextHasParam.setValueAsQueryToken(myContext, Constants.PARAM_HAS, qualifier, next.getValueAsQueryToken(myContext));
903                                        orValues.add(nextHasParam);
904                                }
905
906                        } else {
907
908                                //Ensure that the name of the search param
909                                // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val)
910                                // exists on the target resource type.
911                                RuntimeSearchParam owningParameterDef = mySearchParamRegistry.getActiveSearchParam(targetResourceType, paramName);
912                                if (owningParameterDef == null) {
913                                        throw new InvalidRequestException(Msg.code(1025) + "Unknown parameter name: " + targetResourceType + ':' + parameterName);
914                                }
915
916                                //Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in Patient?_has:Observation:subject:code=sys|val)
917                                //exists on the target resource.
918                                RuntimeSearchParam joiningParameterDef = mySearchParamRegistry.getActiveSearchParam(targetResourceType, paramReference);
919                                if (joiningParameterDef == null) {
920                                        throw new InvalidRequestException(Msg.code(1026) + "Unknown parameter name: " + targetResourceType + ':' + paramReference);
921                                }
922
923                                IQueryParameterAnd<IQueryParameterOr<IQueryParameterType>> parsedParam = (IQueryParameterAnd<IQueryParameterOr<IQueryParameterType>>) JpaParamUtil.parseQueryParams(mySearchParamRegistry, myContext, owningParameterDef, paramName, parameters);
924
925                                for (IQueryParameterOr<IQueryParameterType> next : parsedParam.getValuesAsQueryTokens()) {
926                                        orValues.addAll(next.getValuesAsQueryTokens());
927                                }
928
929                        }
930
931                        //Handle internal chain inside the has.
932                        if (parameterName.contains(".")) {
933                                String chainedPartOfParameter = getChainedPart(parameterName);
934                                orValues.stream()
935                                        .filter(qp -> qp instanceof ReferenceParam)
936                                        .map(qp -> (ReferenceParam) qp)
937                                        .forEach(rp -> rp.setChain(getChainedPart(chainedPartOfParameter)));
938                        }
939
940                        Subquery<Long> subQ = myPredicateBuilder.createLinkSubquery(paramName, targetResourceType, orValues, theRequest, theRequestPartitionId);
941                        Join<?, ResourceLink> join = (Join) myQueryStack.createJoin(SearchBuilderJoinEnum.HAS, "_has");
942
943                        Predicate pathPredicate = myPredicateBuilder.createResourceLinkPathPredicate(targetResourceType, paramReference, join);
944                        Predicate sourceTypePredicate = myCriteriaBuilder.equal(join.get("myTargetResourceType"), theResourceType);
945                        Predicate sourcePidPredicate = join.get("mySourceResourcePid").in(subQ);
946                        Predicate andPredicate = myCriteriaBuilder.and(pathPredicate, sourcePidPredicate, sourceTypePredicate);
947                        myQueryStack.addPredicate(andPredicate);
948                }
949        }
950
951        private String getChainedPart(String parameter) {
952                return parameter.substring(parameter.indexOf(".") + 1);
953        }
954
955        private void addPredicateComposite(String theResourceName, RuntimeSearchParam theParamDef, List<? extends IQueryParameterType> theNextAnd, RequestPartitionId theRequestPartitionId) {
956                // TODO: fail if missing is set for a composite query
957
958                IQueryParameterType or = theNextAnd.get(0);
959                if (!(or instanceof CompositeParam<?, ?>)) {
960                        throw new InvalidRequestException(Msg.code(1027) + "Invalid type for composite param (must be " + CompositeParam.class.getSimpleName() + ": " + or.getClass());
961                }
962                CompositeParam<?, ?> cp = (CompositeParam<?, ?>) or;
963
964                List<RuntimeSearchParam> componentParams = JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef);
965                RuntimeSearchParam left = componentParams.get(0);
966                IQueryParameterType leftValue = cp.getLeftValue();
967                myQueryStack.addPredicate(createCompositeParamPart(theResourceName, myQueryStack.getRootForComposite(), left, leftValue, theRequestPartitionId));
968
969                RuntimeSearchParam right = componentParams.get(1);
970                IQueryParameterType rightValue = cp.getRightValue();
971                myQueryStack.addPredicate(createCompositeParamPart(theResourceName, myQueryStack.getRootForComposite(), right, rightValue, theRequestPartitionId));
972
973        }
974
975        private Predicate createCompositeParamPart(String theResourceName, Root<?> theRoot, RuntimeSearchParam theParam, IQueryParameterType leftValue, RequestPartitionId theRequestPartitionId) {
976                Predicate retVal = null;
977                switch (theParam.getParamType()) {
978                        case STRING: {
979                                From<ResourceIndexedSearchParamString, ResourceIndexedSearchParamString> stringJoin = theRoot.join("myParamsString", JoinType.INNER);
980                                retVal = myPredicateBuilder.createPredicateString(leftValue, theResourceName, theParam, myCriteriaBuilder, stringJoin, theRequestPartitionId);
981                                break;
982                        }
983                        case TOKEN: {
984                                From<ResourceIndexedSearchParamToken, ResourceIndexedSearchParamToken> tokenJoin = theRoot.join("myParamsToken", JoinType.INNER);
985                                List<IQueryParameterType> tokens = Collections.singletonList(leftValue);
986                                Collection<Predicate> tokenPredicates = myPredicateBuilder.createPredicateToken(tokens, theResourceName, theParam, myCriteriaBuilder, tokenJoin, theRequestPartitionId);
987                                retVal = myCriteriaBuilder.and(tokenPredicates.toArray(new Predicate[0]));
988                                break;
989                        }
990                        case DATE: {
991                                From<ResourceIndexedSearchParamDate, ResourceIndexedSearchParamDate> dateJoin = theRoot.join("myParamsDate", JoinType.INNER);
992                                retVal = myPredicateBuilder.createPredicateDate(leftValue, theResourceName, theParam.getName(), myCriteriaBuilder, dateJoin, theRequestPartitionId);
993                                break;
994                        }
995                        case QUANTITY: {
996                                From<ResourceIndexedSearchParamQuantity, ResourceIndexedSearchParamQuantity> dateJoin = theRoot.join("myParamsQuantity", JoinType.INNER);
997                                retVal = myPredicateBuilder.createPredicateQuantity(leftValue, theResourceName, theParam.getName(), myCriteriaBuilder, dateJoin, theRequestPartitionId);
998                                break;
999                        }
1000                        case COMPOSITE:
1001                        case HAS:
1002                        case NUMBER:
1003                        case REFERENCE:
1004                        case URI:
1005                        case SPECIAL:
1006                                break;
1007                }
1008
1009                if (retVal == null) {
1010                        throw new InvalidRequestException(Msg.code(1028) + "Don't know how to handle composite parameter with type of " + theParam.getParamType());
1011                }
1012
1013                return retVal;
1014        }
1015
1016        @Nonnull
1017        private InvalidRequestException newInvalidTargetTypeForChainException(String theResourceName, String theParamName, String theTypeValue) {
1018                String searchParamName = theResourceName + ":" + theParamName;
1019                String msg = myContext.getLocalizer().getMessage(PredicateBuilderReference.class, "invalidTargetTypeForChain", theTypeValue, searchParamName);
1020                return new InvalidRequestException(msg);
1021        }
1022
1023        @Nonnull
1024        private InvalidRequestException newInvalidResourceTypeException(String theResourceType) {
1025                String msg = myContext.getLocalizer().getMessageSanitized(PredicateBuilderReference.class, "invalidResourceType", theResourceType);
1026                throw new InvalidRequestException(Msg.code(1029) + msg);
1027        }
1028
1029}