001package ca.uhn.fhir.rest.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 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                
139                String clientPreference = theRequest.getHeader(Constants.HEADER_PREFER);
140                boolean lenientHandling = false;
141                if(clientPreference != null)
142                {
143                        String[] preferences = clientPreference.split(";");
144                        for( String p : preferences){
145                                if("handling:lenient".equalsIgnoreCase(p))
146                                {
147                                        lenientHandling = true;
148                                        break;
149                                }
150                        }
151                }
152                
153                if (theRequest.getId() != null && myIdParamIndex == null) {
154                        ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId());
155                        return false;
156                }
157                if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
158                        ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation());
159                        return false;
160                }
161                if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
162                        ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation());
163                        return false;
164                }
165                if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
166                        ourLog.trace("Method {} doesn't match because request type is {}", getMethod());
167                        return false;
168                }
169                if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
170                        ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", new Object[] { getMethod(), myCompartmentName, theRequest.getCompartmentName() });
171                        return false;
172                }
173                // This is used to track all the parameters so we can reject queries that
174                // have additional params we don't understand
175                Set<String> methodParamsTemp = new HashSet<String>();
176
177                Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
178                Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
179                for (int i = 0; i < this.getParameters().size(); i++) {
180                        if (!(getParameters().get(i) instanceof BaseQueryParameter)) {
181                                continue;
182                        }
183                        BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i);
184                        String name = temp.getName();
185                        if (temp.isRequired()) {
186
187                                if (qualifiedParamNames.contains(name)) {
188                                        QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
189                                        if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
190                                                methodParamsTemp.add(name);
191                                        }
192                                }
193                                if (unqualifiedNames.contains(name)) {
194                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
195                                        qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
196                                        methodParamsTemp.addAll(qualifiedNames);
197                                }
198                                if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name))
199                                {
200                                        ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
201                                        return false;
202                                }
203
204                        } else {
205                                if (qualifiedParamNames.contains(name)) {
206                                        QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
207                                        if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
208                                                methodParamsTemp.add(name);
209                                        }
210                                } 
211                                if (unqualifiedNames.contains(name)) {
212                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
213                                        qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
214                                        methodParamsTemp.addAll(qualifiedNames);
215                                }
216                                if (!qualifiedParamNames.contains(name)) { 
217                                        methodParamsTemp.add(name);
218                                }
219                        }
220                }
221                if (myQueryName != null) {
222                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
223                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
224                                String queryName = queryNameValues[0];
225                                if (!myQueryName.equals(queryName)) {
226                                        ourLog.trace("Query name does not match {}", myQueryName);
227                                        return false;
228                                }
229                                methodParamsTemp.add(Constants.PARAM_QUERY);
230                        } else {
231                                ourLog.trace("Query name does not match {}", myQueryName);
232                                return false;
233                        }
234                } else {
235                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
236                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
237                                ourLog.trace("Query has name");
238                                return false;
239                        }
240                }
241                for (String next : theRequest.getParameters().keySet()) {
242                        if (ALLOWED_PARAMS.contains(next)) {
243                                methodParamsTemp.add(next);
244                        }
245                }
246                Set<String> keySet = theRequest.getParameters().keySet();
247                if(lenientHandling == true)
248                        return true;
249
250                if (myAllowUnknownParams == false) {
251                        for (String next : keySet) {
252                                if (!methodParamsTemp.contains(next)) {
253                                        return false;
254                                }
255                        }
256                }
257                return true;
258        }
259
260        @Override
261        public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
262                assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null");
263
264                Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>();
265
266                if (myQueryName != null) {
267                        queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName));
268                }
269
270                IdDt id = (IdDt) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null);
271
272                String resourceName = getResourceName();
273                if (theArgs != null) {
274                        for (int idx = 0; idx < theArgs.length; idx++) {
275                                IParameter nextParam = getParameters().get(idx);
276                                nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, null);
277                        }
278                }
279
280                BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null);
281
282                return retVal;
283        }
284
285        @Override
286        public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
287                if (myIdParamIndex != null) {
288                        theMethodParams[myIdParamIndex] = theRequest.getId();
289                }
290
291                Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
292
293                return toResourceList(response);
294
295        }
296
297        @Override
298        protected boolean isAddContentLocationHeader() {
299                return false;
300        }
301
302        private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
303                if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
304                        return theQualifiedNames;
305                }
306                ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size());
307                for (String next : theQualifiedNames) {
308                        QualifierDetails qualifiers = extractQualifiersFromParameterName(next);
309                        if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
310                                continue;
311                        }
312                        retVal.add(next);
313                }
314                return retVal;
315        }
316
317        @Override
318        public String toString() {
319                return getMethod().toString();
320        }
321
322        public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName,
323                        SearchStyleEnum theSearchStyle) {
324                SearchStyleEnum searchStyle = theSearchStyle;
325                if (searchStyle == null) {
326                        int length = 0;
327                        for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) {
328                                length += nextEntry.getKey().length();
329                                for (String next : nextEntry.getValue()) {
330                                        length += next.length();
331                                }
332                        }
333
334                        if (length < 5000) {
335                                searchStyle = SearchStyleEnum.GET;
336                        } else {
337                                searchStyle = SearchStyleEnum.POST;
338                        }
339                }
340
341                BaseHttpClientInvocation invocation;
342
343                boolean compartmentSearch = false;
344                if (theCompartmentName != null) {
345                        if (theId == null || !theId.hasIdPart()) {
346                                String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch");
347                                throw new InvalidRequestException(msg);
348                        }
349                        compartmentSearch = true;
350                }
351
352                /*
353                 * 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)
354                 */
355                switch (searchStyle) {
356                case GET:
357                default:
358                        if (compartmentSearch) {
359                                invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName);
360                        } else {
361                                invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName);
362                        }
363                        break;
364                case GET_WITH_SEARCH:
365                        if (compartmentSearch) {
366                                invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
367                        } else {
368                                invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH);
369                        }
370                        break;
371                case POST:
372                        if (compartmentSearch) {
373                                invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH);
374                        } else {
375                                invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH);
376                        }
377                }
378
379                return invocation;
380        }
381
382        public static QualifierDetails extractQualifiersFromParameterName(String theParamName) {
383                QualifierDetails retVal = new QualifierDetails();
384                if (theParamName == null || theParamName.length() == 0) {
385                        return retVal;
386                }
387
388                int dotIdx = -1;
389                int colonIdx = -1;
390                for (int idx = 0; idx < theParamName.length(); idx++) {
391                        char nextChar = theParamName.charAt(idx);
392                        if (nextChar == '.' && dotIdx == -1) {
393                                dotIdx = idx;
394                        } else if (nextChar == ':' && colonIdx == -1) {
395                                colonIdx = idx;
396                        }
397                }
398
399                if (dotIdx != -1 && colonIdx != -1) {
400                        if (dotIdx < colonIdx) {
401                                retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx));
402                                retVal.setColonQualifier(theParamName.substring(colonIdx));
403                                retVal.setParamName(theParamName.substring(0, dotIdx));
404                                retVal.setWholeQualifier(theParamName.substring(dotIdx));
405                        } else {
406                                retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx));
407                                retVal.setDotQualifier(theParamName.substring(dotIdx));
408                                retVal.setParamName(theParamName.substring(0, colonIdx));
409                                retVal.setWholeQualifier(theParamName.substring(colonIdx));
410                        }
411                } else if (dotIdx != -1) {
412                        retVal.setDotQualifier(theParamName.substring(dotIdx));
413                        retVal.setParamName(theParamName.substring(0, dotIdx));
414                        retVal.setWholeQualifier(theParamName.substring(dotIdx));
415                } else if (colonIdx != -1) {
416                        retVal.setColonQualifier(theParamName.substring(colonIdx));
417                        retVal.setParamName(theParamName.substring(0, colonIdx));
418                        retVal.setWholeQualifier(theParamName.substring(colonIdx));
419                } else {
420                        retVal.setParamName(theParamName);
421                        retVal.setColonQualifier(null);
422                        retVal.setDotQualifier(null);
423                        retVal.setWholeQualifier(null);
424                }
425
426                return retVal;
427        }
428
429        public static class QualifierDetails {
430
431                private String myColonQualifier;
432                private String myDotQualifier;
433                private String myParamName;
434                private String myWholeQualifier;
435
436                public boolean passes(Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
437                        if (theQualifierWhitelist != null) {
438                                if (!theQualifierWhitelist.contains(".*")) {
439                                        if (myDotQualifier != null) {
440                                                if (!theQualifierWhitelist.contains(myDotQualifier)) {
441                                                        return false;
442                                                }
443                                        } else {
444                                                if (!theQualifierWhitelist.contains(".")) {
445                                                        return false;
446                                                }
447                                        }
448                                }
449                                /*
450                                 * This was removed Sep 9 2015, as I don't see any way it could possibly be triggered.
451                                if (!theQualifierWhitelist.contains(SearchParameter.QUALIFIER_ANY_TYPE)) {
452                                        if (myColonQualifier != null) {
453                                                if (!theQualifierWhitelist.contains(myColonQualifier)) {
454                                                        return false;
455                                                }
456                                        } else {
457                                                if (!theQualifierWhitelist.contains(":")) {
458                                                        return false;
459                                                }
460                                        }
461                                }
462                                */
463                        }
464                        if (theQualifierBlacklist != null) {
465                                if (myDotQualifier != null) {
466                                        if (theQualifierBlacklist.contains(myDotQualifier)) {
467                                                return false;
468                                        }
469                                }
470                                if (myColonQualifier != null) {
471                                        if (theQualifierBlacklist.contains(myColonQualifier)) {
472                                                return false;
473                                        }
474                                }
475                        }
476
477                        return true;
478                }
479
480                public void setParamName(String theParamName) {
481                        myParamName = theParamName;
482                }
483
484                public String getParamName() {
485                        return myParamName;
486                }
487
488                public void setColonQualifier(String theColonQualifier) {
489                        myColonQualifier = theColonQualifier;
490                }
491
492                public void setDotQualifier(String theDotQualifier) {
493                        myDotQualifier = theDotQualifier;
494                }
495
496                public String getWholeQualifier() {
497                        return myWholeQualifier;
498                }
499
500                public void setWholeQualifier(String theWholeQualifier) {
501                        myWholeQualifier = theWholeQualifier;
502                }
503
504        }
505
506        public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theSearchUrl, Map<String, List<String>> theParams) {
507                return new HttpGetClientInvocation(theContext, theParams, theSearchUrl);
508        }
509
510}