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                        @Nonnull 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 (myContext.getParserOptions().isStripVersionsFromReferences()
617                                && !myContext
618                                                .getParserOptions()
619                                                .getDontStripVersionsFromReferencesAtPaths()
620                                                .contains(thePathAndRef.getPath())
621                                && nextId.hasVersionIdPart()) {
622                        nextId = nextId.toVersionless();
623                }
624
625                theNewParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName());
626
627                boolean canonical = thePathAndRef.isCanonical();
628                if (LogicalReferenceHelper.isLogicalReference(myStorageSettings, nextId) || canonical) {
629                        String value = nextId.getValue();
630                        ResourceLink resourceLink =
631                                        ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, transactionDate);
632                        if (theNewParams.myLinks.add(resourceLink)) {
633                                ourLog.debug("Indexing remote resource reference URL: {}", nextId);
634                        }
635                        return;
636                }
637
638                String baseUrl = nextId.getBaseUrl();
639
640                // If this is a conditional URL, the part after the question mark
641                // can include URLs (e.g. token system URLs) and these really confuse
642                // the IdType parser because a conditional URL isn't actually a valid
643                // FHIR ID. So in order to truly determine whether we're dealing with
644                // an absolute reference, we strip the query part and reparse
645                // the reference.
646                int questionMarkIndex = nextId.getValue().indexOf('?');
647                if (questionMarkIndex != -1) {
648                        IdType preQueryId = new IdType(nextId.getValue().substring(0, questionMarkIndex - 1));
649                        baseUrl = preQueryId.getBaseUrl();
650                }
651
652                String typeString = nextId.getResourceType();
653                if (isBlank(typeString)) {
654                        String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - "
655                                        + nextId.getValue();
656                        if (theFailOnInvalidReference) {
657                                throw new InvalidRequestException(Msg.code(505) + msg);
658                        } else {
659                                ourLog.debug(msg);
660                                return;
661                        }
662                }
663                RuntimeResourceDefinition resourceDefinition;
664                try {
665                        resourceDefinition = myContext.getResourceDefinition(typeString);
666                } catch (DataFormatException e) {
667                        String msg = "Invalid resource reference found at path[" + path
668                                        + "] - Resource type is unknown or not supported on this server - " + nextId.getValue();
669                        if (theFailOnInvalidReference) {
670                                throw new InvalidRequestException(Msg.code(506) + msg);
671                        } else {
672                                ourLog.debug(msg);
673                                return;
674                        }
675                }
676
677                if (theRuntimeSearchParam.hasTargets()) {
678                        if (!theRuntimeSearchParam.getTargets().contains(typeString)) {
679                                return;
680                        }
681                }
682
683                if (isNotBlank(baseUrl)) {
684                        if (!myStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl)
685                                        && !myStorageSettings.isAllowExternalReferences()) {
686                                String msg = myContext
687                                                .getLocalizer()
688                                                .getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue());
689                                throw new InvalidRequestException(Msg.code(507) + msg);
690                        } else {
691                                ResourceLink resourceLink =
692                                                ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, transactionDate);
693                                if (theNewParams.myLinks.add(resourceLink)) {
694                                        ourLog.debug("Indexing remote resource reference URL: {}", nextId);
695                                }
696                                return;
697                        }
698                }
699
700                Class<? extends IBaseResource> type = resourceDefinition.getImplementingClass();
701                String targetId = nextId.getIdPart();
702                if (StringUtils.isBlank(targetId)) {
703                        String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - "
704                                        + nextId.getValue();
705                        if (theFailOnInvalidReference) {
706                                throw new InvalidRequestException(Msg.code(508) + msg);
707                        } else {
708                                ourLog.debug(msg);
709                                return;
710                        }
711                }
712
713                IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
714                if (isBlank(referenceElement.getValue())) {
715                        // it's an embedded element maybe;
716                        // we need a valid referenceElement, becuase we
717                        // resolve the resource by this value (and if we use "null", we can't resolve multiple values)
718                        referenceElement = thePathAndRef.getRef().getResource().getIdElement();
719                }
720                JpaPid resolvedTargetId = (JpaPid) theTransactionDetails.getResolvedResourceId(referenceElement);
721                ResourceLink resourceLink;
722
723                Long targetVersionId = nextId.getVersionIdPartAsLong();
724                if (resolvedTargetId != null) {
725
726                        /*
727                         * If we have already resolved the given reference within this transaction, we don't
728                         * need to resolve it again
729                         */
730                        myResourceLinkResolver.validateTypeOrThrowException(type);
731
732                        ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
733                                        .setSourcePath(thePathAndRef.getPath())
734                                        .setSourceResource(theEntity)
735                                        .setTargetResourceType(typeString)
736                                        .setTargetResourcePid(resolvedTargetId.getId())
737                                        .setTargetResourceId(targetId)
738                                        .setUpdated(transactionDate)
739                                        .setTargetResourceVersion(targetVersionId)
740                                        .setTargetResourcePartitionablePartitionId(resolvedTargetId.getPartitionablePartitionId());
741
742                        resourceLink = forLocalReference(params);
743
744                } else if (theFailOnInvalidReference) {
745
746                        /*
747                         * The reference points to another resource, so let's look it up. We need to do this
748                         * since the target may be a forced ID, but also so that we can throw an exception
749                         * if the reference is invalid
750                         */
751                        myResourceLinkResolver.validateTypeOrThrowException(type);
752
753                        /*
754                         * We need to obtain a resourceLink out of the provided {@literal thePathAndRef}.  In the case
755                         * where we are updating a resource that already has resourceLinks (stored in {@literal theExistingParams.getResourceLinks()}),
756                         * let's try to match thePathAndRef to an already existing resourceLink to avoid the
757                         * very expensive operation of creating a resourceLink that would end up being exactly the same
758                         * one we already have.
759                         */
760                        Optional<ResourceLink> optionalResourceLink =
761                                        findMatchingResourceLink(thePathAndRef, theExistingParams.getResourceLinks());
762                        if (optionalResourceLink.isPresent()) {
763                                resourceLink = optionalResourceLink.get();
764                        } else {
765                                resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull(
766                                                theRequestPartitionId,
767                                                theSourceResourceName,
768                                                thePathAndRef,
769                                                theEntity,
770                                                transactionDate,
771                                                nextId,
772                                                theRequest,
773                                                theTransactionDetails);
774                        }
775
776                        if (resourceLink == null) {
777                                return;
778                        } else {
779                                // Cache the outcome in the current transaction in case there are more references
780                                JpaPid persistentId =
781                                                JpaPid.fromId(resourceLink.getTargetResourcePid(), resourceLink.getTargetResourcePartitionId());
782                                persistentId.setPartitionablePartitionId(PartitionablePartitionId.with(
783                                                resourceLink.getTargetResourcePartitionId(), resourceLink.getTargetResourcePartitionDate()));
784                                theTransactionDetails.addResolvedResourceId(referenceElement, persistentId);
785                        }
786
787                } else {
788
789                        /*
790                         * Just assume the reference is valid. This is used for in-memory matching since there
791                         * is no expectation of a database in this situation
792                         */
793                        ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
794                                        .setSourcePath(thePathAndRef.getPath())
795                                        .setSourceResource(theEntity)
796                                        .setTargetResourceType(typeString)
797                                        .setTargetResourceId(targetId)
798                                        .setUpdated(transactionDate)
799                                        .setTargetResourceVersion(targetVersionId);
800
801                        resourceLink = forLocalReference(params);
802                }
803
804                theNewParams.myLinks.add(resourceLink);
805        }
806
807        private Optional<ResourceLink> findMatchingResourceLink(
808                        PathAndRef thePathAndRef, Collection<ResourceLink> theResourceLinks) {
809                IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
810                List<ResourceLink> resourceLinks = new ArrayList<>(theResourceLinks);
811                for (ResourceLink resourceLink : resourceLinks) {
812
813                        // comparing the searchParam path ex: Group.member.entity
814                        boolean hasMatchingSearchParamPath =
815                                        StringUtils.equals(resourceLink.getSourcePath(), thePathAndRef.getPath());
816
817                        boolean hasMatchingResourceType =
818                                        StringUtils.equals(resourceLink.getTargetResourceType(), referenceElement.getResourceType());
819
820                        boolean hasMatchingResourceId =
821                                        StringUtils.equals(resourceLink.getTargetResourceId(), referenceElement.getIdPart());
822
823                        boolean hasMatchingResourceVersion = myContext.getParserOptions().isStripVersionsFromReferences()
824                                        || referenceElement.getVersionIdPartAsLong() == null
825                                        || referenceElement.getVersionIdPartAsLong().equals(resourceLink.getTargetResourceVersion());
826
827                        if (hasMatchingSearchParamPath
828                                        && hasMatchingResourceType
829                                        && hasMatchingResourceId
830                                        && hasMatchingResourceVersion) {
831                                return Optional.of(resourceLink);
832                        }
833                }
834
835                return Optional.empty();
836        }
837
838        private void extractResourceLinksForContainedResources(
839                        RequestPartitionId theRequestPartitionId,
840                        ResourceIndexedSearchParams theParams,
841                        ResourceTable theEntity,
842                        IBaseResource theResource,
843                        TransactionDetails theTransactionDetails,
844                        boolean theFailOnInvalidReference,
845                        RequestDetails theRequest) {
846
847                FhirTerser terser = myContext.newTerser();
848
849                // 1. get all contained resources
850                Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
851
852                extractResourceLinksForContainedResources(
853                                theRequestPartitionId,
854                                theParams,
855                                theEntity,
856                                theResource,
857                                theTransactionDetails,
858                                theFailOnInvalidReference,
859                                theRequest,
860                                containedResources,
861                                new HashSet<>());
862        }
863
864        private void extractResourceLinksForContainedResources(
865                        RequestPartitionId theRequestPartitionId,
866                        ResourceIndexedSearchParams theParams,
867                        ResourceTable theEntity,
868                        IBaseResource theResource,
869                        TransactionDetails theTransactionDetails,
870                        boolean theFailOnInvalidReference,
871                        RequestDetails theRequest,
872                        Collection<IBaseResource> theContainedResources,
873                        Collection<IBaseResource> theAlreadySeenResources) {
874
875                // 2. Find referenced search parameters
876                ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet =
877                                mySearchParamExtractor.extractResourceLinks(theResource, true);
878
879                String spNamePrefix;
880                ResourceIndexedSearchParams currParams;
881                // 3. for each referenced search parameter, create an index
882                for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
883
884                        // 3.1 get the search parameter name as spname prefix
885                        spNamePrefix = nextPathAndRef.getSearchParamName();
886
887                        if (spNamePrefix == null || nextPathAndRef.getRef() == null) continue;
888
889                        // 3.2 find the contained resource
890                        IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef());
891                        if (containedResource == null) continue;
892
893                        // 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite
894                        // loops
895                        if (theAlreadySeenResources.contains(containedResource)) {
896                                continue;
897                        }
898
899                        currParams = ResourceIndexedSearchParams.withSets();
900
901                        // 3.3 create indexes for the current contained resource
902                        ISearchParamExtractor.SearchParamSet<PathAndRef> indexedReferences =
903                                        mySearchParamExtractor.extractResourceLinks(containedResource, true);
904                        extractResourceLinks(
905                                        theRequestPartitionId,
906                                        currParams,
907                                        theEntity,
908                                        containedResource,
909                                        theTransactionDetails,
910                                        theFailOnInvalidReference,
911                                        theRequest,
912                                        indexedReferences);
913
914                        // 3.4 recurse to process any other contained resources referenced by this one
915                        if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
916                                HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
917                                nextAlreadySeenResources.add(containedResource);
918                                extractResourceLinksForContainedResources(
919                                                theRequestPartitionId,
920                                                currParams,
921                                                theEntity,
922                                                containedResource,
923                                                theTransactionDetails,
924                                                theFailOnInvalidReference,
925                                                theRequest,
926                                                theContainedResources,
927                                                nextAlreadySeenResources);
928                        }
929
930                        // 3.4 added reference name as a prefix for the contained resource if any
931                        // e.g. for Observation.subject contained reference
932                        // the SP_NAME = subject.family
933                        currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath());
934
935                        // 3.5 merge to the mainParams
936                        // NOTE: the spname prefix is different
937                        theParams.getResourceLinks().addAll(currParams.getResourceLinks());
938                }
939        }
940
941        @SuppressWarnings("unchecked")
942        private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(
943                        @Nonnull RequestPartitionId theRequestPartitionId,
944                        String theSourceResourceName,
945                        PathAndRef thePathAndRef,
946                        ResourceTable theEntity,
947                        Date theUpdateTime,
948                        IIdType theNextId,
949                        RequestDetails theRequest,
950                        TransactionDetails theTransactionDetails) {
951                JpaPid resolvedResourceId = (JpaPid) theTransactionDetails.getResolvedResourceId(theNextId);
952
953                if (resolvedResourceId != null) {
954                        String targetResourceType = theNextId.getResourceType();
955                        Long targetResourcePid = resolvedResourceId.getId();
956                        String targetResourceIdPart = theNextId.getIdPart();
957                        Long targetVersion = theNextId.getVersionIdPartAsLong();
958
959                        ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
960                                        .setSourcePath(thePathAndRef.getPath())
961                                        .setSourceResource(theEntity)
962                                        .setTargetResourceType(targetResourceType)
963                                        .setTargetResourcePid(targetResourcePid)
964                                        .setTargetResourceId(targetResourceIdPart)
965                                        .setUpdated(theUpdateTime)
966                                        .setTargetResourceVersion(targetVersion)
967                                        .setTargetResourcePartitionablePartitionId(resolvedResourceId.getPartitionablePartitionId());
968
969                        return ResourceLink.forLocalReference(params);
970                }
971
972                /*
973                 * We keep a cache of resolved target resources. This is good since for some resource types, there
974                 * are multiple search parameters that map to the same element path within a resource (e.g.
975                 * Observation:patient and Observation.subject and we don't want to force a resolution of the
976                 * target any more times than we have to.
977                 */
978
979                IResourceLookup<JpaPid> targetResource;
980                if (myPartitionSettings.isPartitioningEnabled()) {
981                        if (myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) {
982
983                                // Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED
984                                IInterceptorBroadcaster compositeBroadcaster =
985                                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
986                                if (compositeBroadcaster.hasHooks(Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE)) {
987                                        CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails(
988                                                        theRequestPartitionId,
989                                                        theSourceResourceName,
990                                                        thePathAndRef,
991                                                        theRequest,
992                                                        theTransactionDetails);
993                                        HookParams params = new HookParams(referenceDetails);
994                                        targetResource = (IResourceLookup<JpaPid>) compositeBroadcaster.callHooksAndReturnObject(
995                                                        Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, params);
996                                } else {
997                                        targetResource = myResourceLinkResolver.findTargetResource(
998                                                        RequestPartitionId.allPartitions(),
999                                                        theSourceResourceName,
1000                                                        thePathAndRef,
1001                                                        theRequest,
1002                                                        theTransactionDetails);
1003                                }
1004
1005                        } else {
1006                                targetResource = myResourceLinkResolver.findTargetResource(
1007                                                theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
1008                        }
1009                } else {
1010                        targetResource = myResourceLinkResolver.findTargetResource(
1011                                        theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
1012                }
1013
1014                if (targetResource == null) {
1015                        return null;
1016                }
1017
1018                String targetResourceType = targetResource.getResourceType();
1019                Long targetResourcePid = targetResource.getPersistentId().getId();
1020                String targetResourceIdPart = theNextId.getIdPart();
1021                Long targetVersion = theNextId.getVersionIdPartAsLong();
1022
1023                ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
1024                                .setSourcePath(thePathAndRef.getPath())
1025                                .setSourceResource(theEntity)
1026                                .setTargetResourceType(targetResourceType)
1027                                .setTargetResourcePid(targetResourcePid)
1028                                .setTargetResourceId(targetResourceIdPart)
1029                                .setUpdated(theUpdateTime)
1030                                .setTargetResourceVersion(targetVersion)
1031                                .setTargetResourcePartitionablePartitionId(targetResource.getPartitionId());
1032
1033                return forLocalReference(params);
1034        }
1035
1036        private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) {
1037                RequestPartitionId targetRequestPartitionId = theRequestPartitionId;
1038                if (myPartitionSettings.isPartitioningEnabled()
1039                                && myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) {
1040                        targetRequestPartitionId = RequestPartitionId.allPartitions();
1041                }
1042                return targetRequestPartitionId;
1043        }
1044
1045        private void populateResourceTable(
1046                        Collection<? extends BaseResourceIndexedSearchParam> theParams, ResourceTable theResourceTable) {
1047                for (BaseResourceIndexedSearchParam next : theParams) {
1048                        if (next.getResourcePid() == null) {
1049                                next.setResource(theResourceTable);
1050                        }
1051                }
1052        }
1053
1054        private void populateResourceTableForComboParams(
1055                        Collection<? extends IResourceIndexComboSearchParameter> theParams, ResourceTable theResourceTable) {
1056                for (IResourceIndexComboSearchParameter next : theParams) {
1057                        if (next.getResource() == null) {
1058                                next.setResource(theResourceTable);
1059                                if (next instanceof BasePartitionable) {
1060                                        ((BasePartitionable) next).setPartitionId(theResourceTable.getPartitionId());
1061                                }
1062                        }
1063                }
1064        }
1065
1066        @VisibleForTesting
1067        void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) {
1068                myInterceptorBroadcaster = theInterceptorBroadcaster;
1069        }
1070
1071        @Nonnull
1072        public List<String> extractParamValuesAsStrings(
1073                        RuntimeSearchParam theActiveSearchParam, IBaseResource theResource) {
1074                return mySearchParamExtractor.extractParamValuesAsStrings(theActiveSearchParam, theResource);
1075        }
1076
1077        public void extractSearchParamComboUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
1078                String resourceType = theEntity.getResourceType();
1079                Set<ResourceIndexedComboStringUnique> comboUniques =
1080                                mySearchParamExtractor.extractSearchParamComboUnique(resourceType, theParams);
1081                theParams.myComboStringUniques.addAll(comboUniques);
1082                populateResourceTableForComboParams(theParams.myComboStringUniques, theEntity);
1083        }
1084
1085        public void extractSearchParamComboNonUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
1086                String resourceType = theEntity.getResourceType();
1087                Set<ResourceIndexedComboTokenNonUnique> comboNonUniques =
1088                                mySearchParamExtractor.extractSearchParamComboNonUnique(resourceType, theParams);
1089                theParams.myComboTokenNonUnique.addAll(comboNonUniques);
1090                populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity);
1091        }
1092
1093        /**
1094         * This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)}
1095         * in order to use that method for extracting chained search parameter indexes both
1096         * from contained resources and from uplifted refchains.
1097         */
1098        private interface IChainedSearchParameterExtractionStrategy {
1099
1100                /**
1101                 * Which search parameters should be indexed for the resource target
1102                 * at the given path. In other words if thePathAndRef contains
1103                 * "Patient/123", then we could return a filter that only lets the
1104                 * "name" and "gender" search params through  if we only want those
1105                 * two parameters to be indexed for the resolved Patient resource
1106                 * with that ID.
1107                 */
1108                @Nonnull
1109                ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef);
1110
1111                /**
1112                 * Actually fetch the resource at the given path, or return
1113                 * {@literal null} if none can be found.
1114                 */
1115                @Nullable
1116                IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef);
1117        }
1118
1119        static void handleWarnings(
1120                        RequestDetails theRequestDetails,
1121                        IInterceptorBroadcaster theInterceptorBroadcaster,
1122                        ISearchParamExtractor.SearchParamSet<?> theSearchParamSet) {
1123                if (theSearchParamSet.getWarnings().isEmpty()) {
1124                        return;
1125                }
1126
1127                // If extraction generated any warnings, broadcast an error
1128                IInterceptorBroadcaster compositeBroadcaster =
1129                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequestDetails);
1130                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) {
1131                        for (String next : theSearchParamSet.getWarnings()) {
1132                                StorageProcessingMessage messageHolder = new StorageProcessingMessage();
1133                                messageHolder.setMessage(next);
1134                                HookParams params = new HookParams()
1135                                                .add(RequestDetails.class, theRequestDetails)
1136                                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1137                                                .add(StorageProcessingMessage.class, messageHolder);
1138                                compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params);
1139                        }
1140                }
1141        }
1142}