001package ca.uhn.fhir.rest.server.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
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.i18n.Msg;
024import ca.uhn.fhir.context.ConfigurationException;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.model.api.annotation.Description;
027import ca.uhn.fhir.model.valueset.BundleTypeEnum;
028import ca.uhn.fhir.rest.annotation.Search;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.RequestTypeEnum;
031import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
032import ca.uhn.fhir.rest.api.server.IBundleProvider;
033import ca.uhn.fhir.rest.api.server.IRestfulServer;
034import ca.uhn.fhir.rest.api.server.RequestDetails;
035import ca.uhn.fhir.rest.param.ParameterUtil;
036import ca.uhn.fhir.rest.param.QualifierDetails;
037import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
039import ca.uhn.fhir.util.ParametersUtil;
040import org.apache.commons.lang3.StringUtils;
041import org.hl7.fhir.instance.model.api.IAnyResource;
042import org.hl7.fhir.instance.model.api.IBaseResource;
043
044import javax.annotation.Nonnull;
045import java.lang.reflect.Method;
046import java.util.Collections;
047import java.util.HashSet;
048import java.util.List;
049import java.util.Set;
050import java.util.stream.Collectors;
051
052import static org.apache.commons.lang3.StringUtils.isBlank;
053import static org.apache.commons.lang3.StringUtils.isNotBlank;
054
055public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
056        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
057
058        private static final Set<String> SPECIAL_SEARCH_PARAMS;
059
060        static {
061                HashSet<String> specialSearchParams = new HashSet<>();
062                specialSearchParams.add(IAnyResource.SP_RES_ID);
063                specialSearchParams.add(Constants.PARAM_INCLUDE);
064                specialSearchParams.add(Constants.PARAM_REVINCLUDE);
065                SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams);
066        }
067
068        private final String myResourceProviderResourceName;
069        private final List<String> myRequiredParamNames;
070        private final List<String> myOptionalParamNames;
071        private final String myCompartmentName;
072        private String myDescription;
073        private final Integer myIdParamIndex;
074        private final String myQueryName;
075        private final boolean myAllowUnknownParams;
076
077        public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Class<? extends IBaseResource> theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
078                super(theReturnResourceType, theMethod, theContext, theProvider);
079                Search search = theMethod.getAnnotation(Search.class);
080                this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null);
081                this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null);
082                this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
083                this.myAllowUnknownParams = search.allowUnknownParams();
084                this.myDescription = ParametersUtil.extractDescription(theMethod);
085
086                /*
087                 * Only compartment searching methods may have an ID parameter
088                 */
089                if (isBlank(myCompartmentName) && myIdParamIndex != null) {
090                        String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass());
091                        throw new ConfigurationException(Msg.code(412) + msg);
092                }
093
094                if (theResourceProviderResourceType != null) {
095                        this.myResourceProviderResourceName = theContext.getResourceType(theResourceProviderResourceType);
096                } else {
097                        this.myResourceProviderResourceName = null;
098                }
099
100                myRequiredParamNames = getQueryParameters()
101                        .stream()
102                        .filter(t -> t.isRequired())
103                        .map(t -> t.getName())
104                        .collect(Collectors.toList());
105                myOptionalParamNames = getQueryParameters()
106                        .stream()
107                        .filter(t -> !t.isRequired())
108                        .map(t -> t.getName())
109                        .collect(Collectors.toList());
110
111        }
112
113        public String getDescription() {
114                return myDescription;
115        }
116
117        public String getQueryName() {
118                return myQueryName;
119        }
120
121        public String getResourceProviderResourceName() {
122                return myResourceProviderResourceName;
123        }
124
125        @Nonnull
126        @Override
127        public RestOperationTypeEnum getRestOperationType() {
128                return RestOperationTypeEnum.SEARCH_TYPE;
129        }
130
131        @Override
132        protected BundleTypeEnum getResponseBundleType() {
133                return BundleTypeEnum.SEARCHSET;
134        }
135
136        @Override
137        public ReturnTypeEnum getReturnType() {
138                return ReturnTypeEnum.BUNDLE;
139        }
140
141        @Override
142        public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
143
144                if (!mightBeSearchRequest(theRequest)) {
145                        return MethodMatchEnum.NONE;
146                }
147
148                if (theRequest.getId() != null && myIdParamIndex == null) {
149                        ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId());
150                        return MethodMatchEnum.NONE;
151                }
152                if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
153                        ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName());
154                        return MethodMatchEnum.NONE;
155                }
156
157                if (myQueryName != null) {
158                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
159                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
160                                String queryName = queryNameValues[0];
161                                if (!myQueryName.equals(queryName)) {
162                                        ourLog.trace("Query name does not match {}", myQueryName);
163                                        return MethodMatchEnum.NONE;
164                                }
165                        } else {
166                                ourLog.trace("Query name does not match {}", myQueryName);
167                                return MethodMatchEnum.NONE;
168                        }
169                } else {
170                        String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY);
171                        if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
172                                ourLog.trace("Query has name");
173                                return MethodMatchEnum.NONE;
174                        }
175                }
176
177                Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
178                Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
179
180                MethodMatchEnum retVal = MethodMatchEnum.EXACT;
181                for (String nextRequestParam : theRequest.getParameters().keySet()) {
182                        String nextUnqualifiedRequestParam = ParameterUtil.stripModifierPart(nextRequestParam);
183                        if (nextRequestParam.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(nextUnqualifiedRequestParam)) {
184                                continue;
185                        }
186
187                        boolean parameterMatches = false;
188                        boolean approx = false;
189                        for (BaseQueryParameter nextMethodParam : getQueryParameters()) {
190
191                                if (nextRequestParam.equals(nextMethodParam.getName())) {
192                                        QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(nextRequestParam);
193                                        if (qualifiers.passes(nextMethodParam.getQualifierWhitelist(), nextMethodParam.getQualifierBlacklist())) {
194                                                parameterMatches = true;
195                                        }
196                                } else if (nextUnqualifiedRequestParam.equals(nextMethodParam.getName())) {
197                                        List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(nextUnqualifiedRequestParam);
198                                        if (passesWhitelistAndBlacklist(qualifiedNames, nextMethodParam.getQualifierWhitelist(), nextMethodParam.getQualifierBlacklist())) {
199                                                parameterMatches = true;
200                                        }
201                                }
202
203                                // Repetitions supplied by URL but not supported by this parameter
204                                if (theRequest.getParameters().get(nextRequestParam).length > 1 != nextMethodParam.supportsRepetition()) {
205                                        approx = true;
206                                }
207
208                        }
209
210
211                        if (parameterMatches) {
212
213                                if (approx) {
214                                        retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE);
215                                }
216
217                        } else {
218
219                                if (myAllowUnknownParams) {
220                                        retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE);
221                                } else {
222                                        retVal = retVal.weakerOf(MethodMatchEnum.NONE);
223                                }
224
225                        }
226
227                        if (retVal == MethodMatchEnum.NONE) {
228                                break;
229                        }
230
231                }
232
233                if (retVal != MethodMatchEnum.NONE) {
234                        for (String nextRequiredParamName : myRequiredParamNames) {
235                                if (!qualifiedParamNames.contains(nextRequiredParamName)) {
236                                        if (!unqualifiedNames.contains(nextRequiredParamName)) {
237                                                retVal = MethodMatchEnum.NONE;
238                                                break;
239                                        }
240                                }
241                        }
242                }
243                if (retVal != MethodMatchEnum.NONE) {
244                        for (String nextRequiredParamName : myOptionalParamNames) {
245                                if (!qualifiedParamNames.contains(nextRequiredParamName)) {
246                                        if (!unqualifiedNames.contains(nextRequiredParamName)) {
247                                                retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE);
248                                        }
249                                }
250                        }
251                }
252
253                return retVal;
254        }
255
256        /**
257         * Is this request a request for a normal search - Ie. not a named search, nor a compartment
258         * search, just a plain old search.
259         *
260         * @since 5.4.0
261         */
262        public static boolean isPlainSearchRequest(RequestDetails theRequest) {
263                if (theRequest.getId() != null) {
264                        return false;
265                }
266                if (isNotBlank(theRequest.getCompartmentName())) {
267                        return false;
268                }
269                return mightBeSearchRequest(theRequest);
270        }
271
272        private static boolean mightBeSearchRequest(RequestDetails theRequest) {
273                if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
274                        return false;
275                }
276                if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
277                        return false;
278                }
279                if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
280                        return false;
281                }
282                if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) {
283                        return false;
284                }
285                return true;
286        }
287
288        @Override
289        public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
290                if (myIdParamIndex != null) {
291                        theMethodParams[myIdParamIndex] = theRequest.getId();
292                }
293
294                Object response = invokeServerMethod(theRequest, theMethodParams);
295
296                return toResourceList(response);
297
298        }
299
300        @Override
301        protected boolean isAddContentLocationHeader() {
302                return false;
303        }
304
305
306        private boolean passesWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) {
307                if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
308                        return true;
309                }
310                for (String next : theQualifiedNames) {
311                        QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(next);
312                        if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
313                                return false;
314                        }
315                }
316                return true;
317        }
318
319        @Override
320        public String toString() {
321                return getMethod().toString();
322        }
323
324
325}