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}