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