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