
001/* 002 * #%L 003 * HAPI FHIR Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2023 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.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.Optional; 061import java.util.Set; 062 063import static org.apache.commons.lang3.StringUtils.isBlank; 064 065public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceChangeListener, ISearchParamRegistryController { 066 067 public static final Set<String> NON_DISABLEABLE_SEARCH_PARAMS = Collections.unmodifiableSet(Sets.newHashSet( 068 "*:url", 069 "Subscription:*", 070 "SearchParameter:*" 071 )); 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 @Autowired 079 private StorageSettings myStorageSettings; 080 @Autowired 081 private ISearchParamProvider mySearchParamProvider; 082 @Autowired 083 private FhirContext myFhirContext; 084 @Autowired 085 private SearchParameterCanonicalizer mySearchParameterCanonicalizer; 086 @Autowired 087 private IInterceptorService myInterceptorBroadcaster; 088 @Autowired 089 private IResourceChangeListenerRegistry myResourceChangeListenerRegistry; 090 091 private IResourceChangeListenerCache myResourceChangeListenerCache; 092 private volatile ReadOnlySearchParamCache myBuiltInSearchParams; 093 private volatile IPhoneticEncoder myPhoneticEncoder; 094 private volatile RuntimeSearchParamCache myActiveSearchParams; 095 096 /** 097 * Constructor 098 */ 099 public SearchParamRegistryImpl() { 100 super(); 101 } 102 103 @Override 104 public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { 105 requiresActiveSearchParams(); 106 107 // Can still be null in unit test scenarios 108 if (myActiveSearchParams != null) { 109 return myActiveSearchParams.get(theResourceName, theParamName); 110 } else { 111 return null; 112 } 113 } 114 115 @Nonnull 116 @Override 117 public ResourceSearchParams getActiveSearchParams(String theResourceName) { 118 requiresActiveSearchParams(); 119 return getActiveSearchParams().getSearchParamMap(theResourceName); 120 } 121 122 private void requiresActiveSearchParams() { 123 if (myActiveSearchParams == null) { 124 myResourceChangeListenerCache.forceRefresh(); 125 } 126 } 127 128 @Override 129 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName) { 130 return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName); 131 } 132 133 @Override 134 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, ComboSearchParamType theParamType) { 135 return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamType); 136 } 137 138 @Override 139 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) { 140 return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamNames); 141 } 142 143 @Nullable 144 @Override 145 public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) { 146 if (myActiveSearchParams != null) { 147 return myActiveSearchParams.getByUrl(theUrl); 148 } else { 149 return null; 150 } 151 } 152 153 154 @Override 155 public Optional<RuntimeSearchParam> getActiveComboSearchParamById(String theResourceName, IIdType theId) { 156 return myJpaSearchParamCache.getActiveComboSearchParamById(theResourceName, theId); 157 } 158 159 private void rebuildActiveSearchParams() { 160 ourLog.info("Rebuilding SearchParamRegistry"); 161 SearchParameterMap params = new SearchParameterMap(); 162 params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); 163 params.setCount(MAX_MANAGED_PARAM_COUNT); 164 165 IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params); 166 167 List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, MAX_MANAGED_PARAM_COUNT); 168 Integer size = allSearchParamsBp.size(); 169 170 ourLog.trace("Loaded {} search params from the DB", allSearchParams.size()); 171 172 if (size == null) { 173 ourLog.error("Only {} search parameters have been loaded, but there are more than that in the repository. Is offset search configured on this server?", allSearchParams.size()); 174 } else if (size >= MAX_MANAGED_PARAM_COUNT) { 175 ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!"); 176 } 177 178 initializeActiveSearchParams(allSearchParams); 179 } 180 181 private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearchParams) { 182 StopWatch sw = new StopWatch(); 183 184 ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams(); 185 RuntimeSearchParamCache searchParams = RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams); 186 long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); 187 ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); 188 removeInactiveSearchParams(searchParams); 189 myActiveSearchParams = searchParams; 190 191 myJpaSearchParamCache.populateActiveSearchParams(myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams); 192 ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis()); 193 } 194 195 @VisibleForTesting 196 public void setFhirContext(FhirContext theFhirContext) { 197 myFhirContext = theFhirContext; 198 } 199 200 private ReadOnlySearchParamCache getBuiltInSearchParams() { 201 if (myBuiltInSearchParams == null) { 202 if (myStorageSettings.isAutoSupportDefaultSearchParams()) { 203 myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer); 204 } else { 205 // Only the built-in search params that can not be disabled will be supported automatically 206 myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer, NON_DISABLEABLE_SEARCH_PARAMS); 207 } 208 } 209 return myBuiltInSearchParams; 210 } 211 212 private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) { 213 for (String resourceName : theSearchParams.getResourceNameKeys()) { 214 ResourceSearchParams resourceSearchParams = theSearchParams.getSearchParamMap(resourceName); 215 resourceSearchParams.removeInactive(); 216 } 217 } 218 219 @VisibleForTesting 220 public void setStorageSettings(StorageSettings theStorageSettings) { 221 myStorageSettings = theStorageSettings; 222 } 223 224 private long overrideBuiltinSearchParamsWithActiveJpaSearchParams(RuntimeSearchParamCache theSearchParamCache, Collection<IBaseResource> theSearchParams) { 225 if (!myStorageSettings.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) { 226 return 0; 227 } 228 229 long retval = 0; 230 for (IBaseResource searchParam : theSearchParams) { 231 retval += overrideSearchParam(theSearchParamCache, searchParam); 232 } 233 return retval; 234 } 235 236 private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) { 237 if (theSearchParameter == null) { 238 return 0; 239 } 240 241 RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter); 242 if (runtimeSp == null) { 243 return 0; 244 } 245 if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) { 246 return 0; 247 } 248 249 long retval = 0; 250 for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter)) { 251 if (isBlank(nextBaseName)) { 252 continue; 253 } 254 255 String name = runtimeSp.getName(); 256 257 theSearchParams.add(nextBaseName, name, runtimeSp); 258 ourLog.debug("Adding search parameter {}.{} to SearchParamRegistry", nextBaseName, StringUtils.defaultString(name, "[composite]")); 259 retval++; 260 } 261 return retval; 262 } 263 264 @Override 265 public void requestRefresh() { 266 myResourceChangeListenerCache.requestRefresh(); 267 } 268 269 @Override 270 public void forceRefresh() { 271 myResourceChangeListenerCache.forceRefresh(); 272 } 273 274 @Override 275 public ResourceChangeResult refreshCacheIfNecessary() { 276 return myResourceChangeListenerCache.refreshCacheIfNecessary(); 277 } 278 279 @VisibleForTesting 280 public void setResourceChangeListenerRegistry(IResourceChangeListenerRegistry theResourceChangeListenerRegistry) { 281 myResourceChangeListenerRegistry = theResourceChangeListenerRegistry; 282 } 283 284 285 /** 286 * There is a circular reference between this class and the ResourceChangeListenerRegistry: 287 * SearchParamRegistryImpl -> ResourceChangeListenerRegistry -> InMemoryResourceMatcher -> SearchParamRegistryImpl. Sicne we only need this once on boot-up, we delay 288 * until ContextRefreshedEvent. 289 */ 290 @PostConstruct 291 public void registerListener() { 292 myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("SearchParameter", SearchParameterMap.newSynchronous(), this, REFRESH_INTERVAL); 293 } 294 295 @PreDestroy 296 public void unregisterListener() { 297 myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this); 298 } 299 300 public ReadOnlySearchParamCache getActiveSearchParams() { 301 requiresActiveSearchParams(); 302 if (myActiveSearchParams == null) { 303 throw new IllegalStateException(Msg.code(511) + "SearchParamRegistry has not been initialized"); 304 } 305 return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams); 306 } 307 308 /** 309 * All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder. 310 * 311 * @since 5.1.0 312 */ 313 @Override 314 public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) { 315 myPhoneticEncoder = thePhoneticEncoder; 316 317 if (myActiveSearchParams == null) { 318 return; 319 } 320 myActiveSearchParams.getSearchParamStream().forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam)); 321 } 322 323 @Override 324 public void handleChange(IResourceChangeEvent theResourceChangeEvent) { 325 if (theResourceChangeEvent.isEmpty()) { 326 return; 327 } 328 329 ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent); 330 if (result.created > 0) { 331 ourLog.info("Adding {} search parameters to SearchParamRegistry: {}", result.created, unqualified(theResourceChangeEvent.getCreatedResourceIds())); 332 } 333 if (result.updated > 0) { 334 ourLog.info("Updating {} search parameters in SearchParamRegistry: {}", result.updated, unqualified(theResourceChangeEvent.getUpdatedResourceIds())); 335 } 336 if (result.deleted > 0) { 337 ourLog.info("Deleting {} search parameters from SearchParamRegistry: {}", result.deleted, unqualified(theResourceChangeEvent.getDeletedResourceIds())); 338 } 339 rebuildActiveSearchParams(); 340 } 341 342 private String unqualified(List<IIdType> theIds) { 343 Iterator<String> unqualifiedIds = theIds.stream() 344 .map(IIdType::toUnqualifiedVersionless) 345 .map(IIdType::getValue) 346 .iterator(); 347 348 return StringUtils.join(unqualifiedIds, ", "); 349 } 350 351 @Override 352 public void handleInit(Collection<IIdType> theResourceIds) { 353 List<IBaseResource> searchParams = new ArrayList<>(); 354 for (IIdType id : theResourceIds) { 355 try { 356 IBaseResource searchParam = mySearchParamProvider.read(id); 357 searchParams.add(searchParam); 358 } catch (ResourceNotFoundException e) { 359 ourLog.warn("SearchParameter {} not found. Excluding from list of active search params.", id); 360 } 361 } 362 initializeActiveSearchParams(searchParams); 363 } 364 365 @VisibleForTesting 366 public void resetForUnitTest() { 367 myBuiltInSearchParams = null; 368 handleInit(Collections.emptyList()); 369 } 370 371 @VisibleForTesting 372 public void setSearchParameterCanonicalizerForUnitTest(SearchParameterCanonicalizer theSearchParameterCanonicalizerForUnitTest) { 373 mySearchParameterCanonicalizer = theSearchParameterCanonicalizerForUnitTest; 374 } 375}