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}