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