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}