
001/*- 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2025 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.RuntimeSearchParam; 024import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; 025import ca.uhn.fhir.interceptor.api.HookParams; 026import ca.uhn.fhir.interceptor.api.IInterceptorService; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.interceptor.model.RequestPartitionId; 029import ca.uhn.fhir.jpa.model.config.PartitionSettings; 030import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 031import ca.uhn.fhir.jpa.model.util.SearchParamHash; 032import ca.uhn.fhir.rest.api.Constants; 033import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 034import ca.uhn.fhir.rest.api.server.RequestDetails; 035import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 036import ca.uhn.fhir.rest.server.util.IndexedSearchParam; 037import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 038import jakarta.annotation.Nonnull; 039import org.hl7.fhir.instance.model.api.IIdType; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.Collections; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.List; 049import java.util.Map; 050import java.util.Objects; 051import java.util.Optional; 052import java.util.Set; 053import java.util.TreeSet; 054import java.util.stream.Collectors; 055 056import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.DATE; 057import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.NUMBER; 058import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.QUANTITY; 059import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.REFERENCE; 060import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.SPECIAL; 061import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.STRING; 062import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.TOKEN; 063import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.URI; 064import static org.apache.commons.lang3.StringUtils.isNotBlank; 065 066public class JpaSearchParamCache { 067 private static final Logger ourLog = LoggerFactory.getLogger(JpaSearchParamCache.class); 068 069 private static final List<RestSearchParameterTypeEnum> SUPPORTED_INDEXED_SEARCH_PARAMS = 070 List.of(SPECIAL, DATE, NUMBER, QUANTITY, STRING, TOKEN, URI, REFERENCE); 071 072 volatile Map<String, List<RuntimeSearchParam>> myActiveComboSearchParams = Collections.emptyMap(); 073 volatile Map<String, Map<Set<String>, List<RuntimeSearchParam>>> myActiveParamNamesToComboSearchParams = 074 Collections.emptyMap(); 075 volatile Map<Long, IndexedSearchParam> myHashIdentityToIndexedSearchParams = Collections.emptyMap(); 076 077 private final PartitionSettings myPartitionSettings; 078 079 /** 080 * Constructor 081 */ 082 public JpaSearchParamCache(@Nonnull PartitionSettings thePartitionSettings) { 083 assert thePartitionSettings != null; 084 myPartitionSettings = thePartitionSettings; 085 } 086 087 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName) { 088 List<RuntimeSearchParam> retval = myActiveComboSearchParams.get(theResourceName); 089 if (retval == null) { 090 retval = Collections.emptyList(); 091 } 092 return retval; 093 } 094 095 public List<RuntimeSearchParam> getActiveComboSearchParams( 096 String theResourceName, ComboSearchParamType theParamType) { 097 List<RuntimeSearchParam> activeComboSearchParams = getActiveComboSearchParams(theResourceName); 098 List<RuntimeSearchParam> retVal = activeComboSearchParams.stream() 099 .filter(param -> Objects.equals(theParamType, param.getComboSearchParamType())) 100 .collect(Collectors.toList()); 101 return retVal; 102 } 103 104 public Optional<RuntimeSearchParam> getActiveComboSearchParamById(String theResourceName, IIdType theId) { 105 IIdType idToFind = theId.toUnqualifiedVersionless(); 106 return getActiveComboSearchParams(theResourceName).stream() 107 .filter((param) -> Objects.equals(idToFind, param.getIdUnqualifiedVersionless())) 108 .findFirst(); 109 } 110 111 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) { 112 Map<Set<String>, List<RuntimeSearchParam>> paramNamesToParams = 113 myActiveParamNamesToComboSearchParams.get(theResourceName); 114 if (paramNamesToParams == null) { 115 return Collections.emptyList(); 116 } 117 118 List<RuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames); 119 if (retVal == null) { 120 retVal = Collections.emptyList(); 121 } 122 return Collections.unmodifiableList(retVal); 123 } 124 125 public Map<Long, IndexedSearchParam> getHashIdentityToIndexedSearchParamMap() { 126 return myHashIdentityToIndexedSearchParams; 127 } 128 129 void populateActiveSearchParams( 130 IInterceptorService theInterceptorBroadcaster, 131 IPhoneticEncoder theDefaultPhoneticEncoder, 132 RuntimeSearchParamCache theActiveSearchParams) { 133 Map<String, List<RuntimeSearchParam>> resourceNameToComboSearchParams = new HashMap<>(); 134 Map<String, Map<Set<String>, List<RuntimeSearchParam>>> activeParamNamesToComboSearchParams = new HashMap<>(); 135 136 Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>(); 137 List<RuntimeSearchParam> jpaSearchParams = new ArrayList<>(); 138 Map<Long, IndexedSearchParam> hashIdentityToIndexedSearchParams = new HashMap<>(); 139 140 /* 141 * Loop through parameters and find JPA params 142 */ 143 for (String theResourceName : theActiveSearchParams.getResourceNameKeys()) { 144 ResourceSearchParams searchParams = theActiveSearchParams.getSearchParamMap(theResourceName); 145 List<RuntimeSearchParam> comboSearchParams = 146 resourceNameToComboSearchParams.computeIfAbsent(theResourceName, k -> new ArrayList<>()); 147 Collection<RuntimeSearchParam> nextSearchParamsForResourceName = searchParams.values(); 148 149 ourLog.trace("Resource {} has {} params", theResourceName, searchParams.size()); 150 151 for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) { 152 153 ourLog.trace( 154 "Resource {} has parameter {} with ID {}", 155 theResourceName, 156 nextCandidate.getName(), 157 nextCandidate.getId()); 158 159 if (nextCandidate.getId() != null) { 160 idToRuntimeSearchParam.put( 161 nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate); 162 } 163 if (isNotBlank(nextCandidate.getUri())) { 164 idToRuntimeSearchParam.put(nextCandidate.getUri(), nextCandidate); 165 } 166 167 jpaSearchParams.add(nextCandidate); 168 if (nextCandidate.getComboSearchParamType() != null) { 169 comboSearchParams.add(nextCandidate); 170 } 171 172 setPhoneticEncoder(theDefaultPhoneticEncoder, nextCandidate); 173 populateIndexedSearchParams(theResourceName, nextCandidate, hashIdentityToIndexedSearchParams); 174 } 175 } 176 177 ourLog.trace("Have {} search params loaded", idToRuntimeSearchParam.size()); 178 179 Set<String> haveSeen = new HashSet<>(); 180 for (RuntimeSearchParam next : jpaSearchParams) { 181 if (next.getId() != null 182 && !haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) { 183 continue; 184 } 185 186 Set<String> paramNames = new TreeSet<>(); 187 for (RuntimeSearchParam.Component nextComponent : next.getComponents()) { 188 String nextRef = nextComponent.getReference(); 189 RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef); 190 if (componentTarget != null) { 191 paramNames.add(componentTarget.getName()); 192 } else { 193 String message = "Search parameter " + next + " refers to unknown component " + nextRef 194 + ", ignoring this parameter"; 195 ourLog.warn(message); 196 197 // Interceptor broadcast: JPA_PERFTRACE_WARNING 198 HookParams params = new HookParams() 199 .add(RequestDetails.class, null) 200 .add(ServletRequestDetails.class, null) 201 .add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message)); 202 theInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); 203 } 204 } 205 206 if (next.getComboSearchParamType() != null) { 207 for (String nextBase : next.getBase()) { 208 activeParamNamesToComboSearchParams.computeIfAbsent(nextBase, v -> new HashMap<>()); 209 activeParamNamesToComboSearchParams 210 .get(nextBase) 211 .computeIfAbsent(paramNames, t -> new ArrayList<>()); 212 activeParamNamesToComboSearchParams 213 .get(nextBase) 214 .get(paramNames) 215 .add(next); 216 } 217 } 218 } 219 220 ourLog.debug("Have {} unique search params", activeParamNamesToComboSearchParams.size()); 221 222 myActiveComboSearchParams = resourceNameToComboSearchParams; 223 myActiveParamNamesToComboSearchParams = activeParamNamesToComboSearchParams; 224 myHashIdentityToIndexedSearchParams = hashIdentityToIndexedSearchParams; 225 } 226 227 void setPhoneticEncoder(IPhoneticEncoder theDefaultPhoneticEncoder, RuntimeSearchParam searchParam) { 228 if ("phonetic".equals(searchParam.getName())) { 229 ourLog.debug( 230 "Setting search param {} on {} phonetic encoder to {}", 231 searchParam.getName(), 232 searchParam.getPath(), 233 theDefaultPhoneticEncoder == null ? "null" : theDefaultPhoneticEncoder.name()); 234 searchParam.setPhoneticEncoder(theDefaultPhoneticEncoder); 235 } 236 } 237 238 private void populateIndexedSearchParams( 239 String theResourceName, 240 RuntimeSearchParam theRuntimeSearchParam, 241 Map<Long, IndexedSearchParam> theHashIdentityToIndexedSearchParams) { 242 243 if (SUPPORTED_INDEXED_SEARCH_PARAMS.contains(theRuntimeSearchParam.getParamType())) { 244 addIndexedSearchParam( 245 theResourceName, theHashIdentityToIndexedSearchParams, theRuntimeSearchParam.getName()); 246 // handle token search parameters with :of-type modifier 247 if (theRuntimeSearchParam.getParamType() == TOKEN) { 248 addIndexedSearchParam( 249 theResourceName, 250 theHashIdentityToIndexedSearchParams, 251 theRuntimeSearchParam.getName() + Constants.PARAMQUALIFIER_TOKEN_OF_TYPE); 252 } 253 // handle Uplifted Ref Chain Search Parameters 254 theRuntimeSearchParam.getUpliftRefchainCodes().stream() 255 .map(urCode -> theRuntimeSearchParam.getName() + "." + urCode) 256 .forEach(urSpName -> 257 addIndexedSearchParam(theResourceName, theHashIdentityToIndexedSearchParams, urSpName)); 258 } 259 } 260 261 private void addIndexedSearchParam( 262 String theResourceName, 263 Map<Long, IndexedSearchParam> theHashIdentityToIndexedSearchParams, 264 String theSpName) { 265 Long hashIdentity = SearchParamHash.hashSearchParam( 266 myPartitionSettings, RequestPartitionId.defaultPartition(), theResourceName, theSpName); 267 theHashIdentityToIndexedSearchParams.put(hashIdentity, new IndexedSearchParam(theSpName, theResourceName)); 268 } 269}