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;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.context.RuntimeSearchParam;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.model.util.JpaConstants;
028import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
029import ca.uhn.fhir.model.api.IQueryParameterAnd;
030import ca.uhn.fhir.model.api.IQueryParameterType;
031import ca.uhn.fhir.model.api.Include;
032import ca.uhn.fhir.rest.api.Constants;
033import ca.uhn.fhir.rest.api.QualifiedParamList;
034import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
035import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
036import ca.uhn.fhir.rest.param.DateRangeParam;
037import ca.uhn.fhir.rest.param.ParameterUtil;
038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
039import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
040import ca.uhn.fhir.rest.server.util.MatchUrlUtil;
041import ca.uhn.fhir.util.ReflectionUtil;
042import ca.uhn.fhir.util.UrlUtil;
043import com.google.common.collect.ArrayListMultimap;
044import org.apache.http.NameValuePair;
045import org.springframework.beans.factory.annotation.Autowired;
046
047import java.util.List;
048
049import static org.apache.commons.lang3.StringUtils.isBlank;
050import static org.apache.commons.lang3.StringUtils.isNotBlank;
051
052public class MatchUrlService {
053
054        @Autowired
055        private FhirContext myFhirContext;
056
057        @Autowired
058        private ISearchParamRegistry mySearchParamRegistry;
059
060        public SearchParameterMap translateMatchUrl(
061                        String theMatchUrl, RuntimeResourceDefinition theResourceDefinition, Flag... theFlags) {
062                SearchParameterMap paramMap = new SearchParameterMap();
063                List<NameValuePair> parameters = MatchUrlUtil.translateMatchUrl(theMatchUrl);
064
065                ArrayListMultimap<String, QualifiedParamList> nameToParamLists = ArrayListMultimap.create();
066                for (NameValuePair next : parameters) {
067                        if (isBlank(next.getValue())) {
068                                continue;
069                        }
070
071                        String paramName = next.getName();
072                        String qualifier = null;
073                        for (int i = 0; i < paramName.length(); i++) {
074                                switch (paramName.charAt(i)) {
075                                        case '.':
076                                        case ':':
077                                                qualifier = paramName.substring(i);
078                                                paramName = paramName.substring(0, i);
079                                                i = Integer.MAX_VALUE - 1;
080                                                break;
081                                }
082                        }
083
084                        QualifiedParamList paramList =
085                                        QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, next.getValue());
086                        nameToParamLists.put(paramName, paramList);
087                }
088
089                for (String nextParamName : nameToParamLists.keySet()) {
090                        List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
091
092                        if (theFlags != null) {
093                                for (Flag next : theFlags) {
094                                        next.process(nextParamName, paramList, paramMap);
095                                }
096                        }
097
098                        if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
099                                if (!paramList.isEmpty()) {
100                                        if (paramList.size() > 2) {
101                                                throw new InvalidRequestException(Msg.code(484) + "Failed to parse match URL[" + theMatchUrl
102                                                                + "] - Can not have more than 2 " + Constants.PARAM_LASTUPDATED
103                                                                + " parameter repetitions");
104                                        } else {
105                                                DateRangeParam p1 = new DateRangeParam();
106                                                p1.setValuesAsQueryTokens(myFhirContext, nextParamName, paramList);
107                                                paramMap.setLastUpdated(p1);
108                                        }
109                                }
110                        } else if (Constants.PARAM_HAS.equals(nextParamName)) {
111                                IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(
112                                                myFhirContext, RestSearchParameterTypeEnum.HAS, nextParamName, paramList);
113                                paramMap.add(nextParamName, param);
114                        } else if (Constants.PARAM_COUNT.equals(nextParamName)) {
115                                if (!paramList.isEmpty() && !paramList.get(0).isEmpty()) {
116                                        String intString = paramList.get(0).get(0);
117                                        try {
118                                                paramMap.setCount(Integer.parseInt(intString));
119                                        } catch (NumberFormatException e) {
120                                                throw new InvalidRequestException(
121                                                                Msg.code(485) + "Invalid " + Constants.PARAM_COUNT + " value: " + intString);
122                                        }
123                                }
124                        } else if (Constants.PARAM_SEARCH_TOTAL_MODE.equals(nextParamName)) {
125                                if (!paramList.isEmpty() && !paramList.get(0).isEmpty()) {
126                                        String totalModeEnumStr = paramList.get(0).get(0);
127                                        SearchTotalModeEnum searchTotalMode = SearchTotalModeEnum.fromCode(totalModeEnumStr);
128                                        if (searchTotalMode == null) {
129                                                // We had an oops here supporting the UPPER CASE enum instead of the FHIR code for _total.
130                                                // Keep supporting it in case someone is using it.
131                                                try {
132                                                        searchTotalMode = SearchTotalModeEnum.valueOf(totalModeEnumStr);
133                                                } catch (IllegalArgumentException e) {
134                                                        throw new InvalidRequestException(Msg.code(2078) + "Invalid "
135                                                                        + Constants.PARAM_SEARCH_TOTAL_MODE + " value: " + totalModeEnumStr);
136                                                }
137                                        }
138                                        paramMap.setSearchTotalMode(searchTotalMode);
139                                }
140                        } else if (Constants.PARAM_OFFSET.equals(nextParamName)) {
141                                if (!paramList.isEmpty() && !paramList.get(0).isEmpty()) {
142                                        String intString = paramList.get(0).get(0);
143                                        try {
144                                                paramMap.setOffset(Integer.parseInt(intString));
145                                        } catch (NumberFormatException e) {
146                                                throw new InvalidRequestException(
147                                                                Msg.code(486) + "Invalid " + Constants.PARAM_OFFSET + " value: " + intString);
148                                        }
149                                }
150                        } else if (ResourceMetaParams.RESOURCE_META_PARAMS.containsKey(nextParamName)) {
151                                if (isNotBlank(paramList.get(0).getQualifier())
152                                                && paramList.get(0).getQualifier().startsWith(".")) {
153                                        throw new InvalidRequestException(Msg.code(487) + "Invalid parameter chain: " + nextParamName
154                                                        + paramList.get(0).getQualifier());
155                                }
156                                IQueryParameterAnd<?> type = newInstanceAnd(nextParamName);
157                                type.setValuesAsQueryTokens(myFhirContext, nextParamName, (paramList));
158                                paramMap.add(nextParamName, type);
159                        } else if (Constants.PARAM_SOURCE.equals(nextParamName)) {
160                                IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(
161                                                myFhirContext, RestSearchParameterTypeEnum.URI, nextParamName, paramList);
162                                paramMap.add(nextParamName, param);
163                        } else if (JpaConstants.PARAM_DELETE_EXPUNGE.equals(nextParamName)) {
164                                paramMap.setDeleteExpunge(true);
165                        } else if (Constants.PARAM_LIST.equals(nextParamName)) {
166                                IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(
167                                                myFhirContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList);
168                                paramMap.add(nextParamName, param);
169                        } else if (nextParamName.startsWith("_") && !Constants.PARAM_LANGUAGE.equals(nextParamName)) {
170                                // ignore these since they aren't search params (e.g. _sort)
171                        } else {
172                                RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(
173                                                theResourceDefinition.getName(),
174                                                nextParamName,
175                                                ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
176                                if (paramDef == null) {
177                                        throw throwUnrecognizedParamException(theMatchUrl, theResourceDefinition, nextParamName);
178                                }
179
180                                IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(
181                                                mySearchParamRegistry, myFhirContext, paramDef, nextParamName, paramList);
182                                paramMap.add(nextParamName, param);
183                        }
184                }
185                return paramMap;
186        }
187
188        public static class UnrecognizedSearchParameterException extends InvalidRequestException {
189
190                private final String myResourceName;
191                private final String myParamName;
192
193                UnrecognizedSearchParameterException(String theMessage, String theResourceName, String theParamName) {
194                        super(theMessage);
195                        myResourceName = theResourceName;
196                        myParamName = theParamName;
197                }
198
199                public String getResourceName() {
200                        return myResourceName;
201                }
202
203                public String getParamName() {
204                        return myParamName;
205                }
206        }
207
208        private InvalidRequestException throwUnrecognizedParamException(
209                        String theMatchUrl, RuntimeResourceDefinition theResourceDefinition, String nextParamName) {
210                return new UnrecognizedSearchParameterException(
211                                Msg.code(488) + "Failed to parse match URL[" + theMatchUrl + "] - Resource type "
212                                                + theResourceDefinition.getName() + " does not have a parameter with name: " + nextParamName,
213                                theResourceDefinition.getName(),
214                                nextParamName);
215        }
216
217        private IQueryParameterAnd<?> newInstanceAnd(String theParamType) {
218                Class<? extends IQueryParameterAnd<?>> clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(theParamType);
219                return ReflectionUtil.newInstance(clazz);
220        }
221
222        public IQueryParameterType newInstanceType(String theParamType) {
223                Class<? extends IQueryParameterType> clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(theParamType);
224                return ReflectionUtil.newInstance(clazz);
225        }
226
227        public ResourceSearch getResourceSearch(String theUrl, RequestPartitionId theRequestPartitionId) {
228                RuntimeResourceDefinition resourceDefinition;
229                resourceDefinition = UrlUtil.parseUrlResourceType(myFhirContext, theUrl);
230                SearchParameterMap searchParameterMap = translateMatchUrl(theUrl, resourceDefinition);
231                return new ResourceSearch(resourceDefinition, searchParameterMap, theRequestPartitionId);
232        }
233
234        public ResourceSearch getResourceSearch(String theUrl) {
235                return getResourceSearch(theUrl, null);
236        }
237
238        public interface Flag {
239                void process(String theParamName, List<QualifiedParamList> theValues, SearchParameterMap theMapToPopulate);
240        }
241
242        /**
243         * Indicates that the parser should process _include and _revinclude (by default these are not handled)
244         */
245        public static Flag processIncludes() {
246                return (theParamName, theValues, theMapToPopulate) -> {
247                        if (Constants.PARAM_INCLUDE.equals(theParamName)) {
248                                for (QualifiedParamList nextQualifiedList : theValues) {
249                                        for (String nextValue : nextQualifiedList) {
250                                                theMapToPopulate.addInclude(new Include(
251                                                                nextValue, ParameterUtil.isIncludeIterate(nextQualifiedList.getQualifier())));
252                                        }
253                                }
254                        } else if (Constants.PARAM_REVINCLUDE.equals(theParamName)) {
255                                for (QualifiedParamList nextQualifiedList : theValues) {
256                                        for (String nextValue : nextQualifiedList) {
257                                                theMapToPopulate.addRevInclude(new Include(
258                                                                nextValue, ParameterUtil.isIncludeIterate(nextQualifiedList.getQualifier())));
259                                        }
260                                }
261                        }
262                };
263        }
264}