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