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