001package ca.uhn.fhir.jpa.searchparam.registry;
002
003/*
004 * #%L
005 * HAPI FHIR Search Parameters
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 * http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
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.ResourceChangeResult;
033import ca.uhn.fhir.jpa.model.entity.ModelConfig;
034import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
035import ca.uhn.fhir.rest.api.server.IBundleProvider;
036import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
037import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
038import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
039import ca.uhn.fhir.util.SearchParameterUtil;
040import ca.uhn.fhir.util.StopWatch;
041import com.google.common.annotations.VisibleForTesting;
042import com.google.common.collect.Sets;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.lang3.time.DateUtils;
045import org.hl7.fhir.instance.model.api.IBaseResource;
046import org.hl7.fhir.instance.model.api.IIdType;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049import org.springframework.beans.factory.annotation.Autowired;
050
051import javax.annotation.Nonnull;
052import javax.annotation.Nullable;
053import javax.annotation.PostConstruct;
054import javax.annotation.PreDestroy;
055import java.util.ArrayList;
056import java.util.Collection;
057import java.util.Collections;
058import java.util.Iterator;
059import java.util.List;
060import java.util.Set;
061
062import static org.apache.commons.lang3.StringUtils.isBlank;
063
064public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceChangeListener, ISearchParamRegistryController {
065
066        public static final Set<String> NON_DISABLEABLE_SEARCH_PARAMS = Collections.unmodifiableSet(Sets.newHashSet(
067                "*:url",
068                "Subscription:*",
069                "SearchParameter:*"
070        ));
071
072        private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class);
073        private static final int MAX_MANAGED_PARAM_COUNT = 10000;
074        private static final long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE;
075
076        private final JpaSearchParamCache myJpaSearchParamCache = new JpaSearchParamCache();
077        @Autowired
078        private ModelConfig myModelConfig;
079        @Autowired
080        private ISearchParamProvider mySearchParamProvider;
081        @Autowired
082        private FhirContext myFhirContext;
083        @Autowired
084        private SearchParameterCanonicalizer mySearchParameterCanonicalizer;
085        @Autowired
086        private IInterceptorService myInterceptorBroadcaster;
087        @Autowired
088        private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
089
090        private IResourceChangeListenerCache myResourceChangeListenerCache;
091        private volatile ReadOnlySearchParamCache myBuiltInSearchParams;
092        private volatile IPhoneticEncoder myPhoneticEncoder;
093        private volatile RuntimeSearchParamCache myActiveSearchParams;
094
095        /**
096         * Constructor
097         */
098        public SearchParamRegistryImpl() {
099                super();
100        }
101
102        @Override
103        public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
104                requiresActiveSearchParams();
105
106                // Can still be null in unit test scenarios
107                if (myActiveSearchParams != null) {
108                        return myActiveSearchParams.get(theResourceName, theParamName);
109                } else {
110                        return null;
111                }
112        }
113
114        @Nonnull
115        @Override
116        public ResourceSearchParams getActiveSearchParams(String theResourceName) {
117                requiresActiveSearchParams();
118                return getActiveSearchParams().getSearchParamMap(theResourceName);
119        }
120
121        private void requiresActiveSearchParams() {
122                if (myActiveSearchParams == null) {
123                        myResourceChangeListenerCache.forceRefresh();
124                }
125        }
126
127        @Override
128        public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName) {
129                return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName);
130        }
131
132        @Override
133        public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) {
134                return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamNames);
135        }
136
137        @Nullable
138        @Override
139        public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) {
140                if (myActiveSearchParams != null) {
141                        return myActiveSearchParams.getByUrl(theUrl);
142                } else {
143                        return null;
144                }
145        }
146
147        private void rebuildActiveSearchParams() {
148                ourLog.info("Rebuilding SearchParamRegistry");
149                SearchParameterMap params = new SearchParameterMap();
150                params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
151
152                IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
153
154                List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, MAX_MANAGED_PARAM_COUNT);
155                int size = allSearchParamsBp.sizeOrThrowNpe();
156
157                ourLog.trace("Loaded {} search params from the DB", size);
158
159                // Just in case..
160                if (size >= MAX_MANAGED_PARAM_COUNT) {
161                        ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
162                }
163
164                initializeActiveSearchParams(allSearchParams);
165        }
166
167        private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearchParams) {
168                StopWatch sw = new StopWatch();
169
170                ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams();
171                RuntimeSearchParamCache searchParams = RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams);
172                long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams);
173                ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
174                removeInactiveSearchParams(searchParams);
175                myActiveSearchParams = searchParams;
176
177                myJpaSearchParamCache.populateActiveSearchParams(myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
178                ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
179        }
180
181        @VisibleForTesting
182        public void setFhirContext(FhirContext theFhirContext) {
183                myFhirContext = theFhirContext;
184        }
185
186        private ReadOnlySearchParamCache getBuiltInSearchParams() {
187                if (myBuiltInSearchParams == null) {
188                        if (myModelConfig.isAutoSupportDefaultSearchParams()) {
189                                myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer);
190                        } else {
191                                // Only the built-in search params that can not be disabled will be supported automatically
192                                myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer, NON_DISABLEABLE_SEARCH_PARAMS);
193                        }
194                }
195                return myBuiltInSearchParams;
196        }
197
198        private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) {
199                for (String resourceName : theSearchParams.getResourceNameKeys()) {
200                        ResourceSearchParams resourceSearchParams = theSearchParams.getSearchParamMap(resourceName);
201                        resourceSearchParams.removeInactive();
202                }
203        }
204
205        @VisibleForTesting
206        public void setModelConfig(ModelConfig theModelConfig) {
207                myModelConfig = theModelConfig;
208        }
209
210        private long overrideBuiltinSearchParamsWithActiveJpaSearchParams(RuntimeSearchParamCache theSearchParamCache, Collection<IBaseResource> theSearchParams) {
211                if (!myModelConfig.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) {
212                        return 0;
213                }
214
215                long retval = 0;
216                for (IBaseResource searchParam : theSearchParams) {
217                        retval += overrideSearchParam(theSearchParamCache, searchParam);
218                }
219                return retval;
220        }
221
222        private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) {
223                if (theSearchParameter == null) {
224                        return 0;
225                }
226
227                RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter);
228                if (runtimeSp == null) {
229                        return 0;
230                }
231                if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
232                        return 0;
233                }
234
235                long retval = 0;
236                for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter)) {
237                        if (isBlank(nextBaseName)) {
238                                continue;
239                        }
240
241                        String name = runtimeSp.getName();
242
243                        theSearchParams.add(nextBaseName, name, runtimeSp);
244                        ourLog.debug("Adding search parameter {}.{} to SearchParamRegistry", nextBaseName, StringUtils.defaultString(name, "[composite]"));
245                        retval++;
246                }
247                return retval;
248        }
249
250        @Override
251        public void requestRefresh() {
252                myResourceChangeListenerCache.requestRefresh();
253        }
254
255        @Override
256        public void forceRefresh() {
257                myResourceChangeListenerCache.forceRefresh();
258        }
259
260        @Override
261        public ResourceChangeResult refreshCacheIfNecessary() {
262                return myResourceChangeListenerCache.refreshCacheIfNecessary();
263        }
264
265        @VisibleForTesting
266        public void setResourceChangeListenerRegistry(IResourceChangeListenerRegistry theResourceChangeListenerRegistry) {
267                myResourceChangeListenerRegistry = theResourceChangeListenerRegistry;
268        }
269
270
271        /**
272         *
273         * There is a circular reference between this class and the ResourceChangeListenerRegistry:
274         * SearchParamRegistryImpl -> ResourceChangeListenerRegistry -> InMemoryResourceMatcher -> SearchParamRegistryImpl. Sicne we only need this once on boot-up, we delay
275         * until ContextRefreshedEvent.
276         *
277         */
278        @PostConstruct
279        public void registerListener() {
280                myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("SearchParameter", SearchParameterMap.newSynchronous(), this, REFRESH_INTERVAL);
281        }
282
283        @PreDestroy
284        public void unregisterListener() {
285                myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
286        }
287
288        public ReadOnlySearchParamCache getActiveSearchParams() {
289                requiresActiveSearchParams();
290                if (myActiveSearchParams == null) {
291                        throw new IllegalStateException(Msg.code(511) + "SearchParamRegistry has not been initialized");
292                }
293                return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams);
294        }
295
296        /**
297         * All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder.
298         *
299         * @since 5.1.0
300         */
301        @Override
302        public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
303                myPhoneticEncoder = thePhoneticEncoder;
304
305                if (myActiveSearchParams == null) {
306                        return;
307                }
308                myActiveSearchParams.getSearchParamStream().forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam));
309        }
310
311        @Override
312        public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
313                if (theResourceChangeEvent.isEmpty()) {
314                        return;
315                }
316
317                ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent);
318                if (result.created > 0) {
319                        ourLog.info("Adding {} search parameters to SearchParamRegistry: {}", result.created, unqualified(theResourceChangeEvent.getCreatedResourceIds()));
320                }
321                if (result.updated > 0) {
322                        ourLog.info("Updating {} search parameters in SearchParamRegistry: {}", result.updated, unqualified(theResourceChangeEvent.getUpdatedResourceIds()));
323                }
324                if (result.deleted > 0) {
325                        ourLog.info("Deleting {} search parameters from SearchParamRegistry: {}", result.deleted, unqualified(theResourceChangeEvent.getDeletedResourceIds()));
326                }
327                rebuildActiveSearchParams();
328        }
329
330        private String unqualified(List<IIdType> theIds) {
331                Iterator<String> unqualifiedIds = theIds.stream()
332                        .map(IIdType::toUnqualifiedVersionless)
333                        .map(IIdType::getValue)
334                        .iterator();
335
336                return StringUtils.join(unqualifiedIds, ", ");
337        }
338
339        @Override
340        public void handleInit(Collection<IIdType> theResourceIds) {
341                List<IBaseResource> searchParams = new ArrayList<>();
342                for (IIdType id : theResourceIds) {
343                        try {
344                                IBaseResource searchParam = mySearchParamProvider.read(id);
345                                searchParams.add(searchParam);
346                        } catch (ResourceNotFoundException e) {
347                                ourLog.warn("SearchParameter {} not found.  Excluding from list of active search params.", id);
348                        }
349                }
350                initializeActiveSearchParams(searchParams);
351        }
352
353        @VisibleForTesting
354        public void resetForUnitTest() {
355                myBuiltInSearchParams = null;
356                handleInit(Collections.emptyList());
357        }
358
359        @VisibleForTesting
360        public void setSearchParameterCanonicalizerForUnitTest(SearchParameterCanonicalizer theSearchParameterCanonicalizerForUnitTest) {
361                mySearchParameterCanonicalizer = theSearchParameterCanonicalizerForUnitTest;
362        }
363
364}