
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}