001package ca.uhn.fhir.rest.server.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2021 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.context.ConfigurationException;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.interceptor.api.HookParams;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.model.api.IResource;
028import ca.uhn.fhir.model.api.Include;
029import ca.uhn.fhir.parser.DataFormatException;
030import ca.uhn.fhir.rest.annotation.*;
031import ca.uhn.fhir.rest.api.MethodOutcome;
032import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
033import ca.uhn.fhir.rest.api.server.IBundleProvider;
034import ca.uhn.fhir.rest.api.server.IRestfulServer;
035import ca.uhn.fhir.rest.api.server.RequestDetails;
036import ca.uhn.fhir.rest.server.BundleProviders;
037import ca.uhn.fhir.rest.server.IResourceProvider;
038import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
039import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
040import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
041import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
042import ca.uhn.fhir.util.ReflectionUtil;
043import org.hl7.fhir.instance.model.api.IAnyResource;
044import org.hl7.fhir.instance.model.api.IBaseBundle;
045import org.hl7.fhir.instance.model.api.IBaseResource;
046
047import javax.annotation.Nonnull;
048import java.io.IOException;
049import java.lang.reflect.InvocationTargetException;
050import java.lang.reflect.Method;
051import java.util.ArrayList;
052import java.util.Collection;
053import java.util.HashSet;
054import java.util.List;
055import java.util.Set;
056import java.util.TreeSet;
057import java.util.stream.Collectors;
058
059import static org.apache.commons.lang3.StringUtils.isNotBlank;
060
061public abstract class BaseMethodBinding<T> {
062
063        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class);
064        private final List<BaseQueryParameter> myQueryParameters;
065        private FhirContext myContext;
066        private Method myMethod;
067        private List<IParameter> myParameters;
068        private Object myProvider;
069        private boolean mySupportsConditional;
070        private boolean mySupportsConditionalMultiple;
071        public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
072                assert theMethod != null;
073                assert theContext != null;
074
075                myMethod = theMethod;
076                myContext = theContext;
077                myProvider = theProvider;
078                myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType());
079                myQueryParameters = myParameters
080                        .stream()
081                        .filter(t -> t instanceof BaseQueryParameter)
082                        .map(t -> (BaseQueryParameter) t)
083                        .collect(Collectors.toList());
084
085                for (IParameter next : myParameters) {
086                        if (next instanceof ConditionalParamBinder) {
087                                mySupportsConditional = true;
088                                if (((ConditionalParamBinder) next).isSupportsMultiple()) {
089                                        mySupportsConditionalMultiple = true;
090                                }
091                                break;
092                        }
093                }
094
095                // This allows us to invoke methods on private classes
096                myMethod.setAccessible(true);
097        }
098
099        protected List<BaseQueryParameter> getQueryParameters() {
100                return myQueryParameters;
101        }
102
103        protected Object[] createMethodParams(RequestDetails theRequest) {
104                Object[] params = new Object[getParameters().size()];
105                for (int i = 0; i < getParameters().size(); i++) {
106                        IParameter param = getParameters().get(i);
107                        if (param != null) {
108                                params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
109                        }
110                }
111                return params;
112        }
113
114        protected Object[] createParametersForServerRequest(RequestDetails theRequest) {
115                Object[] params = new Object[getParameters().size()];
116                for (int i = 0; i < getParameters().size(); i++) {
117                        IParameter param = getParameters().get(i);
118                        if (param == null) {
119                                continue;
120                        }
121                        params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
122                }
123                return params;
124        }
125
126        /**
127         * Subclasses may override to declare that they apply to all resource types
128         */
129        public boolean isGlobalMethod() {
130                return false;
131        }
132
133        public List<Class<?>> getAllowableParamAnnotations() {
134                return null;
135        }
136
137        public FhirContext getContext() {
138                return myContext;
139        }
140
141        public Set<String> getIncludes() {
142                return doGetIncludesOrRevIncludes(false);
143        }
144
145        public Set<String> getRevIncludes() {
146                return doGetIncludesOrRevIncludes(true);
147        }
148
149        private Set<String> doGetIncludesOrRevIncludes(boolean reverse) {
150                Set<String> retVal = new TreeSet<>();
151                for (IParameter next : myParameters) {
152                        if (next instanceof IncludeParameter) {
153                                IncludeParameter includeParameter = (IncludeParameter) next;
154                                if (includeParameter.isReverse() == reverse) {
155                                        retVal.addAll(includeParameter.getAllow());
156                                }
157                        }
158                }
159                return retVal;
160        }
161
162        public Method getMethod() {
163                return myMethod;
164        }
165
166        public List<IParameter> getParameters() {
167                return myParameters;
168        }
169
170        /**
171         * For unit tests only
172         */
173        public void setParameters(List<IParameter> theParameters) {
174                myParameters = theParameters;
175        }
176
177        public Object getProvider() {
178                return myProvider;
179        }
180
181        @SuppressWarnings({"unchecked", "rawtypes"})
182        public Set<Include> getRequestIncludesFromParams(Object[] params) {
183                if (params == null || params.length == 0) {
184                        return null;
185                }
186                int index = 0;
187                boolean match = false;
188                for (IParameter parameter : myParameters) {
189                        if (parameter instanceof IncludeParameter) {
190                                match = true;
191                                break;
192                        }
193                        index++;
194                }
195                if (!match) {
196                        return null;
197                }
198                if (index >= params.length) {
199                        ourLog.warn("index out of parameter range (should never happen");
200                        return null;
201                }
202                if (params[index] instanceof Set) {
203                        return (Set<Include>) params[index];
204                }
205                if (params[index] instanceof Iterable) {
206                        Set includes = new HashSet<Include>();
207                        for (Object o : (Iterable) params[index]) {
208                                if (o instanceof Include) {
209                                        includes.add(o);
210                                }
211                        }
212                        return includes;
213                }
214                ourLog.warn("include params wasn't Set or Iterable, it was {}", params[index].getClass());
215                return null;
216        }
217
218        /**
219         * Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific
220         */
221        public abstract String getResourceName();
222
223        @Nonnull
224        public abstract RestOperationTypeEnum getRestOperationType();
225
226        /**
227         * Determine which operation is being fired for a specific request
228         *
229         * @param theRequestDetails The request
230         */
231        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
232                return getRestOperationType();
233        }
234
235        public abstract MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest);
236
237        public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException;
238
239        protected final Object invokeServerMethod(RequestDetails theRequest, Object[] theMethodParams) {
240                // Handle server action interceptors
241                RestOperationTypeEnum operationType = getRestOperationType(theRequest);
242                if (operationType != null) {
243
244                        ActionRequestDetails details = new ActionRequestDetails(theRequest);
245                        populateActionRequestDetailsForInterceptor(theRequest, details, theMethodParams);
246
247                        // Interceptor invoke: SERVER_INCOMING_REQUEST_PRE_HANDLED
248                        HookParams preHandledParams = new HookParams();
249                        preHandledParams.add(RestOperationTypeEnum.class, theRequest.getRestOperationType());
250                        preHandledParams.add(RequestDetails.class, theRequest);
251                        preHandledParams.addIfMatchesType(ServletRequestDetails.class, theRequest);
252                        preHandledParams.add(ActionRequestDetails.class, details);
253                        if (theRequest.getInterceptorBroadcaster() != null) {
254                                theRequest
255                                        .getInterceptorBroadcaster()
256                                        .callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, preHandledParams);
257                        }
258
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("Failed to call access method: " + e.getCause(), e);
273                } catch (Exception e) {
274                        throw new InternalErrorException("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         *
296         * @param theRequestDetails The server request details
297         * @param theDetails        The details object to populate
298         * @param theMethodParams   The method params as generated by the specific method binding
299         */
300        protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
301                // nothing by default
302        }
303
304        protected IBundleProvider toResourceList(Object response) throws InternalErrorException {
305                if (response == null) {
306                        return BundleProviders.newEmptyList();
307                } else if (response instanceof IBundleProvider) {
308                        return (IBundleProvider) response;
309                } else if (response instanceof IBaseResource) {
310                        return BundleProviders.newList((IBaseResource) response);
311                } else if (response instanceof Collection) {
312                        List<IBaseResource> retVal = new ArrayList<IBaseResource>();
313                        for (Object next : ((Collection<?>) response)) {
314                                retVal.add((IBaseResource) next);
315                        }
316                        return BundleProviders.newList(retVal);
317                } else if (response instanceof MethodOutcome) {
318                        IBaseResource retVal = ((MethodOutcome) response).getOperationOutcome();
319                        if (retVal == null) {
320                                retVal = getContext().getResourceDefinition("OperationOutcome").newInstance();
321                        }
322                        return BundleProviders.newList(retVal);
323                } else {
324                        throw new InternalErrorException("Unexpected return type: " + response.getClass().getCanonicalName());
325                }
326        }
327
328        public void close() {
329                // subclasses may override
330        }
331
332        @SuppressWarnings("unchecked")
333        public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
334                Read read = theMethod.getAnnotation(Read.class);
335                Search search = theMethod.getAnnotation(Search.class);
336                Metadata conformance = theMethod.getAnnotation(Metadata.class);
337                Create create = theMethod.getAnnotation(Create.class);
338                Update update = theMethod.getAnnotation(Update.class);
339                Delete delete = theMethod.getAnnotation(Delete.class);
340                History history = theMethod.getAnnotation(History.class);
341                Validate validate = theMethod.getAnnotation(Validate.class);
342                AddTags addTags = theMethod.getAnnotation(AddTags.class);
343                DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class);
344                Transaction transaction = theMethod.getAnnotation(Transaction.class);
345                Operation operation = theMethod.getAnnotation(Operation.class);
346                GetPage getPage = theMethod.getAnnotation(GetPage.class);
347                Patch patch = theMethod.getAnnotation(Patch.class);
348                GraphQL graphQL = theMethod.getAnnotation(GraphQL.class);
349
350                // ** if you add another annotation above, also add it to the next line:
351                if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, addTags, deleteTags, transaction, operation, getPage, patch, graphQL)) {
352                        return null;
353                }
354
355                if (getPage != null) {
356                        return new PageMethodBinding(theContext, theMethod);
357                }
358
359                if (graphQL != null) {
360                        return new GraphQLMethodBinding(theMethod, graphQL.type(), theContext, theProvider);
361                }
362
363                Class<? extends IBaseResource> returnType;
364
365                Class<? extends IBaseResource> returnTypeFromRp = null;
366                if (theProvider instanceof IResourceProvider) {
367                        returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
368                        if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
369                                throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned "
370                                        + toLogString(returnTypeFromRp) + " - Must return a resource type");
371                        }
372                }
373
374                Class<?> returnTypeFromMethod = theMethod.getReturnType();
375                if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) {
376                        // returns a method outcome
377                } else if (IBundleProvider.class.equals(returnTypeFromMethod)) {
378                        // returns a bundle provider
379                } else if (void.class.equals(returnTypeFromMethod)) {
380                        // returns a bundle
381                } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
382                        returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
383                        if (returnTypeFromMethod == null) {
384                                ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod);
385                        } else if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !isResourceInterface(returnTypeFromMethod)) {
386                                throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
387                                        + " returns a collection with generic type " + toLogString(returnTypeFromMethod)
388                                        + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )");
389                        }
390                } else if (IBaseBundle.class.isAssignableFrom(returnTypeFromMethod) && returnTypeFromRp == null) {
391                        // If a plain provider method returns a Bundle, we'll assume it to be a system
392                        // level operation and not a type/instance level operation on the Bundle type.
393                        returnTypeFromMethod = null;
394                } else {
395                        if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
396                                throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
397                                        + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, Bundle, " + IBundleProvider.class.getSimpleName()
398                                        + ", etc., see the documentation for more details)");
399                        }
400                }
401
402                Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class;
403                String returnTypeNameFromAnnotation = null;
404                if (read != null) {
405                        returnTypeFromAnnotation = read.type();
406                        returnTypeNameFromAnnotation = read.typeName();
407                } else if (search != null) {
408                        returnTypeFromAnnotation = search.type();
409                        returnTypeNameFromAnnotation = search.typeName();
410                } else if (history != null) {
411                        returnTypeFromAnnotation = history.type();
412                        returnTypeNameFromAnnotation = history.typeName();
413                } else if (delete != null) {
414                        returnTypeFromAnnotation = delete.type();
415                        returnTypeNameFromAnnotation = delete.typeName();
416                } else if (patch != null) {
417                        returnTypeFromAnnotation = patch.type();
418                        returnTypeNameFromAnnotation = patch.typeName();
419                } else if (create != null) {
420                        returnTypeFromAnnotation = create.type();
421                        returnTypeNameFromAnnotation = create.typeName();
422                } else if (update != null) {
423                        returnTypeFromAnnotation = update.type();
424                        returnTypeNameFromAnnotation = update.typeName();
425                } else if (validate != null) {
426                        returnTypeFromAnnotation = validate.type();
427                        returnTypeNameFromAnnotation = validate.typeName();
428                } else if (addTags != null) {
429                        returnTypeFromAnnotation = addTags.type();
430                        returnTypeNameFromAnnotation = addTags.typeName();
431                } else if (deleteTags != null) {
432                        returnTypeFromAnnotation = deleteTags.type();
433                        returnTypeNameFromAnnotation = deleteTags.typeName();
434                }
435
436                if (isNotBlank(returnTypeNameFromAnnotation)) {
437                        returnTypeFromAnnotation = theContext.getResourceDefinition(returnTypeNameFromAnnotation).getImplementingClass();
438                }
439
440                if (returnTypeFromRp != null) {
441                        if (returnTypeFromAnnotation != null && !isResourceInterface(returnTypeFromAnnotation)) {
442                                if (returnTypeFromMethod != null && !returnTypeFromRp.isAssignableFrom(returnTypeFromMethod)) {
443                                        throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type "
444                                                + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
445                                }
446                                if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
447                                        throw new ConfigurationException(
448                                                "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("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("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("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}