001package ca.uhn.fhir.rest.server.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.ConfigurationException;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.model.api.annotation.Description;
026import ca.uhn.fhir.model.valueset.BundleTypeEnum;
027import ca.uhn.fhir.rest.annotation.Search;
028import ca.uhn.fhir.rest.api.Constants;
029import ca.uhn.fhir.rest.api.RequestTypeEnum;
030import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
031import ca.uhn.fhir.rest.api.server.IBundleProvider;
032import ca.uhn.fhir.rest.api.server.IRestfulServer;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.param.ParameterUtil;
035import ca.uhn.fhir.rest.param.QualifierDetails;
036import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
038import org.apache.commons.lang3.StringUtils;
039import org.hl7.fhir.instance.model.api.IAnyResource;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041
042import javax.annotation.Nonnull;
043import java.lang.reflect.Method;
044import java.util.*;
045
046import static org.apache.commons.lang3.StringUtils.isBlank;
047import static org.apache.commons.lang3.StringUtils.isNotBlank;
048
049public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
050        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
051
052        private static final Set<String> SPECIAL_SEARCH_PARAMS;
053
054        static {
055                HashSet<String> specialSearchParams = new HashSet<>();
056                specialSearchParams.add(IAnyResource.SP_RES_ID);
057                specialSearchParams.add(IAnyResource.SP_RES_LANGUAGE);
058                specialSearchParams.add(Constants.PARAM_INCLUDE);
059                specialSearchParams.add(Constants.PARAM_REVINCLUDE);
060                SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams);
061        }
062
063        private final String myResourceProviderResourceName;
064        private String myCompartmentName;
065        private String myDescription;
066        private Integer myIdParamIndex;
067        private String myQueryName;
068        private boolean myAllowUnknownParams;
069
070        public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Class<? extends IBaseResource> theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
071                super(theReturnResourceType, theMethod, theContext, theProvider);
072                Search search = theMethod.getAnnotation(Search.class);
073                this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null);
074                this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null);
075                this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
076                this.myAllowUnknownParams = search.allowUnknownParams();
077
078                Description desc = theMethod.getAnnotation(Description.class);
079                if (desc != null) {
080                        if (isNotBlank(desc.formalDefinition())) {
081                                myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null);
082                        } else {
083                                myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null);
084                        }
085                }
086
087                /*
088                 * Only compartment searching methods may have an ID parameter
089                 */
090                if (isBlank(myCompartmentName) && myIdParamIndex != null) {
091                        String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass());
092                        throw new ConfigurationException(msg);
093                }
094
095                if (theResourceProviderResourceType != null) {
096                        this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName();
097                } else {
098                        this.myResourceProviderResourceName = null;
099                }
100
101        }
102
103        public String getDescription() {
104                return myDescription;
105        }
106
107        public String getQueryName() {
108                return myQueryName;
109        }
110
111        public String getResourceProviderResourceName() {
112                return myResourceProviderResourceName;
113        }
114
115        @Nonnull
116        @Override
117        public RestOperationTypeEnum getRestOperationType() {
118                return RestOperationTypeEnum.SEARCH_TYPE;
119        }
120
121        @Override
122        protected BundleTypeEnum getResponseBundleType() {
123                return BundleTypeEnum.SEARCHSET;
124        }
125
126        @Override
127        public ReturnTypeEnum getReturnType() {
128                return ReturnTypeEnum.BUNDLE;
129        }
130
131        @Override
132        public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
133
134                if (theRequest.getId() != null && myIdParamIndex == null) {
135                        ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId());
136                        return false;
137                }
138                if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
139                        ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation());
140                        return false;
141                }
142                if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
143                        ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation());
144                        return false;
145                }
146                if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
147                        ourLog.trace("Method {} doesn't match because request type is {}", getMethod(), theRequest.getRequestType());
148                        return false;
149                }
150                if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
151                        ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName());
152                        return false;
153                }
154                if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) {
155                        return false;
156                }
157
158                // This is used to track all the parameters so we can reject queries that
159                // have additional params we don't understand
160                Set<String> methodParamsTemp = new HashSet<>();
161
162                Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
163                Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
164                for (IParameter nextParameter : getParameters()) {
165                        if (!(nextParameter instanceof BaseQueryParameter)) {
166                                continue;
167                        }
168                        BaseQueryParameter nextQueryParameter = (BaseQueryParameter) nextParameter;
169                        String name = nextQueryParameter.getName();
170                        if (nextQueryParameter.isRequired()) {
171
172                                if (qualifiedParamNames.contains(name)) {
173                                        QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
174                                        if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
175                                                methodParamsTemp.add(name);
176                                        }
177                                }
178                                if (unqualifiedNames.contains(name)) {
179                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
180                                        qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
181                                        methodParamsTemp.addAll(qualifiedNames);
182                                }
183                                if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) {
184                                        ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
185                                        return false;
186                                }
187
188                        } else {
189                                if (qualifiedParamNames.contains(name)) {
190                                        QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
191                                        if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
192                                                methodParamsTemp.add(name);
193                                        }
194                                }
195                                if (unqualifiedNames.contains(name)) {
196                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
197                                        qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
198                                        methodParamsTemp.addAll(qualifiedNames);
199                                }
200                                if (!qualifiedParamNames.contains(name)) {
201                                        methodParamsTemp.add(name);
202                                }
203                        }
204                }
205                if (myQueryName != null) {
206                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
207                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
208                                String queryName = queryNameValues[0];
209                                if (!myQueryName.equals(queryName)) {
210                                        ourLog.trace("Query name does not match {}", myQueryName);
211                                        return false;
212                                }
213                                methodParamsTemp.add(Constants.PARAM_QUERY);
214                        } else {
215                                ourLog.trace("Query name does not match {}", myQueryName);
216                                return false;
217                        }
218                } else {
219                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
220                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
221                                ourLog.trace("Query has name");
222                                return false;
223                        }
224                }
225                for (String next : theRequest.getParameters().keySet()) {
226                        if (next.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(truncModifierPart(next))) {
227                                methodParamsTemp.add(next);
228                        }
229                }
230                Set<String> keySet = theRequest.getParameters().keySet();
231
232                if (myAllowUnknownParams == false) {
233                        for (String next : keySet) {
234                                if (!methodParamsTemp.contains(next)) {
235                                        return false;
236                                }
237                        }
238                }
239                return true;
240        }
241
242        private String truncModifierPart(String param) {
243                int indexOfSeparator = param.indexOf(":");
244                if (indexOfSeparator != -1) {
245                        return param.substring(0, indexOfSeparator);
246                }
247                return param;
248        }
249
250        @Override
251        public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
252                if (myIdParamIndex != null) {
253                        theMethodParams[myIdParamIndex] = theRequest.getId();
254                }
255
256                Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
257
258                return toResourceList(response);
259
260        }
261
262        @Override
263        protected boolean isAddContentLocationHeader() {
264                return false;
265        }
266
267        private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
268                if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
269                        return theQualifiedNames;
270                }
271                ArrayList<String> retVal = new ArrayList<>(theQualifiedNames.size());
272                for (String next : theQualifiedNames) {
273                        QualifierDetails qualifiers = extractQualifiersFromParameterName(next);
274                        if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
275                                continue;
276                        }
277                        retVal.add(next);
278                }
279                return retVal;
280        }
281
282        @Override
283        public String toString() {
284                return getMethod().toString();
285        }
286
287        public static QualifierDetails extractQualifiersFromParameterName(String theParamName) {
288                QualifierDetails retVal = new QualifierDetails();
289                if (theParamName == null || theParamName.length() == 0) {
290                        return retVal;
291                }
292
293                int dotIdx = -1;
294                int colonIdx = -1;
295                for (int idx = 0; idx < theParamName.length(); idx++) {
296                        char nextChar = theParamName.charAt(idx);
297                        if (nextChar == '.' && dotIdx == -1) {
298                                dotIdx = idx;
299                        } else if (nextChar == ':' && colonIdx == -1) {
300                                colonIdx = idx;
301                        }
302                }
303
304                if (dotIdx != -1 && colonIdx != -1) {
305                        if (dotIdx < colonIdx) {
306                                retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx));
307                                retVal.setColonQualifier(theParamName.substring(colonIdx));
308                                retVal.setParamName(theParamName.substring(0, dotIdx));
309                                retVal.setWholeQualifier(theParamName.substring(dotIdx));
310                        } else {
311                                retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx));
312                                retVal.setDotQualifier(theParamName.substring(dotIdx));
313                                retVal.setParamName(theParamName.substring(0, colonIdx));
314                                retVal.setWholeQualifier(theParamName.substring(colonIdx));
315                        }
316                } else if (dotIdx != -1) {
317                        retVal.setDotQualifier(theParamName.substring(dotIdx));
318                        retVal.setParamName(theParamName.substring(0, dotIdx));
319                        retVal.setWholeQualifier(theParamName.substring(dotIdx));
320                } else if (colonIdx != -1) {
321                        retVal.setColonQualifier(theParamName.substring(colonIdx));
322                        retVal.setParamName(theParamName.substring(0, colonIdx));
323                        retVal.setWholeQualifier(theParamName.substring(colonIdx));
324                } else {
325                        retVal.setParamName(theParamName);
326                        retVal.setColonQualifier(null);
327                        retVal.setDotQualifier(null);
328                        retVal.setWholeQualifier(null);
329                }
330
331                return retVal;
332        }
333
334
335}