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