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.registry;
021
022import ca.uhn.fhir.context.ComboSearchParamType;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.interceptor.api.IInterceptorService;
028import ca.uhn.fhir.interceptor.model.RequestPartitionId;
029import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
030import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
031import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache;
032import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
033import ca.uhn.fhir.jpa.cache.ISearchParamIdentityCacheSvc;
034import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
035import ca.uhn.fhir.jpa.model.config.PartitionSettings;
036import ca.uhn.fhir.jpa.model.entity.StorageSettings;
037import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
038import ca.uhn.fhir.rest.api.Constants;
039import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
040import ca.uhn.fhir.rest.api.server.IBundleProvider;
041import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
042import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
043import ca.uhn.fhir.rest.server.util.IndexedSearchParam;
044import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
045import ca.uhn.fhir.util.SearchParameterUtil;
046import ca.uhn.fhir.util.StopWatch;
047import com.google.common.annotations.VisibleForTesting;
048import com.google.common.collect.Sets;
049import jakarta.annotation.Nonnull;
050import jakarta.annotation.Nullable;
051import jakarta.annotation.PostConstruct;
052import jakarta.annotation.PreDestroy;
053import org.apache.commons.lang3.StringUtils;
054import org.apache.commons.lang3.time.DateUtils;
055import org.hl7.fhir.instance.model.api.IBaseResource;
056import org.hl7.fhir.instance.model.api.IIdType;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059import org.springframework.beans.factory.ObjectProvider;
060import org.springframework.beans.factory.annotation.Autowired;
061
062import java.util.ArrayList;
063import java.util.Collection;
064import java.util.Collections;
065import java.util.HashSet;
066import java.util.Iterator;
067import java.util.List;
068import java.util.Map;
069import java.util.Objects;
070import java.util.Optional;
071import java.util.Set;
072import java.util.stream.Collectors;
073
074import static ca.uhn.fhir.rest.server.util.ISearchParamRegistry.isAllowedForContext;
075import static org.apache.commons.lang3.StringUtils.isNotBlank;
076
077public class SearchParamRegistryImpl
078                implements ISearchParamRegistry, IResourceChangeListener, ISearchParamRegistryController {
079
080        // Basic is needed by the R4 SubscriptionTopic registry
081        public static final Set<String> NON_DISABLEABLE_SEARCH_PARAMS =
082                        Collections.unmodifiableSet(Sets.newHashSet("*:url", "Subscription:*", "SearchParameter:*", "Basic:*"));
083
084        private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class);
085        public static final int MAX_MANAGED_PARAM_COUNT = 10000;
086        private static final long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE;
087        public static final String PARAM_LANGUAGE_ID = "SearchParameter/Resource-language";
088        public static final String PARAM_LANGUAGE_DESCRIPTION = "Language of the resource content";
089        public static final String PARAM_LANGUAGE_PATH = "language";
090        public static final String PARAM_TEXT_DESCRIPTION = "Text search against the narrative";
091        public static final String PARAM_CONTENT_DESCRIPTION = "Search on the entire content of the resource";
092
093        private JpaSearchParamCache myJpaSearchParamCache;
094
095        @Autowired
096        private StorageSettings myStorageSettings;
097
098        @Autowired
099        private ISearchParamProvider mySearchParamProvider;
100
101        @Autowired
102        private FhirContext myFhirContext;
103
104        @Autowired
105        private SearchParameterCanonicalizer mySearchParameterCanonicalizer;
106
107        @Autowired
108        private IInterceptorService myInterceptorService;
109
110        @Autowired
111        private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
112
113        @Autowired
114        private PartitionSettings myPartitionSettings;
115
116        @Autowired
117        private ObjectProvider<ISearchParamIdentityCacheSvc> mySearchParamIdentityCacheSvcProvider;
118
119        private IResourceChangeListenerCache myResourceChangeListenerCache;
120        private volatile ReadOnlySearchParamCache myBuiltInSearchParams;
121        private volatile IPhoneticEncoder myPhoneticEncoder;
122        private volatile RuntimeSearchParamCache myActiveSearchParams;
123        private boolean myPrePopulateSearchParamIdentities = true;
124
125        @VisibleForTesting
126        public void setPopulateSearchParamIdentities(boolean myPrePopulateSearchParamIdentities) {
127                this.myPrePopulateSearchParamIdentities = myPrePopulateSearchParamIdentities;
128        }
129
130        @VisibleForTesting
131        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
132                myPartitionSettings = thePartitionSettings;
133        }
134
135        /**
136         * Constructor
137         */
138        public SearchParamRegistryImpl() {
139                super();
140        }
141
142        @PostConstruct
143        public void start() {
144                myJpaSearchParamCache = new JpaSearchParamCache(myPartitionSettings);
145        }
146
147        @Override
148        public RuntimeSearchParam getActiveSearchParam(
149                        @Nonnull String theResourceName,
150                        @Nonnull String theParamName,
151                        @Nonnull SearchParamLookupContextEnum theContext) {
152                requiresActiveSearchParams();
153
154                // Can still be null in unit test scenarios
155                if (myActiveSearchParams != null) {
156                        RuntimeSearchParam param = myActiveSearchParams.get(theResourceName, theParamName);
157                        if (param != null) {
158                                if (isAllowedForContext(param, theContext)) {
159                                        return param;
160                                }
161                        }
162                }
163
164                return null;
165        }
166
167        @Nonnull
168        @Override
169        public ResourceSearchParams getActiveSearchParams(
170                        @Nonnull String theResourceName, @Nonnull SearchParamLookupContextEnum theContext) {
171                requiresActiveSearchParams();
172                return getActiveSearchParams().getSearchParamMap(theResourceName).toFilteredForContext(theContext);
173        }
174
175        private void requiresActiveSearchParams() {
176                if (myActiveSearchParams == null) {
177                        // forced refreshes should not use a cache - we're forcibly refreshing it, after all
178                        myResourceChangeListenerCache.forceRefresh();
179                }
180        }
181
182        @Override
183        public List<RuntimeSearchParam> getActiveComboSearchParams(
184                        @Nonnull String theResourceName, @Nonnull SearchParamLookupContextEnum theContext) {
185                return filteredForContext(myJpaSearchParamCache.getActiveComboSearchParams(theResourceName), theContext);
186        }
187
188        @Override
189        public List<RuntimeSearchParam> getActiveComboSearchParams(
190                        @Nonnull String theResourceName,
191                        @Nonnull ComboSearchParamType theParamType,
192                        @Nonnull SearchParamLookupContextEnum theContext) {
193                return filteredForContext(
194                                myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamType), theContext);
195        }
196
197        @Override
198        public List<RuntimeSearchParam> getActiveComboSearchParams(
199                        @Nonnull String theResourceName,
200                        @Nonnull Set<String> theParamNames,
201                        @Nonnull SearchParamLookupContextEnum theContext) {
202                return filteredForContext(
203                                myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamNames), theContext);
204        }
205
206        @Nullable
207        @Override
208        public RuntimeSearchParam getActiveSearchParamByUrl(
209                        @Nonnull String theUrl, @Nonnull SearchParamLookupContextEnum theContext) {
210                if (myActiveSearchParams != null) {
211                        RuntimeSearchParam param = myActiveSearchParams.getByUrl(theUrl);
212                        if (param != null && isAllowedForContext(param, theContext)) {
213                                return param;
214                        }
215                }
216                return null;
217        }
218
219        @Override
220        public Optional<RuntimeSearchParam> getActiveComboSearchParamById(
221                        @Nonnull String theResourceName, @Nonnull IIdType theId) {
222                return myJpaSearchParamCache.getActiveComboSearchParamById(theResourceName, theId);
223        }
224
225        private void rebuildActiveSearchParams() {
226                ourLog.info("Rebuilding SearchParamRegistry");
227                SearchParameterMap params = new SearchParameterMap();
228                params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
229                params.setCount(MAX_MANAGED_PARAM_COUNT);
230
231                IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
232
233                List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, MAX_MANAGED_PARAM_COUNT);
234                Integer size = allSearchParamsBp.size();
235
236                ourLog.trace("Loaded {} search params from the DB", allSearchParams.size());
237
238                if (size == null) {
239                        ourLog.error(
240                                        "Only {} search parameters have been loaded, but there are more than that in the repository.  Is offset search configured on this server?",
241                                        allSearchParams.size());
242                } else if (size >= MAX_MANAGED_PARAM_COUNT) {
243                        ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
244                }
245
246                initializeActiveSearchParams(allSearchParams);
247        }
248
249        private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearchParams) {
250                StopWatch sw = new StopWatch();
251
252                ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams();
253                RuntimeSearchParamCache searchParams =
254                                RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams);
255                long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams);
256                ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
257
258                // Auto-register: _language
259                if (myStorageSettings.isLanguageSearchParameterEnabled()) {
260                        registerImplicitSearchParam(
261                                        searchParams,
262                                        Constants.PARAM_LANGUAGE_URL,
263                                        Constants.PARAM_LANGUAGE,
264                                        PARAM_LANGUAGE_DESCRIPTION,
265                                        PARAM_LANGUAGE_PATH,
266                                        RestSearchParameterTypeEnum.TOKEN);
267                } else {
268                        unregisterImplicitSearchParam(searchParams, Constants.PARAM_LANGUAGE);
269                }
270
271                // Auto-register: _content and _text
272                if (myStorageSettings.isHibernateSearchIndexFullText()) {
273                        registerImplicitSearchParam(
274                                        searchParams,
275                                        Constants.PARAM_TEXT_URL,
276                                        Constants.PARAM_TEXT,
277                                        PARAM_TEXT_DESCRIPTION,
278                                        "Resource",
279                                        RestSearchParameterTypeEnum.STRING);
280                        registerImplicitSearchParam(
281                                        searchParams,
282                                        Constants.PARAM_CONTENT_URL,
283                                        Constants.PARAM_CONTENT,
284                                        PARAM_CONTENT_DESCRIPTION,
285                                        "Resource",
286                                        RestSearchParameterTypeEnum.STRING);
287                } else {
288                        unregisterImplicitSearchParam(searchParams, Constants.PARAM_CONTENT);
289                        unregisterImplicitSearchParam(searchParams, Constants.PARAM_TEXT);
290                }
291
292                removeInactiveSearchParams(searchParams);
293
294                setActiveSearchParams(searchParams);
295
296                myJpaSearchParamCache.populateActiveSearchParams(myInterceptorService, myPhoneticEncoder, myActiveSearchParams);
297                updateSearchParameterIdentityCache();
298                ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
299        }
300
301        private void unregisterImplicitSearchParam(RuntimeSearchParamCache theSearchParams, String theParamName) {
302                for (String resourceType : theSearchParams.getResourceNameKeys()) {
303                        theSearchParams.remove(resourceType, theParamName);
304                }
305        }
306
307        private void registerImplicitSearchParam(
308                        RuntimeSearchParamCache searchParams,
309                        String url,
310                        String code,
311                        String description,
312                        String path,
313                        RestSearchParameterTypeEnum type) {
314                if (searchParams.getByUrl(url) == null) {
315                        RuntimeSearchParam sp = new RuntimeSearchParam(
316                                        myFhirContext.getVersion().newIdType(PARAM_LANGUAGE_ID),
317                                        url,
318                                        code,
319                                        description,
320                                        path,
321                                        type,
322                                        Collections.emptySet(),
323                                        Collections.emptySet(),
324                                        RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE,
325                                        myFhirContext.getResourceTypes());
326                        for (String baseResourceType : sp.getBase()) {
327                                searchParams.add(baseResourceType, sp.getName(), sp);
328                        }
329                }
330        }
331
332        private void updateSearchParameterIdentityCache() {
333                if (!myPrePopulateSearchParamIdentities) {
334                        return;
335                }
336
337                ISearchParamIdentityCacheSvc spIdentityCacheSvc = mySearchParamIdentityCacheSvcProvider.getIfAvailable();
338                if (spIdentityCacheSvc == null) {
339                        return;
340                }
341
342                myJpaSearchParamCache
343                                .getHashIdentityToIndexedSearchParamMap()
344                                .forEach((hash, param) -> spIdentityCacheSvc.findOrCreateSearchParamIdentity(
345                                                hash, param.getResourceType(), param.getParameterName()));
346        }
347
348        @VisibleForTesting
349        public Map<Long, IndexedSearchParam> getHashIdentityToIndexedSearchParamMap() {
350                return myJpaSearchParamCache.getHashIdentityToIndexedSearchParamMap();
351        }
352
353        @VisibleForTesting
354        public void setFhirContext(FhirContext theFhirContext) {
355                myFhirContext = theFhirContext;
356        }
357
358        private ReadOnlySearchParamCache getBuiltInSearchParams() {
359                if (myBuiltInSearchParams == null) {
360                        if (myStorageSettings.isAutoSupportDefaultSearchParams()) {
361                                myBuiltInSearchParams =
362                                                ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer);
363                        } else {
364                                // Only the built-in search params that can not be disabled will be supported automatically
365                                myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(
366                                                myFhirContext, mySearchParameterCanonicalizer, NON_DISABLEABLE_SEARCH_PARAMS);
367                        }
368                }
369                return myBuiltInSearchParams;
370        }
371
372        private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) {
373                for (String resourceName : theSearchParams.getResourceNameKeys()) {
374                        ResourceSearchParams resourceSearchParams = theSearchParams.getSearchParamMap(resourceName);
375                        resourceSearchParams.removeInactive();
376                }
377        }
378
379        @VisibleForTesting
380        public void setStorageSettings(StorageSettings theStorageSettings) {
381                myStorageSettings = theStorageSettings;
382        }
383
384        private long overrideBuiltinSearchParamsWithActiveJpaSearchParams(
385                        RuntimeSearchParamCache theSearchParamCache, Collection<IBaseResource> theSearchParams) {
386                if (!myStorageSettings.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) {
387                        return 0;
388                }
389
390                long retval = 0;
391                for (IBaseResource searchParam : theSearchParams) {
392                        retval += overrideSearchParam(theSearchParamCache, searchParam);
393                }
394                return retval;
395        }
396
397        /**
398         * For the given SearchParameter which was fetched from the database, look for any
399         * existing search parameters in the cache that should be replaced by the SP (i.e.
400         * because they represent the same parameter)
401         *
402         * @param theSearchParams The cache to populate
403         * @param theSearchParameter The SearchParameter to insert into the cache and potentially replace existing params
404         */
405        private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) {
406                if (theSearchParameter == null) {
407                        return 0;
408                }
409
410                RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter);
411                if (runtimeSp == null) {
412                        return 0;
413                }
414
415                /*
416                 * This check means that we basically ignore SPs from the database if they have a status
417                 * of "draft". I don't know that this makes sense, but it has worked this way for a long
418                 * time and changing it could potentially screw with people who didn't realize they
419                 * were depending on this behaviour? I don't know.. Honestly this is probably being
420                 * overly cautious. -JA
421                 */
422                if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
423                        return 0;
424                }
425
426                /*
427                 * If an SP in the cache has the same URL as the one we are inserting, first remove
428                 * the old SP from anywhere it is registered. This helps us override SPs like _content
429                 * and _text.
430                 */
431                String url = runtimeSp.getUri();
432                RuntimeSearchParam existingParam = theSearchParams.getByUrl(url);
433                if (existingParam != null) {
434                        if (isNotBlank(existingParam.getName()) && !existingParam.getName().equals(runtimeSp.getName())) {
435                                ourLog.warn(
436                                                "Existing SearchParameter with URL[{}] and name[{}] doesn't match name[{}] found on SearchParameter: {}",
437                                                url,
438                                                existingParam.getName(),
439                                                runtimeSp.getName(),
440                                                runtimeSp.getId());
441                        } else {
442                                Set<String> expandedBases = expandBaseList(existingParam.getBase());
443                                for (String base : expandedBases) {
444                                        theSearchParams.remove(base, existingParam.getName());
445                                }
446                        }
447                }
448
449                long retval = 0;
450                for (String nextBaseName :
451                                expandBaseList(SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter))) {
452                        String name = runtimeSp.getName();
453                        theSearchParams.add(nextBaseName, name, runtimeSp);
454                        ourLog.debug(
455                                        "Adding search parameter {}.{} to SearchParamRegistry",
456                                        nextBaseName,
457                                        Objects.toString(name, "[composite]"));
458                        retval++;
459                }
460                return retval;
461        }
462
463        private @Nonnull Set<String> expandBaseList(Collection<String> nextBase) {
464                Set<String> expandedBases = new HashSet<>();
465                for (String base : nextBase) {
466                        if ("Resource".equals(base) || "DomainResource".equals(base)) {
467                                expandedBases.addAll(myFhirContext.getResourceTypes());
468                                break;
469                        } else {
470                                expandedBases.add(base);
471                        }
472                }
473                return expandedBases;
474        }
475
476        @Override
477        public void requestRefresh() {
478                myResourceChangeListenerCache.requestRefresh();
479        }
480
481        @Override
482        public void forceRefresh() {
483                RuntimeSearchParamCache activeSearchParams = myActiveSearchParams;
484                myResourceChangeListenerCache.forceRefresh();
485
486                // If the refresh didn't trigger a change, proceed with one anyway
487                if (myActiveSearchParams == activeSearchParams) {
488                        rebuildActiveSearchParams();
489                }
490        }
491
492        @Override
493        public ResourceChangeResult refreshCacheIfNecessary() {
494                return myResourceChangeListenerCache.refreshCacheIfNecessary();
495        }
496
497        @VisibleForTesting
498        public void setResourceChangeListenerRegistry(IResourceChangeListenerRegistry theResourceChangeListenerRegistry) {
499                myResourceChangeListenerRegistry = theResourceChangeListenerRegistry;
500        }
501
502        /**
503         * There is a circular reference between this class and the ResourceChangeListenerRegistry:
504         * SearchParamRegistryImpl -> ResourceChangeListenerRegistry -> InMemoryResourceMatcher -> SearchParamRegistryImpl. Since we only need this once on boot-up, we delay
505         * until ContextRefreshedEvent.
506         */
507        @PostConstruct
508        public void registerListener() {
509                RequestPartitionId requestPartitionId = RequestPartitionId.defaultPartition(myPartitionSettings);
510
511                SearchParameterMap spMap = SearchParameterMap.newSynchronous();
512                spMap.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
513
514                myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(
515                                "SearchParameter", requestPartitionId, spMap, this, REFRESH_INTERVAL);
516        }
517
518        @PreDestroy
519        public void unregisterListener() {
520                myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
521        }
522
523        public ReadOnlySearchParamCache getActiveSearchParams() {
524                requiresActiveSearchParams();
525                if (myActiveSearchParams == null) {
526                        throw new IllegalStateException(Msg.code(511) + "SearchParamRegistry has not been initialized");
527                }
528                return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams);
529        }
530
531        @VisibleForTesting
532        public void setActiveSearchParams(RuntimeSearchParamCache theSearchParams) {
533                myActiveSearchParams = theSearchParams;
534        }
535
536        /**
537         * All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder.
538         *
539         * @since 5.1.0
540         */
541        @Override
542        public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
543                myPhoneticEncoder = thePhoneticEncoder;
544
545                if (myActiveSearchParams == null) {
546                        return;
547                }
548                myActiveSearchParams
549                                .getSearchParamStream()
550                                .forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam));
551        }
552
553        @Override
554        public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
555                if (theResourceChangeEvent.isEmpty()) {
556                        return;
557                }
558
559                ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent);
560                if (result.created > 0) {
561                        ourLog.info(
562                                        "Adding {} search parameters to SearchParamRegistry: {}",
563                                        result.created,
564                                        unqualified(theResourceChangeEvent.getCreatedResourceIds()));
565                }
566                if (result.updated > 0) {
567                        ourLog.info(
568                                        "Updating {} search parameters in SearchParamRegistry: {}",
569                                        result.updated,
570                                        unqualified(theResourceChangeEvent.getUpdatedResourceIds()));
571                }
572                if (result.deleted > 0) {
573                        ourLog.info(
574                                        "Deleting {} search parameters from SearchParamRegistry: {}",
575                                        result.deleted,
576                                        unqualified(theResourceChangeEvent.getDeletedResourceIds()));
577                }
578                rebuildActiveSearchParams();
579        }
580
581        private String unqualified(List<IIdType> theIds) {
582                Iterator<String> unqualifiedIds = theIds.stream()
583                                .map(IIdType::toUnqualifiedVersionless)
584                                .map(IIdType::getValue)
585                                .iterator();
586
587                return StringUtils.join(unqualifiedIds, ", ");
588        }
589
590        @Override
591        public void handleInit(Collection<IIdType> theResourceIds) {
592                List<IBaseResource> searchParams = new ArrayList<>();
593                for (IIdType id : theResourceIds) {
594                        try {
595                                IBaseResource searchParam = mySearchParamProvider.read(id);
596                                searchParams.add(searchParam);
597                        } catch (ResourceNotFoundException e) {
598                                ourLog.warn("SearchParameter {} not found.  Excluding from list of active search params.", id);
599                        }
600                }
601                initializeActiveSearchParams(searchParams);
602        }
603
604        @Override
605        public boolean isInitialized() {
606                return myActiveSearchParams != null;
607        }
608
609        @VisibleForTesting
610        public void resetForUnitTest() {
611                myBuiltInSearchParams = null;
612                setActiveSearchParams(null);
613                handleInit(Collections.emptyList());
614        }
615
616        @VisibleForTesting
617        public void setSearchParameterCanonicalizerForUnitTest(
618                        SearchParameterCanonicalizer theSearchParameterCanonicalizerForUnitTest) {
619                mySearchParameterCanonicalizer = theSearchParameterCanonicalizerForUnitTest;
620        }
621
622        public void setInterceptorServiceForUnitTest(IInterceptorService theInterceptorService) {
623                myInterceptorService = theInterceptorService;
624        }
625
626        private static List<RuntimeSearchParam> filteredForContext(
627                        List<RuntimeSearchParam> theActiveComboSearchParams, SearchParamLookupContextEnum theContext) {
628                return theActiveComboSearchParams.stream()
629                                .filter(t -> isAllowedForContext(t, theContext))
630                                .collect(Collectors.toList());
631        }
632}