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