
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 return getActiveComboSearchParams(theResourceName).stream() 098 .filter(param -> Objects.equals(theParamType, param.getComboSearchParamType())) 099 .collect(Collectors.toList()); 100 } 101 102 public Optional<RuntimeSearchParam> getActiveComboSearchParamById(String theResourceName, IIdType theId) { 103 IIdType idToFind = theId.toUnqualifiedVersionless(); 104 return getActiveComboSearchParams(theResourceName).stream() 105 .filter((param) -> Objects.equals(idToFind, param.getIdUnqualifiedVersionless())) 106 .findFirst(); 107 } 108 109 public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) { 110 Map<Set<String>, List<RuntimeSearchParam>> paramNamesToParams = 111 myActiveParamNamesToComboSearchParams.get(theResourceName); 112 if (paramNamesToParams == null) { 113 return Collections.emptyList(); 114 } 115 116 List<RuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames); 117 if (retVal == null) { 118 retVal = Collections.emptyList(); 119 } 120 return Collections.unmodifiableList(retVal); 121 } 122 123 public Map<Long, IndexedSearchParam> getHashIdentityToIndexedSearchParamMap() { 124 return myHashIdentityToIndexedSearchParams; 125 } 126 127 void populateActiveSearchParams( 128 IInterceptorService theInterceptorBroadcaster, 129 IPhoneticEncoder theDefaultPhoneticEncoder, 130 RuntimeSearchParamCache theActiveSearchParams) { 131 Map<String, List<RuntimeSearchParam>> resourceNameToComboSearchParams = new HashMap<>(); 132 Map<String, Map<Set<String>, List<RuntimeSearchParam>>> activeParamNamesToComboSearchParams = new HashMap<>(); 133 134 Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>(); 135 List<RuntimeSearchParam> jpaSearchParams = new ArrayList<>(); 136 Map<Long, IndexedSearchParam> hashIdentityToIndexedSearchParams = new HashMap<>(); 137 138 /* 139 * Loop through parameters and find JPA params 140 */ 141 for (String theResourceName : theActiveSearchParams.getResourceNameKeys()) { 142 ResourceSearchParams searchParams = theActiveSearchParams.getSearchParamMap(theResourceName); 143 List<RuntimeSearchParam> comboSearchParams = 144 resourceNameToComboSearchParams.computeIfAbsent(theResourceName, k -> new ArrayList<>()); 145 Collection<RuntimeSearchParam> nextSearchParamsForResourceName = searchParams.values(); 146 147 ourLog.trace("Resource {} has {} params", theResourceName, searchParams.size()); 148 149 for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) { 150 151 ourLog.trace( 152 "Resource {} has parameter {} with ID {}", 153 theResourceName, 154 nextCandidate.getName(), 155 nextCandidate.getId()); 156 157 if (nextCandidate.getId() != null) { 158 idToRuntimeSearchParam.put( 159 nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate); 160 } 161 if (isNotBlank(nextCandidate.getUri())) { 162 idToRuntimeSearchParam.put(nextCandidate.getUri(), nextCandidate); 163 } 164 165 jpaSearchParams.add(nextCandidate); 166 if (nextCandidate.getComboSearchParamType() != null) { 167 comboSearchParams.add(nextCandidate); 168 } 169 170 setPhoneticEncoder(theDefaultPhoneticEncoder, nextCandidate); 171 populateIndexedSearchParams(theResourceName, nextCandidate, hashIdentityToIndexedSearchParams); 172 } 173 } 174 175 ourLog.trace("Have {} search params loaded", idToRuntimeSearchParam.size()); 176 177 Set<String> haveSeen = new HashSet<>(); 178 for (RuntimeSearchParam next : jpaSearchParams) { 179 if (next.getId() != null 180 && !haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) { 181 continue; 182 } 183 184 Set<String> paramNames = new TreeSet<>(); 185 for (RuntimeSearchParam.Component nextComponent : next.getComponents()) { 186 String nextRef = nextComponent.getReference(); 187 RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef); 188 if (componentTarget != null) { 189 paramNames.add(componentTarget.getName()); 190 } else { 191 String message = "Search parameter " + next + " refers to unknown component " + nextRef 192 + ", ignoring this parameter"; 193 ourLog.warn(message); 194 195 // Interceptor broadcast: JPA_PERFTRACE_WARNING 196 HookParams params = new HookParams() 197 .add(RequestDetails.class, null) 198 .add(ServletRequestDetails.class, null) 199 .add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message)); 200 theInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params); 201 } 202 } 203 204 if (next.getComboSearchParamType() != null) { 205 for (String nextBase : next.getBase()) { 206 activeParamNamesToComboSearchParams.computeIfAbsent(nextBase, v -> new HashMap<>()); 207 activeParamNamesToComboSearchParams 208 .get(nextBase) 209 .computeIfAbsent(paramNames, t -> new ArrayList<>()); 210 activeParamNamesToComboSearchParams 211 .get(nextBase) 212 .get(paramNames) 213 .add(next); 214 } 215 } 216 } 217 218 ourLog.debug("Have {} unique search params", activeParamNamesToComboSearchParams.size()); 219 220 myActiveComboSearchParams = resourceNameToComboSearchParams; 221 myActiveParamNamesToComboSearchParams = activeParamNamesToComboSearchParams; 222 myHashIdentityToIndexedSearchParams = hashIdentityToIndexedSearchParams; 223 } 224 225 void setPhoneticEncoder(IPhoneticEncoder theDefaultPhoneticEncoder, RuntimeSearchParam searchParam) { 226 if ("phonetic".equals(searchParam.getName())) { 227 ourLog.debug( 228 "Setting search param {} on {} phonetic encoder to {}", 229 searchParam.getName(), 230 searchParam.getPath(), 231 theDefaultPhoneticEncoder == null ? "null" : theDefaultPhoneticEncoder.name()); 232 searchParam.setPhoneticEncoder(theDefaultPhoneticEncoder); 233 } 234 } 235 236 private void populateIndexedSearchParams( 237 String theResourceName, 238 RuntimeSearchParam theRuntimeSearchParam, 239 Map<Long, IndexedSearchParam> theHashIdentityToIndexedSearchParams) { 240 241 if (SUPPORTED_INDEXED_SEARCH_PARAMS.contains(theRuntimeSearchParam.getParamType())) { 242 addIndexedSearchParam( 243 theResourceName, theHashIdentityToIndexedSearchParams, theRuntimeSearchParam.getName()); 244 // handle token search parameters with :of-type modifier 245 if (theRuntimeSearchParam.getParamType() == TOKEN) { 246 addIndexedSearchParam( 247 theResourceName, 248 theHashIdentityToIndexedSearchParams, 249 theRuntimeSearchParam.getName() + Constants.PARAMQUALIFIER_TOKEN_OF_TYPE); 250 } 251 // handle Uplifted Ref Chain Search Parameters 252 theRuntimeSearchParam.getUpliftRefchainCodes().stream() 253 .map(urCode -> String.format("%s.%s", theRuntimeSearchParam.getName(), urCode)) 254 .forEach(urSpName -> 255 addIndexedSearchParam(theResourceName, theHashIdentityToIndexedSearchParams, urSpName)); 256 } 257 } 258 259 private void addIndexedSearchParam( 260 String theResourceName, 261 Map<Long, IndexedSearchParam> theHashIdentityToIndexedSearchParams, 262 String theSpName) { 263 Long hashIdentity = SearchParamHash.hashSearchParam( 264 myPartitionSettings, RequestPartitionId.defaultPartition(), theResourceName, theSpName); 265 theHashIdentityToIndexedSearchParams.put(hashIdentity, new IndexedSearchParam(theSpName, theResourceName)); 266 } 267}