001package ca.uhn.fhir.rest.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 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 */
022import static org.apache.commons.lang3.StringUtils.isBlank;
023import static org.apache.commons.lang3.StringUtils.isNotBlank;
024
025import java.lang.reflect.Method;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.HashSet;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Set;
034
035import org.apache.commons.lang3.StringUtils;
036import org.hl7.fhir.instance.model.api.IBaseResource;
037
038import ca.uhn.fhir.context.ConfigurationException;
039import ca.uhn.fhir.context.FhirContext;
040import ca.uhn.fhir.model.api.annotation.Description;
041import ca.uhn.fhir.model.primitive.IdDt;
042import ca.uhn.fhir.model.valueset.BundleTypeEnum;
043import ca.uhn.fhir.rest.annotation.Search;
044import ca.uhn.fhir.rest.api.RequestTypeEnum;
045import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
046import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
047import ca.uhn.fhir.rest.param.BaseQueryParameter;
048import ca.uhn.fhir.rest.server.Constants;
049import ca.uhn.fhir.rest.server.IBundleProvider;
050import ca.uhn.fhir.rest.server.IRestfulServer;
051import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
052import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
053
054public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
055        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
056
057        private String myCompartmentName;
058        private String myDescription;
059        private Integer myIdParamIndex;
060        private String myQueryName;
061        private boolean myAllowUnknownParams;
062
063        public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
064                super(theReturnResourceType, theMethod, theContext, theProvider);
065                Search search = theMethod.getAnnotation(Search.class);
066                this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null);
067                this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null);
068                this.myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod, getContext());
069                this.myAllowUnknownParams = search.allowUnknownParams();
070
071                Description desc = theMethod.getAnnotation(Description.class);
072                if (desc != null) {
073                        if (isNotBlank(desc.formalDefinition())) {
074                                myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null);
075                        } else {
076                                myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null);
077                        }
078                }
079
080                /*
081                 * Check for parameter combinations and names that are invalid
082                 */
083                List<IParameter> parameters = getParameters();
084                // List<SearchParameter> searchParameters = new ArrayList<SearchParameter>();
085                for (int i = 0; i < parameters.size(); i++) {
086                        IParameter next = parameters.get(i);
087                        if (!(next instanceof SearchParameter)) {
088                                continue;
089                        }
090
091                        SearchParameter sp = (SearchParameter) next;
092                        if (sp.getName().startsWith("_")) {
093                                if (ALLOWED_PARAMS.contains(sp.getName())) {
094                                        String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(),
095                                                        sp.getName());
096                                        throw new ConfigurationException(msg);
097                                }
098                        }
099
100                        // searchParameters.add(sp);
101                }
102                // for (int i = 0; i < searchParameters.size(); i++) {
103                // SearchParameter next = searchParameters.get(i);
104                // // next.
105                // }
106
107                /*
108                 * Only compartment searching methods may have an ID parameter
109                 */
110                if (isBlank(myCompartmentName) && myIdParamIndex != null) {
111                        String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass());
112                        throw new ConfigurationException(msg);
113                }
114
115        }
116
117        public String getDescription() {
118                return myDescription;
119        }
120
121        @Override
122        public RestOperationTypeEnum getRestOperationType() {
123                return RestOperationTypeEnum.SEARCH_TYPE;
124        }
125
126        @Override
127        protected BundleTypeEnum getResponseBundleType() {
128                return BundleTypeEnum.SEARCHSET;
129        }
130
131        @Override
132        public ReturnTypeEnum getReturnType() {
133                return ReturnTypeEnum.BUNDLE;
134        }
135
136        @Override
137        public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
138                if (theRequest.getId() != null && myIdParamIndex == null) {
139                        ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId());
140                        return false;
141                }
142                if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
143                        ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation());
144                        return false;
145                }
146                if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
147                        ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation());
148                        return false;
149                }
150                if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
151                        ourLog.trace("Method {} doesn't match because request type is {}", getMethod());
152                        return false;
153                }
154                if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
155                        ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", new Object[] { getMethod(), myCompartmentName, theRequest.getCompartmentName() });
156                        return false;
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<String>();
161
162                Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
163                Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
164                for (int i = 0; i < this.getParameters().size(); i++) {
165                        if (!(getParameters().get(i) instanceof BaseQueryParameter)) {
166                                continue;
167                        }
168                        BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i);
169                        String name = temp.getName();
170                        if (temp.isRequired()) {
171
172                                if (qualifiedParamNames.contains(name)) {
173                                        QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
174                                        if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
175                                                methodParamsTemp.add(name);
176                                        }
177                                }
178                                if (unqualifiedNames.contains(name)) {
179                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
180                                        qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
181                                        methodParamsTemp.addAll(qualifiedNames);
182                                }
183                                if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name))
184                                {
185                                        ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
186                                        return false;
187                                }
188
189                        } else {
190                                if (qualifiedParamNames.contains(name)) {
191                                        QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
192                                        if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
193                                                methodParamsTemp.add(name);
194                                        }
195                                } 
196                                if (unqualifiedNames.contains(name)) {
197                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
198                                        qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
199                                        methodParamsTemp.addAll(qualifiedNames);
200                                }
201                                if (!qualifiedParamNames.contains(name) && !qualifiedParamNames.contains(name)) { 
202                                        methodParamsTemp.add(name);
203                                }
204                        }
205                }
206                if (myQueryName != null) {
207                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
208                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
209                                String queryName = queryNameValues[0];
210                                if (!myQueryName.equals(queryName)) {
211                                        ourLog.trace("Query name does not match {}", myQueryName);
212                                        return false;
213                                } else {
214                                        methodParamsTemp.add(Constants.PARAM_QUERY);
215                                }
216                        } else {
217                                ourLog.trace("Query name does not match {}", myQueryName);
218                                return false;
219                        }
220                } else {
221                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
222                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
223                                ourLog.trace("Query has name");
224                                return false;
225                        }
226                }
227                for (String next : theRequest.getParameters().keySet()) {
228                        if (ALLOWED_PARAMS.contains(next)) {
229                                methodParamsTemp.add(next);
230                        }
231                }
232                Set<String> keySet = theRequest.getParameters().keySet();
233                if (myAllowUnknownParams == false) {
234                        for (String next : keySet) {
235                                if (!methodParamsTemp.contains(next)) {
236                                        return false;
237                                }
238                        }
239                }
240                return true;
241        }
242
243        @Override
244        public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
245                assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null");
246
247                Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>();
248
249                if (myQueryName != null) {
250                        queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName));
251                }
252
253                IdDt id = (IdDt) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null);
254
255                String resourceName = getResourceName();
256                if (theArgs != null) {
257                        for (int idx = 0; idx < theArgs.length; idx++) {
258                                IParameter nextParam = getParameters().get(idx);
259                                nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, null);
260                        }
261                }
262
263                BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null);
264
265                return retVal;
266        }
267
268        @Override
269        public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
270                if (myIdParamIndex != null) {
271                        theMethodParams[myIdParamIndex] = theRequest.getId();
272                }
273
274                Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
275
276                return toResourceList(response);
277
278        }
279
280        @Override
281        protected boolean isAddContentLocationHeader() {
282                return false;
283        }
284
285        private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
286                if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
287                        return theQualifiedNames;
288                }
289                ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size());
290                for (String next : theQualifiedNames) {
291                        QualifierDetails qualifiers = extractQualifiersFromParameterName(next);
292                        if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
293                                continue;
294                        }
295                        retVal.add(next);
296                }
297                return retVal;
298        }
299
300        @Override
301        public String toString() {
302                return getMethod().toString();
303        }
304
305        public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName,
306                        SearchStyleEnum theSearchStyle) {
307                SearchStyleEnum searchStyle = theSearchStyle;
308                if (searchStyle == null) {
309                        int length = 0;
310                        for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) {
311                                length += nextEntry.getKey().length();
312                                for (String next : nextEntry.getValue()) {
313                                        length += next.length();
314                                }
315                        }
316
317                        if (length < 5000) {
318                                searchStyle = SearchStyleEnum.GET;
319                        } else {
320                                searchStyle = SearchStyleEnum.POST;
321                        }
322                }
323
324                BaseHttpClientInvocation invocation;
325
326                boolean compartmentSearch = false;
327                if (theCompartmentName != null) {
328                        if (theId == null || !theId.hasIdPart()) {
329                                String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch");
330                                throw new InvalidRequestException(msg);
331                        } else {
332                                compartmentSearch = true;
333                        }
334                }
335
336                /*
337                 * Are we doing a get (GET [base]/Patient?name=foo) or a get with search (GET [base]/Patient/_search?name=foo) or a post (POST [base]/Patient with parameters in the POST body)
338                 */
339                switch (searchStyle) {
340                case GET:
341                default:
342                        if (compartmentSearch) {
343                                invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName);
344                        } else {
345                                invocation = new HttpGetClientInvocation(theParameters, theResourceName);
346                        }
347                        break;
348                case GET_WITH_SEARCH:
349                        if (compartmentSearch) {
350                                invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
351                        } else {
352                                invocation = new HttpGetClientInvocation(theParameters, theResourceName, Constants.PARAM_SEARCH);
353                        }
354                        break;
355                case POST:
356                        if (compartmentSearch) {
357                                invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
358                        } else {
359                                invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH);
360                        }
361                }
362
363                return invocation;
364        }
365
366        public static QualifierDetails extractQualifiersFromParameterName(String theParamName) {
367                QualifierDetails retVal = new QualifierDetails();
368                if (theParamName == null || theParamName.length() == 0) {
369                        return retVal;
370                }
371
372                int dotIdx = -1;
373                int colonIdx = -1;
374                for (int idx = 0; idx < theParamName.length(); idx++) {
375                        char nextChar = theParamName.charAt(idx);
376                        if (nextChar == '.' && dotIdx == -1) {
377                                dotIdx = idx;
378                        } else if (nextChar == ':' && colonIdx == -1) {
379                                colonIdx = idx;
380                        }
381                }
382
383                if (dotIdx != -1 && colonIdx != -1) {
384                        if (dotIdx < colonIdx) {
385                                retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx));
386                                retVal.setColonQualifier(theParamName.substring(colonIdx));
387                        } else {
388                                retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx));
389                                retVal.setDotQualifier(theParamName.substring(dotIdx));
390                        }
391                } else if (dotIdx != -1) {
392                        retVal.setDotQualifier(theParamName.substring(dotIdx));
393                } else if (colonIdx != -1) {
394                        retVal.setColonQualifier(theParamName.substring(colonIdx));
395                }
396
397                return retVal;
398        }
399
400        public static class QualifierDetails {
401
402                private String myColonQualifier;
403                private String myDotQualifier;
404
405                public boolean passes(Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
406                        if (theQualifierWhitelist != null) {
407                                if (!theQualifierWhitelist.contains(".*")) {
408                                        if (myDotQualifier != null) {
409                                                if (!theQualifierWhitelist.contains(myDotQualifier)) {
410                                                        return false;
411                                                }
412                                        } else {
413                                                if (!theQualifierWhitelist.contains(".")) {
414                                                        return false;
415                                                }
416                                        }
417                                }
418                                /*
419                                 * This was removed Sep 9 2015, as I don't see any way it could possibly be triggered.
420                                if (!theQualifierWhitelist.contains(SearchParameter.QUALIFIER_ANY_TYPE)) {
421                                        if (myColonQualifier != null) {
422                                                if (!theQualifierWhitelist.contains(myColonQualifier)) {
423                                                        return false;
424                                                }
425                                        } else {
426                                                if (!theQualifierWhitelist.contains(":")) {
427                                                        return false;
428                                                }
429                                        }
430                                }
431                                */
432                        }
433                        if (theQualifierBlacklist != null) {
434                                if (myDotQualifier != null) {
435                                        if (theQualifierBlacklist.contains(myDotQualifier)) {
436                                                return false;
437                                        }
438                                }
439                                if (myColonQualifier != null) {
440                                        if (theQualifierBlacklist.contains(myColonQualifier)) {
441                                                return false;
442                                        }
443                                }
444                        }
445
446                        return true;
447                }
448
449                public void setColonQualifier(String theColonQualifier) {
450                        myColonQualifier = theColonQualifier;
451                }
452
453                public void setDotQualifier(String theDotQualifier) {
454                        myDotQualifier = theDotQualifier;
455                }
456
457        }
458
459        public static BaseHttpClientInvocation createSearchInvocation(String theSearchUrl, Map<String, List<String>> theParams) {
460                return new HttpGetClientInvocation(theParams, theSearchUrl);
461        }
462
463}