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