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