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