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.interceptor.api.HookParams;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.model.api.IResource;
029import ca.uhn.fhir.model.api.Include;
030import ca.uhn.fhir.parser.DataFormatException;
031import ca.uhn.fhir.rest.annotation.*;
032import ca.uhn.fhir.rest.api.MethodOutcome;
033import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
034import ca.uhn.fhir.rest.api.server.IBundleProvider;
035import ca.uhn.fhir.rest.api.server.IRestfulServer;
036import ca.uhn.fhir.rest.api.server.RequestDetails;
037import ca.uhn.fhir.rest.server.BundleProviders;
038import ca.uhn.fhir.rest.server.IResourceProvider;
039import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
040import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
041import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
042import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
043import ca.uhn.fhir.util.ReflectionUtil;
044import org.hl7.fhir.instance.model.api.IAnyResource;
045import org.hl7.fhir.instance.model.api.IBaseBundle;
046import org.hl7.fhir.instance.model.api.IBaseResource;
047
048import javax.annotation.Nonnull;
049import java.io.IOException;
050import java.lang.reflect.InvocationTargetException;
051import java.lang.reflect.Method;
052import java.util.ArrayList;
053import java.util.Collection;
054import java.util.HashSet;
055import java.util.List;
056import java.util.Set;
057import java.util.TreeSet;
058import java.util.stream.Collectors;
059
060import static org.apache.commons.lang3.StringUtils.isNotBlank;
061
062public abstract class BaseMethodBinding<T> {
063
064        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class);
065        private final List<BaseQueryParameter> myQueryParameters;
066        private FhirContext myContext;
067        private Method myMethod;
068        private List<IParameter> myParameters;
069        private Object myProvider;
070        private boolean mySupportsConditional;
071        private boolean mySupportsConditionalMultiple;
072        public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
073                assert theMethod != null;
074                assert theContext != null;
075
076                myMethod = theMethod;
077                myContext = theContext;
078                myProvider = theProvider;
079                myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType());
080                myQueryParameters = myParameters
081                        .stream()
082                        .filter(t -> t instanceof BaseQueryParameter)
083                        .map(t -> (BaseQueryParameter) t)
084                        .collect(Collectors.toList());
085
086                for (IParameter next : myParameters) {
087                        if (next instanceof ConditionalParamBinder) {
088                                mySupportsConditional = true;
089                                if (((ConditionalParamBinder) next).isSupportsMultiple()) {
090                                        mySupportsConditionalMultiple = true;
091                                }
092                                break;
093                        }
094                }
095
096                // This allows us to invoke methods on private classes
097                myMethod.setAccessible(true);
098        }
099
100        protected List<BaseQueryParameter> getQueryParameters() {
101                return myQueryParameters;
102        }
103
104        protected Object[] createMethodParams(RequestDetails theRequest) {
105                Object[] params = new Object[getParameters().size()];
106                for (int i = 0; i < getParameters().size(); i++) {
107                        IParameter param = getParameters().get(i);
108                        if (param != null) {
109                                params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
110                        }
111                }
112                return params;
113        }
114
115        protected Object[] createParametersForServerRequest(RequestDetails theRequest) {
116                Object[] params = new Object[getParameters().size()];
117                for (int i = 0; i < getParameters().size(); i++) {
118                        IParameter param = getParameters().get(i);
119                        if (param == null) {
120                                continue;
121                        }
122                        params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
123                }
124                return params;
125        }
126
127        /**
128         * Subclasses may override to declare that they apply to all resource types
129         */
130        public boolean isGlobalMethod() {
131                return false;
132        }
133
134        public List<Class<?>> getAllowableParamAnnotations() {
135                return null;
136        }
137
138        public FhirContext getContext() {
139                return myContext;
140        }
141
142        public Set<String> getIncludes() {
143                return doGetIncludesOrRevIncludes(false);
144        }
145
146        public Set<String> getRevIncludes() {
147                return doGetIncludesOrRevIncludes(true);
148        }
149
150        private Set<String> doGetIncludesOrRevIncludes(boolean reverse) {
151                Set<String> retVal = new TreeSet<>();
152                for (IParameter next : myParameters) {
153                        if (next instanceof IncludeParameter) {
154                                IncludeParameter includeParameter = (IncludeParameter) next;
155                                if (includeParameter.isReverse() == reverse) {
156                                        retVal.addAll(includeParameter.getAllow());
157                                }
158                        }
159                }
160                return retVal;
161        }
162
163        public Method getMethod() {
164                return myMethod;
165        }
166
167        public List<IParameter> getParameters() {
168                return myParameters;
169        }
170
171        /**
172         * For unit tests only
173         */
174        public void setParameters(List<IParameter> theParameters) {
175                myParameters = theParameters;
176        }
177
178        public Object getProvider() {
179                return myProvider;
180        }
181
182        @SuppressWarnings({"unchecked", "rawtypes"})
183        public Set<Include> getRequestIncludesFromParams(Object[] params) {
184                if (params == null || params.length == 0) {
185                        return null;
186                }
187                int index = 0;
188                boolean match = false;
189                for (IParameter parameter : myParameters) {
190                        if (parameter instanceof IncludeParameter) {
191                                match = true;
192                                break;
193                        }
194                        index++;
195                }
196                if (!match) {
197                        return null;
198                }
199                if (index >= params.length) {
200                        ourLog.warn("index out of parameter range (should never happen");
201                        return null;
202                }
203                if (params[index] instanceof Set) {
204                        return (Set<Include>) params[index];
205                }
206                if (params[index] instanceof Iterable) {
207                        Set includes = new HashSet<Include>();
208                        for (Object o : (Iterable) params[index]) {
209                                if (o instanceof Include) {
210                                        includes.add(o);
211                                }
212                        }
213                        return includes;
214                }
215                ourLog.warn("include params wasn't Set or Iterable, it was {}", params[index].getClass());
216                return null;
217        }
218
219        /**
220         * Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific
221         */
222        public abstract String getResourceName();
223
224        @Nonnull
225        public abstract RestOperationTypeEnum getRestOperationType();
226
227        /**
228         * Determine which operation is being fired for a specific request
229         *
230         * @param theRequestDetails The request
231         */
232        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
233                return getRestOperationType();
234        }
235
236        public abstract MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest);
237
238        public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException;
239
240        protected final Object invokeServerMethod(RequestDetails theRequest, Object[] theMethodParams) {
241                // Handle server action interceptors
242                RestOperationTypeEnum operationType = getRestOperationType(theRequest);
243                if (operationType != null) {
244
245                        ActionRequestDetails details = new ActionRequestDetails(theRequest);
246                        populateActionRequestDetailsForInterceptor(theRequest, details, theMethodParams);
247
248                        // Interceptor invoke: SERVER_INCOMING_REQUEST_PRE_HANDLED
249                        HookParams preHandledParams = new HookParams();
250                        preHandledParams.add(RestOperationTypeEnum.class, theRequest.getRestOperationType());
251                        preHandledParams.add(RequestDetails.class, theRequest);
252                        preHandledParams.addIfMatchesType(ServletRequestDetails.class, theRequest);
253                        preHandledParams.add(ActionRequestDetails.class, details);
254                        if (theRequest.getInterceptorBroadcaster() != null) {
255                                theRequest
256                                        .getInterceptorBroadcaster()
257                                        .callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, preHandledParams);
258                        }
259
260                }
261
262                // Actually invoke the method
263                try {
264                        Method method = getMethod();
265                        return method.invoke(getProvider(), theMethodParams);
266                } catch (InvocationTargetException e) {
267                        if (e.getCause() instanceof BaseServerResponseException) {
268                                throw (BaseServerResponseException) e.getCause();
269                        }
270                        if (e.getTargetException() instanceof DataFormatException) {
271                                throw (DataFormatException)e.getTargetException();
272                        }
273                        throw new InternalErrorException(Msg.code(389) + "Failed to call access method: " + e.getCause(), e);
274                } catch (Exception e) {
275                        throw new InternalErrorException(Msg.code(390) + "Failed to call access method: " + e.getCause(), e);
276                }
277        }
278
279        /**
280         * Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't actually support this paramter, so this will only return true occasionally.
281         */
282        public boolean isSupportsConditional() {
283                return mySupportsConditional;
284        }
285
286        /**
287         * Does this method support conditional operations over multiple objects (basically for conditional delete)
288         */
289        public boolean isSupportsConditionalMultiple() {
290                return mySupportsConditionalMultiple;
291        }
292
293        /**
294         * Subclasses may override this method (but should also call super) to provide method specifics to the
295         * interceptors.
296         *
297         * @param theRequestDetails The server request details
298         * @param theDetails        The details object to populate
299         * @param theMethodParams   The method params as generated by the specific method binding
300         */
301        protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
302                // nothing by default
303        }
304
305        protected IBundleProvider toResourceList(Object response) throws InternalErrorException {
306                if (response == null) {
307                        return BundleProviders.newEmptyList();
308                } else if (response instanceof IBundleProvider) {
309                        return (IBundleProvider) response;
310                } else if (response instanceof IBaseResource) {
311                        return BundleProviders.newList((IBaseResource) response);
312                } else if (response instanceof Collection) {
313                        List<IBaseResource> retVal = new ArrayList<IBaseResource>();
314                        for (Object next : ((Collection<?>) response)) {
315                                retVal.add((IBaseResource) next);
316                        }
317                        return BundleProviders.newList(retVal);
318                } else if (response instanceof MethodOutcome) {
319                        IBaseResource retVal = ((MethodOutcome) response).getOperationOutcome();
320                        if (retVal == null) {
321                                retVal = getContext().getResourceDefinition("OperationOutcome").newInstance();
322                        }
323                        return BundleProviders.newList(retVal);
324                } else {
325                        throw new InternalErrorException(Msg.code(391) + "Unexpected return type: " + response.getClass().getCanonicalName());
326                }
327        }
328
329        public void close() {
330                // subclasses may override
331        }
332
333        @SuppressWarnings("unchecked")
334        public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
335                Read read = theMethod.getAnnotation(Read.class);
336                Search search = theMethod.getAnnotation(Search.class);
337                Metadata conformance = theMethod.getAnnotation(Metadata.class);
338                Create create = theMethod.getAnnotation(Create.class);
339                Update update = theMethod.getAnnotation(Update.class);
340                Delete delete = theMethod.getAnnotation(Delete.class);
341                History history = theMethod.getAnnotation(History.class);
342                Validate validate = theMethod.getAnnotation(Validate.class);
343                AddTags addTags = theMethod.getAnnotation(AddTags.class);
344                DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class);
345                Transaction transaction = theMethod.getAnnotation(Transaction.class);
346                Operation operation = theMethod.getAnnotation(Operation.class);
347                GetPage getPage = theMethod.getAnnotation(GetPage.class);
348                Patch patch = theMethod.getAnnotation(Patch.class);
349                GraphQL graphQL = theMethod.getAnnotation(GraphQL.class);
350
351                // ** if you add another annotation above, also add it to the next line:
352                if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, addTags, deleteTags, transaction, operation, getPage, patch, graphQL)) {
353                        return null;
354                }
355
356                if (getPage != null) {
357                        return new PageMethodBinding(theContext, theMethod);
358                }
359
360                if (graphQL != null) {
361                        return new GraphQLMethodBinding(theMethod, graphQL.type(), theContext, theProvider);
362                }
363
364                Class<? extends IBaseResource> returnType;
365
366                Class<? extends IBaseResource> returnTypeFromRp = null;
367                if (theProvider instanceof IResourceProvider) {
368                        returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
369                        if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
370                                throw new ConfigurationException(Msg.code(392) + "getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned "
371                                        + toLogString(returnTypeFromRp) + " - Must return a resource type");
372                        }
373                }
374
375                Class<?> returnTypeFromMethod = theMethod.getReturnType();
376                if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) {
377                        // returns a method outcome
378                } else if (IBundleProvider.class.equals(returnTypeFromMethod)) {
379                        // returns a bundle provider
380                } else if (void.class.equals(returnTypeFromMethod)) {
381                        // returns a bundle
382                } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
383                        returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
384                        if (returnTypeFromMethod == null) {
385                                ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod);
386                        } else if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !isResourceInterface(returnTypeFromMethod)) {
387                                throw new ConfigurationException(Msg.code(393) + "Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
388                                        + " returns a collection with generic type " + toLogString(returnTypeFromMethod)
389                                        + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )");
390                        }
391                } else if (IBaseBundle.class.isAssignableFrom(returnTypeFromMethod) && returnTypeFromRp == null) {
392                        // If a plain provider method returns a Bundle, we'll assume it to be a system
393                        // level operation and not a type/instance level operation on the Bundle type.
394                        returnTypeFromMethod = null;
395                } else {
396                        if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
397                                throw new ConfigurationException(Msg.code(394) + "Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
398                                        + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, Bundle, " + IBundleProvider.class.getSimpleName()
399                                        + ", etc., see the documentation for more details)");
400                        }
401                }
402
403                Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class;
404                String returnTypeNameFromAnnotation = null;
405                if (read != null) {
406                        returnTypeFromAnnotation = read.type();
407                        returnTypeNameFromAnnotation = read.typeName();
408                } else if (search != null) {
409                        returnTypeFromAnnotation = search.type();
410                        returnTypeNameFromAnnotation = search.typeName();
411                } else if (history != null) {
412                        returnTypeFromAnnotation = history.type();
413                        returnTypeNameFromAnnotation = history.typeName();
414                } else if (delete != null) {
415                        returnTypeFromAnnotation = delete.type();
416                        returnTypeNameFromAnnotation = delete.typeName();
417                } else if (patch != null) {
418                        returnTypeFromAnnotation = patch.type();
419                        returnTypeNameFromAnnotation = patch.typeName();
420                } else if (create != null) {
421                        returnTypeFromAnnotation = create.type();
422                        returnTypeNameFromAnnotation = create.typeName();
423                } else if (update != null) {
424                        returnTypeFromAnnotation = update.type();
425                        returnTypeNameFromAnnotation = update.typeName();
426                } else if (validate != null) {
427                        returnTypeFromAnnotation = validate.type();
428                        returnTypeNameFromAnnotation = validate.typeName();
429                } else if (addTags != null) {
430                        returnTypeFromAnnotation = addTags.type();
431                        returnTypeNameFromAnnotation = addTags.typeName();
432                } else if (deleteTags != null) {
433                        returnTypeFromAnnotation = deleteTags.type();
434                        returnTypeNameFromAnnotation = deleteTags.typeName();
435                }
436
437                if (isNotBlank(returnTypeNameFromAnnotation)) {
438                        returnTypeFromAnnotation = theContext.getResourceDefinition(returnTypeNameFromAnnotation).getImplementingClass();
439                }
440
441                if (returnTypeFromRp != null) {
442                        if (returnTypeFromAnnotation != null && !isResourceInterface(returnTypeFromAnnotation)) {
443                                if (returnTypeFromMethod != null && !returnTypeFromRp.isAssignableFrom(returnTypeFromMethod)) {
444                                        throw new ConfigurationException(Msg.code(395) + "Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type "
445                                                + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
446                                }
447                                if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
448                                        throw new ConfigurationException(Msg.code(396) + "Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName()
449                                                        + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
450                                }
451                                returnType = returnTypeFromAnnotation;
452                        } else {
453                                returnType = returnTypeFromRp;
454                        }
455                } else {
456                        if (!isResourceInterface(returnTypeFromAnnotation)) {
457                                if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
458                                        throw new ConfigurationException(Msg.code(397) + "Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
459                                                + " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type");
460                                }
461                                returnType = returnTypeFromAnnotation;
462                        } else {
463                                returnType = (Class<? extends IBaseResource>) returnTypeFromMethod;
464                        }
465                }
466
467                if (read != null) {
468                        return new ReadMethodBinding(returnType, theMethod, theContext, theProvider);
469                } else if (search != null) {
470                        return new SearchMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider);
471                } else if (conformance != null) {
472                        return new ConformanceMethodBinding(theMethod, theContext, theProvider);
473                } else if (create != null) {
474                        return new CreateMethodBinding(theMethod, theContext, theProvider);
475                } else if (update != null) {
476                        return new UpdateMethodBinding(theMethod, theContext, theProvider);
477                } else if (delete != null) {
478                        return new DeleteMethodBinding(theMethod, theContext, theProvider);
479                } else if (patch != null) {
480                        return new PatchMethodBinding(theMethod, theContext, theProvider);
481                } else if (history != null) {
482                        return new HistoryMethodBinding(theMethod, theContext, theProvider);
483                } else if (validate != null) {
484                        return new ValidateMethodBindingDstu2Plus(returnType, returnTypeFromRp, theMethod, theContext, theProvider, validate);
485                } else if (transaction != null) {
486                        return new TransactionMethodBinding(theMethod, theContext, theProvider);
487                } else if (operation != null) {
488                        return new OperationMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation);
489                } else {
490                        throw new ConfigurationException(Msg.code(398) + "Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
491                }
492
493        }
494
495        private static boolean isResourceInterface(Class<?> theReturnTypeFromMethod) {
496                return theReturnTypeFromMethod != null && (theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class));
497        }
498
499        private static String toLogString(Class<?> theType) {
500                if (theType == null) {
501                        return null;
502                }
503                return theType.getCanonicalName();
504        }
505
506        private static boolean verifyIsValidResourceReturnType(Class<?> theReturnType) {
507                if (theReturnType == null) {
508                        return false;
509                }
510                if (!IBaseResource.class.isAssignableFrom(theReturnType)) {
511                        return false;
512                }
513                return true;
514        }
515
516        public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) {
517                Object obj1 = null;
518                for (Object object : theAnnotations) {
519                        if (object != null) {
520                                if (obj1 == null) {
521                                        obj1 = object;
522                                } else {
523                                        throw new ConfigurationException(Msg.code(399) + "Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @"
524                                                + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
525                                }
526
527                        }
528                }
529                if (obj1 == null) {
530                        return false;
531                }
532                return true;
533        }
534
535}