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