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