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