001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.searchparam.extractor;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.FhirVersionEnum;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.interceptor.api.HookParams;
028import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
029import ca.uhn.fhir.interceptor.api.Pointcut;
030import ca.uhn.fhir.interceptor.model.RequestPartitionId;
031import ca.uhn.fhir.jpa.model.config.PartitionSettings;
032import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
033import ca.uhn.fhir.jpa.model.dao.JpaPid;
034import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
035import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
036import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter;
037import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
038import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
039import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
040import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords;
041import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
042import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
043import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
044import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
045import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
046import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
047import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
048import ca.uhn.fhir.jpa.model.entity.ResourceLink;
049import ca.uhn.fhir.jpa.model.entity.ResourceTable;
050import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity;
051import ca.uhn.fhir.jpa.model.entity.StorageSettings;
052import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
053import ca.uhn.fhir.parser.DataFormatException;
054import ca.uhn.fhir.rest.api.server.RequestDetails;
055import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
056import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
057import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
058import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
059import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
060import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
061import ca.uhn.fhir.util.FhirTerser;
062import com.google.common.annotations.VisibleForTesting;
063import jakarta.annotation.Nonnull;
064import jakarta.annotation.Nullable;
065import org.apache.commons.lang3.StringUtils;
066import org.hl7.fhir.instance.model.api.IBaseReference;
067import org.hl7.fhir.instance.model.api.IBaseResource;
068import org.hl7.fhir.instance.model.api.IIdType;
069import org.hl7.fhir.r4.model.IdType;
070import org.springframework.beans.factory.annotation.Autowired;
071
072import java.util.ArrayList;
073import java.util.Collection;
074import java.util.Date;
075import java.util.HashMap;
076import java.util.HashSet;
077import java.util.List;
078import java.util.Map;
079import java.util.Optional;
080import java.util.Set;
081import java.util.stream.Collectors;
082
083import static org.apache.commons.lang3.StringUtils.isBlank;
084import static org.apache.commons.lang3.StringUtils.isNotBlank;
085
086public class SearchParamExtractorService {
087        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class);
088
089        @Autowired
090        private ISearchParamExtractor mySearchParamExtractor;
091
092        @Autowired
093        private IInterceptorBroadcaster myInterceptorBroadcaster;
094
095        @Autowired
096        private StorageSettings myStorageSettings;
097
098        @Autowired
099        private FhirContext myContext;
100
101        @Autowired
102        private ISearchParamRegistry mySearchParamRegistry;
103
104        @Autowired
105        private PartitionSettings myPartitionSettings;
106
107        @Autowired(required = false)
108        private IResourceLinkResolver myResourceLinkResolver;
109
110        @VisibleForTesting
111        public void setSearchParamExtractor(ISearchParamExtractor theSearchParamExtractor) {
112                mySearchParamExtractor = theSearchParamExtractor;
113        }
114
115        public void extractFromResource(
116                        RequestPartitionId theRequestPartitionId,
117                        RequestDetails theRequestDetails,
118                        ResourceIndexedSearchParams theParams,
119                        ResourceTable theEntity,
120                        IBaseResource theResource,
121                        TransactionDetails theTransactionDetails,
122                        boolean theFailOnInvalidReference) {
123                extractFromResource(
124                                theRequestPartitionId,
125                                theRequestDetails,
126                                theParams,
127                                ResourceIndexedSearchParams.withSets(),
128                                theEntity,
129                                theResource,
130                                theTransactionDetails,
131                                theFailOnInvalidReference,
132                                ISearchParamExtractor.ALL_PARAMS);
133        }
134
135        /**
136         * This method is responsible for scanning a resource for all of the search parameter instances.
137         * I.e. for all search parameters defined for
138         * a given resource type, it extracts the associated indexes and populates
139         * {@literal theParams}.
140         */
141        public void extractFromResource(
142                        RequestPartitionId theRequestPartitionId,
143                        RequestDetails theRequestDetails,
144                        ResourceIndexedSearchParams theNewParams,
145                        ResourceIndexedSearchParams theExistingParams,
146                        ResourceTable theEntity,
147                        IBaseResource theResource,
148                        TransactionDetails theTransactionDetails,
149                        boolean theFailOnInvalidReference,
150                        @Nonnull ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
151                // All search parameter types except Reference
152                ResourceIndexedSearchParams normalParams = ResourceIndexedSearchParams.withSets();
153                extractSearchIndexParameters(theRequestDetails, normalParams, theResource, theSearchParamFilter);
154                mergeParams(normalParams, theNewParams);
155
156                boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources();
157                ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences =
158                                mySearchParamExtractor.extractResourceLinks(theResource, indexOnContainedResources);
159                SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences);
160
161                if (indexOnContainedResources) {
162                        ResourceIndexedSearchParams containedParams = ResourceIndexedSearchParams.withSets();
163                        extractSearchIndexParametersForContainedResources(
164                                        theRequestDetails, containedParams, theResource, theEntity, indexedReferences);
165                        mergeParams(containedParams, theNewParams);
166                }
167
168                if (myStorageSettings.isIndexOnUpliftedRefchains()) {
169                        ResourceIndexedSearchParams containedParams = ResourceIndexedSearchParams.withSets();
170                        extractSearchIndexParametersForUpliftedRefchains(
171                                        theRequestDetails,
172                                        containedParams,
173                                        theEntity,
174                                        theRequestPartitionId,
175                                        theTransactionDetails,
176                                        indexedReferences);
177                        mergeParams(containedParams, theNewParams);
178                }
179
180                // Do this after, because we add to strings during both string and token processing, and contained resource if
181                // any
182                populateResourceTables(theNewParams, theEntity);
183
184                // Reference search parameters
185                extractResourceLinks(
186                                theRequestPartitionId,
187                                theExistingParams,
188                                theNewParams,
189                                theEntity,
190                                theResource,
191                                theTransactionDetails,
192                                theFailOnInvalidReference,
193                                theRequestDetails,
194                                indexedReferences);
195
196                if (indexOnContainedResources) {
197                        extractResourceLinksForContainedResources(
198                                        theRequestPartitionId,
199                                        theNewParams,
200                                        theEntity,
201                                        theResource,
202                                        theTransactionDetails,
203                                        theFailOnInvalidReference,
204                                        theRequestDetails);
205                }
206
207                // Missing (:missing) Indexes - These are indexes to satisfy the :missing
208                // modifier
209                if (myStorageSettings.getIndexMissingFields() == StorageSettings.IndexEnabledEnum.ENABLED) {
210
211                        // References
212                        Map<String, Boolean> presenceMap = getReferenceSearchParamPresenceMap(theEntity, theNewParams);
213                        presenceMap.forEach((key, value) -> {
214                                SearchParamPresentEntity present = new SearchParamPresentEntity();
215                                present.setPartitionSettings(myPartitionSettings);
216                                present.setResource(theEntity);
217                                present.setParamName(key);
218                                present.setPresent(value);
219                                present.setPartitionId(theEntity.getPartitionId());
220                                present.calculateHashes();
221                                theNewParams.mySearchParamPresentEntities.add(present);
222                        });
223
224                        // Everything else
225                        ResourceSearchParams activeSearchParams =
226                                        mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType());
227                        theNewParams.findMissingSearchParams(myPartitionSettings, myStorageSettings, theEntity, activeSearchParams);
228                }
229
230                extractSearchParamComboUnique(theEntity, theNewParams);
231
232                extractSearchParamComboNonUnique(theEntity, theNewParams);
233
234                theNewParams.setUpdatedTime(theTransactionDetails.getTransactionDate());
235        }
236
237        @Nonnull
238        private Map<String, Boolean> getReferenceSearchParamPresenceMap(
239                        ResourceTable entity, ResourceIndexedSearchParams newParams) {
240                Map<String, Boolean> retval = new HashMap<>();
241
242                for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
243                        retval.put(nextKey, Boolean.TRUE);
244                }
245
246                ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(entity.getResourceType());
247                activeSearchParams.getReferenceSearchParamNames().forEach(key -> retval.putIfAbsent(key, Boolean.FALSE));
248                return retval;
249        }
250
251        @VisibleForTesting
252        public void setStorageSettings(StorageSettings theStorageSettings) {
253                myStorageSettings = theStorageSettings;
254        }
255
256        /**
257         * Extract search parameter indexes for contained resources. E.g. if we
258         * are storing a Patient with a contained Organization, we might extract
259         * a String index on the Patient with paramName="organization.name" and
260         * value="Org Name"
261         */
262        private void extractSearchIndexParametersForContainedResources(
263                        RequestDetails theRequestDetails,
264                        ResourceIndexedSearchParams theParams,
265                        IBaseResource theResource,
266                        ResourceTable theEntity,
267                        ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
268
269                FhirTerser terser = myContext.newTerser();
270
271                // 1. get all contained resources
272                Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
273
274                // Extract search parameters
275                IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() {
276                        @Nonnull
277                        @Override
278                        public ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef) {
279                                // Currently for contained resources we always index all search parameters
280                                // on all contained resources. A potential nice future optimization would
281                                // be to make this configurable, perhaps with an optional extension you could
282                                // add to a SearchParameter?
283                                return ISearchParamExtractor.ALL_PARAMS;
284                        }
285
286                        @Override
287                        public IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef) {
288                                if (thePathAndRef.getRef() == null) {
289                                        return null;
290                                }
291                                return findContainedResource(containedResources, thePathAndRef.getRef());
292                        }
293                };
294                boolean recurse = myStorageSettings.isIndexOnContainedResourcesRecursively();
295                extractSearchIndexParametersForTargetResources(
296                                theRequestDetails,
297                                theParams,
298                                theEntity,
299                                new HashSet<>(),
300                                strategy,
301                                theIndexedReferences,
302                                recurse,
303                                true);
304        }
305
306        /**
307         * Extract search parameter indexes for uplifted refchains. E.g. if we
308         * are storing a Patient with reference to an Organization and the
309         * "Patient:organization" SearchParameter declares an uplifted refchain
310         * on the "name" SearchParameter, we might extract a String index
311         * on the Patient with paramName="organization.name" and value="Org Name"
312         */
313        private void extractSearchIndexParametersForUpliftedRefchains(
314                        RequestDetails theRequestDetails,
315                        ResourceIndexedSearchParams theParams,
316                        ResourceTable theEntity,
317                        RequestPartitionId theRequestPartitionId,
318                        TransactionDetails theTransactionDetails,
319                        ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
320                IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() {
321
322                        @Nonnull
323                        @Override
324                        public ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef) {
325                                String searchParamName = thePathAndRef.getSearchParamName();
326                                RuntimeSearchParam searchParam =
327                                                mySearchParamRegistry.getActiveSearchParam(theEntity.getResourceType(), searchParamName);
328                                Set<String> upliftRefchainCodes = searchParam.getUpliftRefchainCodes();
329                                if (upliftRefchainCodes.isEmpty()) {
330                                        return ISearchParamExtractor.NO_PARAMS;
331                                }
332                                return sp -> sp.stream()
333                                                .filter(t -> upliftRefchainCodes.contains(t.getName()))
334                                                .collect(Collectors.toList());
335                        }
336
337                        @Override
338                        public IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef) {
339                                // The PathAndRef will contain a resource if the SP path was inside a Bundle
340                                // and pointed to a resource (e.g. Bundle.entry.resource) as opposed to
341                                // pointing to a reference (e.g. Observation.subject)
342                                if (thePathAndRef.getResource() != null) {
343                                        return thePathAndRef.getResource();
344                                }
345
346                                // Ok, it's a normal reference
347                                IIdType reference = thePathAndRef.getRef().getReferenceElement();
348
349                                // If we're processing a FHIR transaction, we store the resources
350                                // mapped by their resolved resource IDs in theTransactionDetails
351                                IBaseResource resolvedResource = theTransactionDetails.getResolvedResource(reference);
352
353                                // And the usual case is that the reference points to a resource
354                                // elsewhere in the repository, so we load it
355                                if (resolvedResource == null
356                                                && myResourceLinkResolver != null
357                                                && !reference.getValue().startsWith("urn:uuid:")) {
358                                        RequestPartitionId targetRequestPartitionId = determineResolverPartitionId(theRequestPartitionId);
359                                        resolvedResource = myResourceLinkResolver.loadTargetResource(
360                                                        targetRequestPartitionId,
361                                                        theEntity.getResourceType(),
362                                                        thePathAndRef,
363                                                        theRequestDetails,
364                                                        theTransactionDetails);
365                                        if (resolvedResource != null) {
366                                                ourLog.trace("Found target: {}", resolvedResource.getIdElement());
367                                                theTransactionDetails.addResolvedResource(
368                                                                thePathAndRef.getRef().getReferenceElement(), resolvedResource);
369                                        }
370                                }
371
372                                return resolvedResource;
373                        }
374                };
375                extractSearchIndexParametersForTargetResources(
376                                theRequestDetails, theParams, theEntity, new HashSet<>(), strategy, theIndexedReferences, false, false);
377        }
378
379        /**
380         * Extract indexes for contained references as well as for uplifted refchains.
381         * These two types of indexes are both similar special cases. Normally we handle
382         * chained searches ("Patient?organization.name=Foo") using a join from the
383         * {@link ResourceLink} table (for the "organization" part) to the
384         * {@link ResourceIndexedSearchParamString} table (for the "name" part). But
385         * for both contained resource indexes and uplifted refchains we use only the
386         * {@link ResourceIndexedSearchParamString} table to handle the entire
387         * "organization.name" part, or the other similar tables for token, number, etc.
388         *
389         * @see #extractSearchIndexParametersForContainedResources(RequestDetails, ResourceIndexedSearchParams, IBaseResource, ResourceTable, ISearchParamExtractor.SearchParamSet)
390         * @see #extractSearchIndexParametersForUpliftedRefchains(RequestDetails, ResourceIndexedSearchParams, ResourceTable, RequestPartitionId, TransactionDetails, ISearchParamExtractor.SearchParamSet)
391         */
392        private void extractSearchIndexParametersForTargetResources(
393                        RequestDetails theRequestDetails,
394                        ResourceIndexedSearchParams theParams,
395                        ResourceTable theEntity,
396                        Collection<IBaseResource> theAlreadySeenResources,
397                        IChainedSearchParameterExtractionStrategy theTargetIndexingStrategy,
398                        ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences,
399                        boolean theRecurse,
400                        boolean theIndexOnContainedResources) {
401                // 2. Find referenced search parameters
402
403                String spnamePrefix;
404                // 3. for each referenced search parameter, create an index
405                for (PathAndRef nextPathAndRef : theIndexedReferences) {
406
407                        // 3.1 get the search parameter name as spname prefix
408                        spnamePrefix = nextPathAndRef.getSearchParamName();
409
410                        if (spnamePrefix == null || (nextPathAndRef.getRef() == null && nextPathAndRef.getResource() == null))
411                                continue;
412
413                        // 3.1.2 check if this ref actually applies here
414                        ISearchParamExtractor.ISearchParamFilter searchParamsToIndex =
415                                        theTargetIndexingStrategy.getSearchParamFilter(nextPathAndRef);
416                        if (searchParamsToIndex == ISearchParamExtractor.NO_PARAMS) {
417                                continue;
418                        }
419
420                        // 3.2 find the target resource
421                        IBaseResource targetResource = theTargetIndexingStrategy.fetchResourceAtPath(nextPathAndRef);
422                        if (targetResource == null) continue;
423
424                        // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite
425                        // loops
426                        if (theAlreadySeenResources.contains(targetResource)) {
427                                continue;
428                        }
429
430                        ResourceIndexedSearchParams currParams = ResourceIndexedSearchParams.withSets();
431
432                        // 3.3 create indexes for the current contained resource
433                        extractSearchIndexParameters(theRequestDetails, currParams, targetResource, searchParamsToIndex);
434
435                        // 3.4 recurse to process any other contained resources referenced by this one
436                        // Recursing is currently only allowed for contained resources and not
437                        // uplifted refchains because the latter could potentially kill performance
438                        // with the number of resource resolutions needed in order to handle
439                        // a single write. Maybe in the future we could add caching to improve
440                        // this
441                        if (theRecurse) {
442                                HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
443                                nextAlreadySeenResources.add(targetResource);
444
445                                ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences =
446                                                mySearchParamExtractor.extractResourceLinks(targetResource, theIndexOnContainedResources);
447                                SearchParamExtractorService.handleWarnings(
448                                                theRequestDetails, myInterceptorBroadcaster, indexedReferences);
449
450                                extractSearchIndexParametersForTargetResources(
451                                                theRequestDetails,
452                                                currParams,
453                                                theEntity,
454                                                nextAlreadySeenResources,
455                                                theTargetIndexingStrategy,
456                                                indexedReferences,
457                                                true,
458                                                theIndexOnContainedResources);
459                        }
460
461                        // 3.5 added reference name as a prefix for the contained resource if any
462                        // e.g. for Observation.subject contained reference
463                        // the SP_NAME = subject.family
464                        currParams.updateSpnamePrefixForIndexOnUpliftedChain(
465                                        theEntity.getResourceType(), nextPathAndRef.getSearchParamName());
466
467                        // 3.6 merge to the mainParams
468                        // NOTE: the spname prefix is different
469                        mergeParams(currParams, theParams);
470                }
471        }
472
473        private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) {
474                for (IBaseResource resource : resources) {
475                        if (resource.getIdElement().equals(reference.getReferenceElement())) return resource;
476                }
477                return null;
478        }
479
480        private void mergeParams(ResourceIndexedSearchParams theSrcParams, ResourceIndexedSearchParams theTargetParams) {
481
482                theTargetParams.myNumberParams.addAll(theSrcParams.myNumberParams);
483                theTargetParams.myQuantityParams.addAll(theSrcParams.myQuantityParams);
484                theTargetParams.myQuantityNormalizedParams.addAll(theSrcParams.myQuantityNormalizedParams);
485                theTargetParams.myDateParams.addAll(theSrcParams.myDateParams);
486                theTargetParams.myUriParams.addAll(theSrcParams.myUriParams);
487                theTargetParams.myTokenParams.addAll(theSrcParams.myTokenParams);
488                theTargetParams.myStringParams.addAll(theSrcParams.myStringParams);
489                theTargetParams.myCoordsParams.addAll(theSrcParams.myCoordsParams);
490                theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams);
491        }
492
493        void extractSearchIndexParameters(
494                        RequestDetails theRequestDetails,
495                        ResourceIndexedSearchParams theParams,
496                        IBaseResource theResource,
497                        @Nonnull ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
498
499                // Strings
500                ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings =
501                                extractSearchParamStrings(theResource, theSearchParamFilter);
502                handleWarnings(theRequestDetails, myInterceptorBroadcaster, strings);
503                theParams.myStringParams.addAll(strings);
504
505                // Numbers
506                ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> numbers =
507                                extractSearchParamNumber(theResource, theSearchParamFilter);
508                handleWarnings(theRequestDetails, myInterceptorBroadcaster, numbers);
509                theParams.myNumberParams.addAll(numbers);
510
511                // Quantities
512                ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> quantities =
513                                extractSearchParamQuantity(theResource, theSearchParamFilter);
514                handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantities);
515                theParams.myQuantityParams.addAll(quantities);
516
517                if (myStorageSettings
518                                                .getNormalizedQuantitySearchLevel()
519                                                .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED)
520                                || myStorageSettings
521                                                .getNormalizedQuantitySearchLevel()
522                                                .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) {
523                        ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized> quantitiesNormalized =
524                                        extractSearchParamQuantityNormalized(theResource, theSearchParamFilter);
525                        handleWarnings(theRequestDetails, myInterceptorBroadcaster, quantitiesNormalized);
526                        theParams.myQuantityNormalizedParams.addAll(quantitiesNormalized);
527                }
528
529                // Dates
530                ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> dates =
531                                extractSearchParamDates(theResource, theSearchParamFilter);
532                handleWarnings(theRequestDetails, myInterceptorBroadcaster, dates);
533                theParams.myDateParams.addAll(dates);
534
535                // URIs
536                ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> uris =
537                                extractSearchParamUri(theResource, theSearchParamFilter);
538                handleWarnings(theRequestDetails, myInterceptorBroadcaster, uris);
539                theParams.myUriParams.addAll(uris);
540
541                // Tokens (can result in both Token and String, as we index the display name for
542                // the types: Coding, CodeableConcept)
543                ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> tokens =
544                                extractSearchParamTokens(theResource, theSearchParamFilter);
545                for (BaseResourceIndexedSearchParam next : tokens) {
546                        if (next instanceof ResourceIndexedSearchParamToken) {
547                                theParams.myTokenParams.add((ResourceIndexedSearchParamToken) next);
548                        } else if (next instanceof ResourceIndexedSearchParamCoords) {
549                                theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next);
550                        } else {
551                                theParams.myStringParams.add((ResourceIndexedSearchParamString) next);
552                        }
553                }
554
555                // Composites
556                // dst2 composites use stuff like value[x] , and we don't support them.
557                if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
558                        ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> composites =
559                                        extractSearchParamComposites(theResource, theSearchParamFilter);
560                        handleWarnings(theRequestDetails, myInterceptorBroadcaster, composites);
561                        theParams.myCompositeParams.addAll(composites);
562                }
563
564                // Specials
565                ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> specials =
566                                extractSearchParamSpecial(theResource, theSearchParamFilter);
567                for (BaseResourceIndexedSearchParam next : specials) {
568                        if (next instanceof ResourceIndexedSearchParamCoords) {
569                                theParams.myCoordsParams.add((ResourceIndexedSearchParamCoords) next);
570                        }
571                }
572        }
573
574        private void populateResourceTables(ResourceIndexedSearchParams theParams, ResourceTable theEntity) {
575
576                populateResourceTable(theParams.myNumberParams, theEntity);
577                populateResourceTable(theParams.myQuantityParams, theEntity);
578                populateResourceTable(theParams.myQuantityNormalizedParams, theEntity);
579                populateResourceTable(theParams.myDateParams, theEntity);
580                populateResourceTable(theParams.myUriParams, theEntity);
581                populateResourceTable(theParams.myTokenParams, theEntity);
582                populateResourceTable(theParams.myStringParams, theEntity);
583                populateResourceTable(theParams.myCoordsParams, theEntity);
584        }
585
586        @VisibleForTesting
587        public void setContext(FhirContext theContext) {
588                myContext = theContext;
589        }
590
591        private void extractResourceLinks(
592                        RequestPartitionId theRequestPartitionId,
593                        ResourceIndexedSearchParams theParams,
594                        ResourceTable theEntity,
595                        IBaseResource theResource,
596                        TransactionDetails theTransactionDetails,
597                        boolean theFailOnInvalidReference,
598                        RequestDetails theRequest,
599                        ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
600                extractResourceLinks(
601                                theRequestPartitionId,
602                                ResourceIndexedSearchParams.withSets(),
603                                theParams,
604                                theEntity,
605                                theResource,
606                                theTransactionDetails,
607                                theFailOnInvalidReference,
608                                theRequest,
609                                theIndexedReferences);
610        }
611
612        private void extractResourceLinks(
613                        RequestPartitionId theRequestPartitionId,
614                        ResourceIndexedSearchParams theExistingParams,
615                        ResourceIndexedSearchParams theNewParams,
616                        ResourceTable theEntity,
617                        IBaseResource theResource,
618                        TransactionDetails theTransactionDetails,
619                        boolean theFailOnInvalidReference,
620                        RequestDetails theRequest,
621                        ISearchParamExtractor.SearchParamSet<PathAndRef> theIndexedReferences) {
622                String sourceResourceName = myContext.getResourceType(theResource);
623
624                for (PathAndRef nextPathAndRef : theIndexedReferences) {
625                        if (nextPathAndRef.getRef() != null) {
626                                if (nextPathAndRef.getRef().getReferenceElement().isLocal()) {
627                                        continue;
628                                }
629
630                                RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(
631                                                sourceResourceName, nextPathAndRef.getSearchParamName());
632                                extractResourceLinks(
633                                                theRequestPartitionId,
634                                                theExistingParams,
635                                                theNewParams,
636                                                theEntity,
637                                                theTransactionDetails,
638                                                sourceResourceName,
639                                                searchParam,
640                                                nextPathAndRef,
641                                                theFailOnInvalidReference,
642                                                theRequest);
643                        }
644                }
645
646                theEntity.setHasLinks(theNewParams.myLinks.size() > 0);
647        }
648
649        private void extractResourceLinks(
650                        @Nonnull RequestPartitionId theRequestPartitionId,
651                        ResourceIndexedSearchParams theExistingParams,
652                        ResourceIndexedSearchParams theNewParams,
653                        ResourceTable theEntity,
654                        TransactionDetails theTransactionDetails,
655                        String theSourceResourceName,
656                        RuntimeSearchParam theRuntimeSearchParam,
657                        PathAndRef thePathAndRef,
658                        boolean theFailOnInvalidReference,
659                        RequestDetails theRequest) {
660                IBaseReference nextReference = thePathAndRef.getRef();
661                IIdType nextId = nextReference.getReferenceElement();
662                String path = thePathAndRef.getPath();
663                Date transactionDate = theTransactionDetails.getTransactionDate();
664
665                /*
666                 * This can only really happen if the DAO is being called
667                 * programmatically with a Bundle (not through the FHIR REST API)
668                 * but Smile does this
669                 */
670                if (nextId.isEmpty() && nextReference.getResource() != null) {
671                        nextId = nextReference.getResource().getIdElement();
672                }
673
674                if (myContext.getParserOptions().isStripVersionsFromReferences()
675                                && !myContext
676                                                .getParserOptions()
677                                                .getDontStripVersionsFromReferencesAtPaths()
678                                                .contains(thePathAndRef.getPath())
679                                && nextId.hasVersionIdPart()) {
680                        nextId = nextId.toVersionless();
681                }
682
683                theNewParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName());
684
685                boolean canonical = thePathAndRef.isCanonical();
686                if (LogicalReferenceHelper.isLogicalReference(myStorageSettings, nextId) || canonical) {
687                        String value = nextId.getValue();
688                        ResourceLink resourceLink =
689                                        ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, transactionDate);
690                        if (theNewParams.myLinks.add(resourceLink)) {
691                                ourLog.debug("Indexing remote resource reference URL: {}", nextId);
692                        }
693                        return;
694                }
695
696                String baseUrl = nextId.getBaseUrl();
697
698                // If this is a conditional URL, the part after the question mark
699                // can include URLs (e.g. token system URLs) and these really confuse
700                // the IdType parser because a conditional URL isn't actually a valid
701                // FHIR ID. So in order to truly determine whether we're dealing with
702                // an absolute reference, we strip the query part and reparse
703                // the reference.
704                int questionMarkIndex = nextId.getValue().indexOf('?');
705                if (questionMarkIndex != -1) {
706                        IdType preQueryId = new IdType(nextId.getValue().substring(0, questionMarkIndex - 1));
707                        baseUrl = preQueryId.getBaseUrl();
708                }
709
710                String typeString = nextId.getResourceType();
711                if (isBlank(typeString)) {
712                        String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - "
713                                        + nextId.getValue();
714                        if (theFailOnInvalidReference) {
715                                throw new InvalidRequestException(Msg.code(505) + msg);
716                        } else {
717                                ourLog.debug(msg);
718                                return;
719                        }
720                }
721                RuntimeResourceDefinition resourceDefinition;
722                try {
723                        resourceDefinition = myContext.getResourceDefinition(typeString);
724                } catch (DataFormatException e) {
725                        String msg = "Invalid resource reference found at path[" + path
726                                        + "] - Resource type is unknown or not supported on this server - " + nextId.getValue();
727                        if (theFailOnInvalidReference) {
728                                throw new InvalidRequestException(Msg.code(506) + msg);
729                        } else {
730                                ourLog.debug(msg);
731                                return;
732                        }
733                }
734
735                if (theRuntimeSearchParam.hasTargets()) {
736                        if (!theRuntimeSearchParam.getTargets().contains(typeString)) {
737                                return;
738                        }
739                }
740
741                if (isNotBlank(baseUrl)) {
742                        if (!myStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl)
743                                        && !myStorageSettings.isAllowExternalReferences()) {
744                                String msg = myContext
745                                                .getLocalizer()
746                                                .getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue());
747                                throw new InvalidRequestException(Msg.code(507) + msg);
748                        } else {
749                                ResourceLink resourceLink =
750                                                ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, transactionDate);
751                                if (theNewParams.myLinks.add(resourceLink)) {
752                                        ourLog.debug("Indexing remote resource reference URL: {}", nextId);
753                                }
754                                return;
755                        }
756                }
757
758                Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
759                String targetId = nextId.getIdPart();
760                if (StringUtils.isBlank(targetId)) {
761                        String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - "
762                                        + nextId.getValue();
763                        if (theFailOnInvalidReference) {
764                                throw new InvalidRequestException(Msg.code(508) + msg);
765                        } else {
766                                ourLog.debug(msg);
767                                return;
768                        }
769                }
770
771                IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
772                JpaPid resolvedTargetId = (JpaPid) theTransactionDetails.getResolvedResourceId(referenceElement);
773                ResourceLink resourceLink;
774
775                Long targetVersionId = nextId.getVersionIdPartAsLong();
776                if (resolvedTargetId != null) {
777
778                        /*
779                         * If we have already resolved the given reference within this transaction, we don't
780                         * need to resolve it again
781                         */
782                        myResourceLinkResolver.validateTypeOrThrowException(type);
783                        resourceLink = ResourceLink.forLocalReference(
784                                        thePathAndRef.getPath(),
785                                        theEntity,
786                                        typeString,
787                                        resolvedTargetId.getId(),
788                                        targetId,
789                                        transactionDate,
790                                        targetVersionId);
791
792                } else if (theFailOnInvalidReference) {
793
794                        /*
795                         * The reference points to another resource, so let's look it up. We need to do this
796                         * since the target may be a forced ID, but also so that we can throw an exception
797                         * if the reference is invalid
798                         */
799                        myResourceLinkResolver.validateTypeOrThrowException(type);
800
801                        /*
802                         * We need to obtain a resourceLink out of the provided {@literal thePathAndRef}.  In the case
803                         * where we are updating a resource that already has resourceLinks (stored in {@literal theExistingParams.getResourceLinks()}),
804                         * let's try to match thePathAndRef to an already existing resourceLink to avoid the
805                         * very expensive operation of creating a resourceLink that would end up being exactly the same
806                         * one we already have.
807                         */
808                        Optional<ResourceLink> optionalResourceLink =
809                                        findMatchingResourceLink(thePathAndRef, theExistingParams.getResourceLinks());
810                        if (optionalResourceLink.isPresent()) {
811                                resourceLink = optionalResourceLink.get();
812                        } else {
813                                resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull(
814                                                theRequestPartitionId,
815                                                theSourceResourceName,
816                                                thePathAndRef,
817                                                theEntity,
818                                                transactionDate,
819                                                nextId,
820                                                theRequest,
821                                                theTransactionDetails);
822                        }
823
824                        if (resourceLink == null) {
825                                return;
826                        } else {
827                                // Cache the outcome in the current transaction in case there are more references
828                                JpaPid persistentId = JpaPid.fromId(resourceLink.getTargetResourcePid());
829                                theTransactionDetails.addResolvedResourceId(referenceElement, persistentId);
830                        }
831
832                } else {
833
834                        /*
835                         * Just assume the reference is valid. This is used for in-memory matching since there
836                         * is no expectation of a database in this situation
837                         */
838                        ResourceTable target;
839                        target = new ResourceTable();
840                        target.setResourceType(typeString);
841                        resourceLink = ResourceLink.forLocalReference(
842                                        thePathAndRef.getPath(), theEntity, typeString, null, targetId, transactionDate, targetVersionId);
843                }
844
845                theNewParams.myLinks.add(resourceLink);
846        }
847
848        private Optional<ResourceLink> findMatchingResourceLink(
849                        PathAndRef thePathAndRef, Collection<ResourceLink> theResourceLinks) {
850                IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
851                List<ResourceLink> resourceLinks = new ArrayList<>(theResourceLinks);
852                for (ResourceLink resourceLink : resourceLinks) {
853
854                        // comparing the searchParam path ex: Group.member.entity
855                        boolean hasMatchingSearchParamPath =
856                                        StringUtils.equals(resourceLink.getSourcePath(), thePathAndRef.getPath());
857
858                        boolean hasMatchingResourceType =
859                                        StringUtils.equals(resourceLink.getTargetResourceType(), referenceElement.getResourceType());
860
861                        boolean hasMatchingResourceId =
862                                        StringUtils.equals(resourceLink.getTargetResourceId(), referenceElement.getIdPart());
863
864                        boolean hasMatchingResourceVersion = myContext.getParserOptions().isStripVersionsFromReferences()
865                                        || referenceElement.getVersionIdPartAsLong() == null
866                                        || referenceElement.getVersionIdPartAsLong().equals(resourceLink.getTargetResourceVersion());
867
868                        if (hasMatchingSearchParamPath
869                                        && hasMatchingResourceType
870                                        && hasMatchingResourceId
871                                        && hasMatchingResourceVersion) {
872                                return Optional.of(resourceLink);
873                        }
874                }
875
876                return Optional.empty();
877        }
878
879        private void extractResourceLinksForContainedResources(
880                        RequestPartitionId theRequestPartitionId,
881                        ResourceIndexedSearchParams theParams,
882                        ResourceTable theEntity,
883                        IBaseResource theResource,
884                        TransactionDetails theTransactionDetails,
885                        boolean theFailOnInvalidReference,
886                        RequestDetails theRequest) {
887
888                FhirTerser terser = myContext.newTerser();
889
890                // 1. get all contained resources
891                Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
892
893                extractResourceLinksForContainedResources(
894                                theRequestPartitionId,
895                                theParams,
896                                theEntity,
897                                theResource,
898                                theTransactionDetails,
899                                theFailOnInvalidReference,
900                                theRequest,
901                                containedResources,
902                                new HashSet<>());
903        }
904
905        private void extractResourceLinksForContainedResources(
906                        RequestPartitionId theRequestPartitionId,
907                        ResourceIndexedSearchParams theParams,
908                        ResourceTable theEntity,
909                        IBaseResource theResource,
910                        TransactionDetails theTransactionDetails,
911                        boolean theFailOnInvalidReference,
912                        RequestDetails theRequest,
913                        Collection<IBaseResource> theContainedResources,
914                        Collection<IBaseResource> theAlreadySeenResources) {
915
916                // 2. Find referenced search parameters
917                ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet =
918                                mySearchParamExtractor.extractResourceLinks(theResource, true);
919
920                String spNamePrefix;
921                ResourceIndexedSearchParams currParams;
922                // 3. for each referenced search parameter, create an index
923                for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
924
925                        // 3.1 get the search parameter name as spname prefix
926                        spNamePrefix = nextPathAndRef.getSearchParamName();
927
928                        if (spNamePrefix == null || nextPathAndRef.getRef() == null) continue;
929
930                        // 3.2 find the contained resource
931                        IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef());
932                        if (containedResource == null) continue;
933
934                        // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite
935                        // loops
936                        if (theAlreadySeenResources.contains(containedResource)) {
937                                continue;
938                        }
939
940                        currParams = ResourceIndexedSearchParams.withSets();
941
942                        // 3.3 create indexes for the current contained resource
943                        ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences =
944                                        mySearchParamExtractor.extractResourceLinks(containedResource, true);
945                        extractResourceLinks(
946                                        theRequestPartitionId,
947                                        currParams,
948                                        theEntity,
949                                        containedResource,
950                                        theTransactionDetails,
951                                        theFailOnInvalidReference,
952                                        theRequest,
953                                        indexedReferences);
954
955                        // 3.4 recurse to process any other contained resources referenced by this one
956                        if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
957                                HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
958                                nextAlreadySeenResources.add(containedResource);
959                                extractResourceLinksForContainedResources(
960                                                theRequestPartitionId,
961                                                currParams,
962                                                theEntity,
963                                                containedResource,
964                                                theTransactionDetails,
965                                                theFailOnInvalidReference,
966                                                theRequest,
967                                                theContainedResources,
968                                                nextAlreadySeenResources);
969                        }
970
971                        // 3.4 added reference name as a prefix for the contained resource if any
972                        // e.g. for Observation.subject contained reference
973                        // the SP_NAME = subject.family
974                        currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath());
975
976                        // 3.5 merge to the mainParams
977                        // NOTE: the spname prefix is different
978                        theParams.getResourceLinks().addAll(currParams.getResourceLinks());
979                }
980        }
981
982        @SuppressWarnings("unchecked")
983        private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(
984                        @Nonnull RequestPartitionId theRequestPartitionId,
985                        String theSourceResourceName,
986                        PathAndRef thePathAndRef,
987                        ResourceTable theEntity,
988                        Date theUpdateTime,
989                        IIdType theNextId,
990                        RequestDetails theRequest,
991                        TransactionDetails theTransactionDetails) {
992                JpaPid resolvedResourceId = (JpaPid) theTransactionDetails.getResolvedResourceId(theNextId);
993                if (resolvedResourceId != null) {
994                        String targetResourceType = theNextId.getResourceType();
995                        Long targetResourcePid = resolvedResourceId.getId();
996                        String targetResourceIdPart = theNextId.getIdPart();
997                        Long targetVersion = theNextId.getVersionIdPartAsLong();
998                        return ResourceLink.forLocalReference(
999                                        thePathAndRef.getPath(),
1000                                        theEntity,
1001                                        targetResourceType,
1002                                        targetResourcePid,
1003                                        targetResourceIdPart,
1004                                        theUpdateTime,
1005                                        targetVersion);
1006                }
1007
1008                /*
1009                 * We keep a cache of resolved target resources. This is good since for some resource types, there
1010                 * are multiple search parameters that map to the same element path within a resource (e.g.
1011                 * Observation:patient and Observation.subject and we don't want to force a resolution of the
1012                 * target any more times than we have to.
1013                 */
1014
1015                IResourceLookup<JpaPid> targetResource;
1016                if (myPartitionSettings.isPartitioningEnabled()) {
1017                        if (myPartitionSettings.getAllowReferencesAcrossPartitions()
1018                                        == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) {
1019
1020                                // Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED
1021                                if (CompositeInterceptorBroadcaster.hasHooks(
1022                                                Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, myInterceptorBroadcaster, theRequest)) {
1023                                        CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails(
1024                                                        theRequestPartitionId,
1025                                                        theSourceResourceName,
1026                                                        thePathAndRef,
1027                                                        theRequest,
1028                                                        theTransactionDetails);
1029                                        HookParams params = new HookParams(referenceDetails);
1030                                        targetResource =
1031                                                        (IResourceLookup<JpaPid>) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject(
1032                                                                        myInterceptorBroadcaster,
1033                                                                        theRequest,
1034                                                                        Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE,
1035                                                                        params);
1036                                } else {
1037                                        targetResource = myResourceLinkResolver.findTargetResource(
1038                                                        RequestPartitionId.allPartitions(),
1039                                                        theSourceResourceName,
1040                                                        thePathAndRef,
1041                                                        theRequest,
1042                                                        theTransactionDetails);
1043                                }
1044
1045                        } else {
1046                                targetResource = myResourceLinkResolver.findTargetResource(
1047                                                theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
1048                        }
1049                } else {
1050                        targetResource = myResourceLinkResolver.findTargetResource(
1051                                        theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
1052                }
1053
1054                if (targetResource == null) {
1055                        return null;
1056                }
1057
1058                String targetResourceType = targetResource.getResourceType();
1059                Long targetResourcePid = targetResource.getPersistentId().getId();
1060                String targetResourceIdPart = theNextId.getIdPart();
1061                Long targetVersion = theNextId.getVersionIdPartAsLong();
1062                return ResourceLink.forLocalReference(
1063                                thePathAndRef.getPath(),
1064                                theEntity,
1065                                targetResourceType,
1066                                targetResourcePid,
1067                                targetResourceIdPart,
1068                                theUpdateTime,
1069                                targetVersion);
1070        }
1071
1072        private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) {
1073                RequestPartitionId targetRequestPartitionId = theRequestPartitionId;
1074                if (myPartitionSettings.isPartitioningEnabled()
1075                                && myPartitionSettings.getAllowReferencesAcrossPartitions()
1076                                                == PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED) {
1077                        targetRequestPartitionId = RequestPartitionId.allPartitions();
1078                }
1079                return targetRequestPartitionId;
1080        }
1081
1082        private void populateResourceTable(
1083                        Collection<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) {
1084                for (BaseResourceIndexedSearchParam next : theParams) {
1085                        if (next.getResourcePid() == null) {
1086                                next.setResource(theResourceTable);
1087                        }
1088                }
1089        }
1090
1091        private void populateResourceTableForComboParams(
1092                        Collection<? extends IResourceIndexComboSearchParameter> theParams, ResourceTable theResourceTable) {
1093                for (IResourceIndexComboSearchParameter next : theParams) {
1094                        if (next.getResource() == null) {
1095                                next.setResource(theResourceTable);
1096                                if (next instanceof BasePartitionable) {
1097                                        ((BasePartitionable) next).setPartitionId(theResourceTable.getPartitionId());
1098                                }
1099                        }
1100                }
1101        }
1102
1103        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(
1104                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1105                return mySearchParamExtractor.extractSearchParamDates(theResource, theSearchParamFilter);
1106        }
1107
1108        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamNumber> extractSearchParamNumber(
1109                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1110                return mySearchParamExtractor.extractSearchParamNumber(theResource, theSearchParamFilter);
1111        }
1112
1113        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(
1114                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1115                return mySearchParamExtractor.extractSearchParamQuantity(theResource, theSearchParamFilter);
1116        }
1117
1118        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamQuantityNormalized>
1119                        extractSearchParamQuantityNormalized(
1120                                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1121                return mySearchParamExtractor.extractSearchParamQuantityNormalized(theResource, theSearchParamFilter);
1122        }
1123
1124        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(
1125                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1126                return mySearchParamExtractor.extractSearchParamStrings(theResource, theSearchParamFilter);
1127        }
1128
1129        private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(
1130                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1131                return mySearchParamExtractor.extractSearchParamTokens(theResource, theSearchParamFilter);
1132        }
1133
1134        private ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamSpecial(
1135                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1136                return mySearchParamExtractor.extractSearchParamSpecial(theResource, theSearchParamFilter);
1137        }
1138
1139        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamUri> extractSearchParamUri(
1140                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1141                return mySearchParamExtractor.extractSearchParamUri(theResource, theSearchParamFilter);
1142        }
1143
1144        private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(
1145                        IBaseResource theResource, ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
1146                return mySearchParamExtractor.extractSearchParamComposites(theResource, theSearchParamFilter);
1147        }
1148
1149        @VisibleForTesting
1150        void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) {
1151                myInterceptorBroadcaster = theInterceptorBroadcaster;
1152        }
1153
1154        @Nonnull
1155        public List<String> extractParamValuesAsStrings(
1156                        RuntimeSearchParam theActiveSearchParam, IBaseResource theResource) {
1157                return mySearchParamExtractor.extractParamValuesAsStrings(theActiveSearchParam, theResource);
1158        }
1159
1160        public void extractSearchParamComboUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
1161                String resourceType = theEntity.getResourceType();
1162                Set<ResourceIndexedComboStringUnique> comboUniques =
1163                                mySearchParamExtractor.extractSearchParamComboUnique(resourceType, theParams);
1164                theParams.myComboStringUniques.addAll(comboUniques);
1165                populateResourceTableForComboParams(theParams.myComboStringUniques, theEntity);
1166        }
1167
1168        public void extractSearchParamComboNonUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
1169                String resourceType = theEntity.getResourceType();
1170                Set<ResourceIndexedComboTokenNonUnique> comboNonUniques =
1171                                mySearchParamExtractor.extractSearchParamComboNonUnique(resourceType, theParams);
1172                theParams.myComboTokenNonUnique.addAll(comboNonUniques);
1173                populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity);
1174        }
1175
1176        /**
1177         * This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)}
1178         * in order to use that method for extracting chained search parameter indexes both
1179         * from contained resources and from uplifted refchains.
1180         */
1181        private interface IChainedSearchParameterExtractionStrategy {
1182
1183                /**
1184                 * Which search parameters should be indexed for the resource target
1185                 * at the given path. In other words if thePathAndRef contains
1186                 * "Patient/123", then we could return a filter that only lets the
1187                 * "name" and "gender" search params through  if we only want those
1188                 * two parameters to be indexed for the resolved Patient resource
1189                 * with that ID.
1190                 */
1191                @Nonnull
1192                ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef);
1193
1194                /**
1195                 * Actually fetch the resource at the given path, or return
1196                 * {@literal null} if none can be found.
1197                 */
1198                @Nullable
1199                IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef);
1200        }
1201
1202        static void handleWarnings(
1203                        RequestDetails theRequestDetails,
1204                        IInterceptorBroadcaster theInterceptorBroadcaster,
1205                        ISearchParamExtractor.SearchParamSet<?> theSearchParamSet) {
1206                if (theSearchParamSet.getWarnings().isEmpty()) {
1207                        return;
1208                }
1209
1210                // If extraction generated any warnings, broadcast an error
1211                if (CompositeInterceptorBroadcaster.hasHooks(
1212                                Pointcut.JPA_PERFTRACE_WARNING, theInterceptorBroadcaster, theRequestDetails)) {
1213                        for (String next : theSearchParamSet.getWarnings()) {
1214                                StorageProcessingMessage messageHolder = new StorageProcessingMessage();
1215                                messageHolder.setMessage(next);
1216                                HookParams params = new HookParams()
1217                                                .add(RequestDetails.class, theRequestDetails)
1218                                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1219                                                .add(StorageProcessingMessage.class, messageHolder);
1220                                CompositeInterceptorBroadcaster.doCallHooks(
1221                                                theInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_WARNING, params);
1222                        }
1223                }
1224        }
1225}