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