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