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