![](/hapi-fhir/images/logos/raccoon-forwards.png)
001/* 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2024 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.ResourceChangeResult; 033import ca.uhn.fhir.jpa.model.entity.StorageSettings; 034import ca.uhn.fhir.jpa.model.search.ISearchParamHashIdentityRegistry; 035import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 036import ca.uhn.fhir.rest.api.Constants; 037import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 038import ca.uhn.fhir.rest.api.server.IBundleProvider; 039import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 040import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 041import ca.uhn.fhir.rest.server.util.IndexedSearchParam; 042import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 043import ca.uhn.fhir.util.SearchParameterUtil; 044import ca.uhn.fhir.util.StopWatch; 045import com.google.common.annotations.VisibleForTesting; 046import com.google.common.collect.Sets; 047import jakarta.annotation.Nonnull; 048import jakarta.annotation.Nullable; 049import jakarta.annotation.PostConstruct; 050import jakarta.annotation.PreDestroy; 051import org.apache.commons.lang3.StringUtils; 052import org.apache.commons.lang3.time.DateUtils; 053import org.hl7.fhir.instance.model.api.IBaseResource; 054import org.hl7.fhir.instance.model.api.IIdType; 055import org.slf4j.Logger; 056import org.slf4j.LoggerFactory; 057import org.springframework.beans.factory.annotation.Autowired; 058 059import java.util.ArrayList; 060import java.util.Collection; 061import java.util.Collections; 062import java.util.Iterator; 063import java.util.List; 064import java.util.Optional; 065import java.util.Set; 066 067import static org.apache.commons.lang3.StringUtils.isBlank; 068 069public class SearchParamRegistryImpl 070 implements ISearchParamRegistry, 071 IResourceChangeListener, 072 ISearchParamRegistryController, 073 ISearchParamHashIdentityRegistry { 074 075 public static final Set<String> NON_DISABLEABLE_SEARCH_PARAMS = 076 Collections.unmodifiableSet(Sets.newHashSet("*:url", "Subscription:*", "SearchParameter:*")); 077 078 private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class); 079 private static final int MAX_MANAGED_PARAM_COUNT = 10000; 080 private static final long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE; 081 082 private final JpaSearchParamCache myJpaSearchParamCache = new JpaSearchParamCache(); 083 084 @Autowired 085 private StorageSettings myStorageSettings; 086 087 @Autowired 088 private ISearchParamProvider mySearchParamProvider; 089 090 @Autowired 091 private FhirContext myFhirContext; 092 093 @Autowired 094 private SearchParameterCanonicalizer mySearchParameterCanonicalizer; 095 096 @Autowired 097 private IInterceptorService myInterceptorBroadcaster; 098 099 @Autowired 100 private IResourceChangeListenerRegistry myResourceChangeListenerRegistry; 101 102 private IResourceChangeListenerCache myResourceChangeListenerCache; 103 private volatile ReadOnlySearchParamCache myBuiltInSearchParams; 104 private volatile IPhoneticEncoder myPhoneticEncoder; 105 private volatile RuntimeSearchParamCache myActiveSearchParams; 106 107 /** 108 * Constructor 109 */ 110 public SearchParamRegistryImpl() { 111 super(); 112 } 113 114 @Override 115 public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { 116 requiresActiveSearchParams(); 117 118 // Can still be null in unit test scenarios 119 if (myActiveSearchParams != null) { 120 return myActiveSearchParams.get(theResourceName, theParamName); 121 } else { 122 return null; 123 } 124 } 125 126 @Nonnull 127 @Override 128 public ResourceSearchParams getActiveSearchParams(String theResourceName) { 129 requiresActiveSearchParams(); 130 return getActiveSearchParams().getSearchParamMap(theResourceName); 131 } 132 133 private void requiresActiveSearchParams() { 134 if (myActiveSearchParams == null) { 135 // forced refreshes should not use a cache - we're forcibly refrsching it, after all 136 myResourceChangeListenerCache.forceRefresh(); 137 } 138 } 139 140 @Override 141 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName) { 142 return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName); 143 } 144 145 @Override 146 public List<RuntimeSearchParam> getActiveComboSearchParams( 147 String theResourceName, ComboSearchParamType theParamType) { 148 return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamType); 149 } 150 151 @Override 152 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) { 153 return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamNames); 154 } 155 156 @Override 157 public Optional<IndexedSearchParam> getIndexedSearchParamByHashIdentity(Long theHashIdentity) { 158 return myJpaSearchParamCache.getIndexedSearchParamByHashIdentity(theHashIdentity); 159 } 160 161 @Nullable 162 @Override 163 public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) { 164 if (myActiveSearchParams != null) { 165 return myActiveSearchParams.getByUrl(theUrl); 166 } else { 167 return null; 168 } 169 } 170 171 @Override 172 public Optional<RuntimeSearchParam> getActiveComboSearchParamById(String theResourceName, IIdType theId) { 173 return myJpaSearchParamCache.getActiveComboSearchParamById(theResourceName, theId); 174 } 175 176 private void rebuildActiveSearchParams() { 177 ourLog.info("Rebuilding SearchParamRegistry"); 178 SearchParameterMap params = new SearchParameterMap(); 179 params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); 180 params.setCount(MAX_MANAGED_PARAM_COUNT); 181 182 IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params); 183 184 List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, MAX_MANAGED_PARAM_COUNT); 185 Integer size = allSearchParamsBp.size(); 186 187 ourLog.trace("Loaded {} search params from the DB", allSearchParams.size()); 188 189 if (size == null) { 190 ourLog.error( 191 "Only {} search parameters have been loaded, but there are more than that in the repository. Is offset search configured on this server?", 192 allSearchParams.size()); 193 } else if (size >= MAX_MANAGED_PARAM_COUNT) { 194 ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!"); 195 } 196 197 initializeActiveSearchParams(allSearchParams); 198 } 199 200 private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearchParams) { 201 StopWatch sw = new StopWatch(); 202 203 ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams(); 204 RuntimeSearchParamCache searchParams = 205 RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams); 206 long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); 207 ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); 208 removeInactiveSearchParams(searchParams); 209 210 /* 211 * The _language SearchParameter is a weird exception - It is actually just a normal 212 * token SP, but we explcitly ban SPs from registering themselves with a prefix 213 * of "_" since that's system reserved so we put this one behind a settings toggle 214 */ 215 if (myStorageSettings.isLanguageSearchParameterEnabled()) { 216 IIdType id = myFhirContext.getVersion().newIdType(); 217 id.setValue("SearchParameter/Resource-language"); 218 RuntimeSearchParam sp = new RuntimeSearchParam( 219 id, 220 "http://hl7.org/fhir/SearchParameter/Resource-language", 221 Constants.PARAM_LANGUAGE, 222 "Language of the resource content", 223 "language", 224 RestSearchParameterTypeEnum.TOKEN, 225 Collections.emptySet(), 226 Collections.emptySet(), 227 RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, 228 myFhirContext.getResourceTypes()); 229 for (String baseResourceType : sp.getBase()) { 230 searchParams.add(baseResourceType, sp.getName(), sp); 231 } 232 } 233 234 setActiveSearchParams(searchParams); 235 236 myJpaSearchParamCache.populateActiveSearchParams( 237 myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams); 238 ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis()); 239 } 240 241 @VisibleForTesting 242 public void setFhirContext(FhirContext theFhirContext) { 243 myFhirContext = theFhirContext; 244 } 245 246 private ReadOnlySearchParamCache getBuiltInSearchParams() { 247 if (myBuiltInSearchParams == null) { 248 if (myStorageSettings.isAutoSupportDefaultSearchParams()) { 249 myBuiltInSearchParams = 250 ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer); 251 } else { 252 // Only the built-in search params that can not be disabled will be supported automatically 253 myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext( 254 myFhirContext, mySearchParameterCanonicalizer, NON_DISABLEABLE_SEARCH_PARAMS); 255 } 256 } 257 return myBuiltInSearchParams; 258 } 259 260 private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) { 261 for (String resourceName : theSearchParams.getResourceNameKeys()) { 262 ResourceSearchParams resourceSearchParams = theSearchParams.getSearchParamMap(resourceName); 263 resourceSearchParams.removeInactive(); 264 } 265 } 266 267 @VisibleForTesting 268 public void setStorageSettings(StorageSettings theStorageSettings) { 269 myStorageSettings = theStorageSettings; 270 } 271 272 private long overrideBuiltinSearchParamsWithActiveJpaSearchParams( 273 RuntimeSearchParamCache theSearchParamCache, Collection<IBaseResource> theSearchParams) { 274 if (!myStorageSettings.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) { 275 return 0; 276 } 277 278 long retval = 0; 279 for (IBaseResource searchParam : theSearchParams) { 280 retval += overrideSearchParam(theSearchParamCache, searchParam); 281 } 282 return retval; 283 } 284 285 private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) { 286 if (theSearchParameter == null) { 287 return 0; 288 } 289 290 RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter); 291 if (runtimeSp == null) { 292 return 0; 293 } 294 if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) { 295 return 0; 296 } 297 298 long retval = 0; 299 for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter)) { 300 if (isBlank(nextBaseName)) { 301 continue; 302 } 303 304 String name = runtimeSp.getName(); 305 306 theSearchParams.add(nextBaseName, name, runtimeSp); 307 ourLog.debug( 308 "Adding search parameter {}.{} to SearchParamRegistry", 309 nextBaseName, 310 StringUtils.defaultString(name, "[composite]")); 311 retval++; 312 } 313 return retval; 314 } 315 316 @Override 317 public void requestRefresh() { 318 myResourceChangeListenerCache.requestRefresh(); 319 } 320 321 @Override 322 public void forceRefresh() { 323 RuntimeSearchParamCache activeSearchParams = myActiveSearchParams; 324 myResourceChangeListenerCache.forceRefresh(); 325 326 // If the refresh didn't trigger a change, proceed with one anyway 327 if (myActiveSearchParams == activeSearchParams) { 328 rebuildActiveSearchParams(); 329 } 330 } 331 332 @Override 333 public ResourceChangeResult refreshCacheIfNecessary() { 334 return myResourceChangeListenerCache.refreshCacheIfNecessary(); 335 } 336 337 @VisibleForTesting 338 public void setResourceChangeListenerRegistry(IResourceChangeListenerRegistry theResourceChangeListenerRegistry) { 339 myResourceChangeListenerRegistry = theResourceChangeListenerRegistry; 340 } 341 342 /** 343 * There is a circular reference between this class and the ResourceChangeListenerRegistry: 344 * SearchParamRegistryImpl -> ResourceChangeListenerRegistry -> InMemoryResourceMatcher -> SearchParamRegistryImpl. Since we only need this once on boot-up, we delay 345 * until ContextRefreshedEvent. 346 */ 347 @PostConstruct 348 public void registerListener() { 349 SearchParameterMap spMap = SearchParameterMap.newSynchronous(); 350 spMap.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); 351 myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener( 352 "SearchParameter", spMap, this, REFRESH_INTERVAL); 353 } 354 355 @PreDestroy 356 public void unregisterListener() { 357 myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this); 358 } 359 360 public ReadOnlySearchParamCache getActiveSearchParams() { 361 requiresActiveSearchParams(); 362 if (myActiveSearchParams == null) { 363 throw new IllegalStateException(Msg.code(511) + "SearchParamRegistry has not been initialized"); 364 } 365 return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams); 366 } 367 368 /** 369 * All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder. 370 * 371 * @since 5.1.0 372 */ 373 @Override 374 public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) { 375 myPhoneticEncoder = thePhoneticEncoder; 376 377 if (myActiveSearchParams == null) { 378 return; 379 } 380 myActiveSearchParams 381 .getSearchParamStream() 382 .forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam)); 383 } 384 385 @Override 386 public void handleChange(IResourceChangeEvent theResourceChangeEvent) { 387 if (theResourceChangeEvent.isEmpty()) { 388 return; 389 } 390 391 ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent); 392 if (result.created > 0) { 393 ourLog.info( 394 "Adding {} search parameters to SearchParamRegistry: {}", 395 result.created, 396 unqualified(theResourceChangeEvent.getCreatedResourceIds())); 397 } 398 if (result.updated > 0) { 399 ourLog.info( 400 "Updating {} search parameters in SearchParamRegistry: {}", 401 result.updated, 402 unqualified(theResourceChangeEvent.getUpdatedResourceIds())); 403 } 404 if (result.deleted > 0) { 405 ourLog.info( 406 "Deleting {} search parameters from SearchParamRegistry: {}", 407 result.deleted, 408 unqualified(theResourceChangeEvent.getDeletedResourceIds())); 409 } 410 rebuildActiveSearchParams(); 411 } 412 413 private String unqualified(List<IIdType> theIds) { 414 Iterator<String> unqualifiedIds = theIds.stream() 415 .map(IIdType::toUnqualifiedVersionless) 416 .map(IIdType::getValue) 417 .iterator(); 418 419 return StringUtils.join(unqualifiedIds, ", "); 420 } 421 422 @Override 423 public void handleInit(Collection<IIdType> theResourceIds) { 424 List<IBaseResource> searchParams = new ArrayList<>(); 425 for (IIdType id : theResourceIds) { 426 try { 427 IBaseResource searchParam = mySearchParamProvider.read(id); 428 searchParams.add(searchParam); 429 } catch (ResourceNotFoundException e) { 430 ourLog.warn("SearchParameter {} not found. Excluding from list of active search params.", id); 431 } 432 } 433 initializeActiveSearchParams(searchParams); 434 } 435 436 @Override 437 public boolean isInitialized() { 438 return myActiveSearchParams != null; 439 } 440 441 @VisibleForTesting 442 public void resetForUnitTest() { 443 myBuiltInSearchParams = null; 444 setActiveSearchParams(null); 445 handleInit(Collections.emptyList()); 446 } 447 448 @VisibleForTesting 449 public void setSearchParameterCanonicalizerForUnitTest( 450 SearchParameterCanonicalizer theSearchParameterCanonicalizerForUnitTest) { 451 mySearchParameterCanonicalizer = theSearchParameterCanonicalizerForUnitTest; 452 } 453 454 @VisibleForTesting 455 public int getMaxManagedParamCountForUnitTests() { 456 return MAX_MANAGED_PARAM_COUNT; 457 } 458 459 @VisibleForTesting 460 public void setActiveSearchParams(RuntimeSearchParamCache theSearchParams) { 461 myActiveSearchParams = theSearchParams; 462 } 463}