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.model.base.resource.BaseOperationOutcome;
030import ca.uhn.fhir.parser.IParser;
031import ca.uhn.fhir.rest.annotation.*;
032import ca.uhn.fhir.rest.api.Constants;
033import ca.uhn.fhir.rest.api.EncodingEnum;
034import ca.uhn.fhir.rest.api.MethodOutcome;
035import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
036import ca.uhn.fhir.rest.api.server.IBundleProvider;
037import ca.uhn.fhir.rest.api.server.IRestfulServer;
038import ca.uhn.fhir.rest.api.server.RequestDetails;
039import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
040import ca.uhn.fhir.rest.server.BundleProviders;
041import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider;
042import ca.uhn.fhir.rest.server.IResourceProvider;
043import ca.uhn.fhir.rest.server.exceptions.*;
044import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
045import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
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 IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, List<Class<? extends IBaseResource>> thePreferTypes) {
094                EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType);
095                if (encoding == null) {
096                        NonFhirResponseException ex = NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader);
097                        populateException(ex, theResponseReader);
098                        throw ex;
099                }
100
101                IParser parser = encoding.newParser(getContext());
102                
103                parser.setPreferTypes(thePreferTypes);
104                
105                return parser;
106        }
107
108        protected IParser createAppropriateParserForParsingServerRequest(RequestDetails theRequest) {
109                String contentTypeHeader = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
110                EncodingEnum encoding;
111                if (isBlank(contentTypeHeader)) {
112                        encoding = EncodingEnum.XML;
113                } else {
114                        int semicolon = contentTypeHeader.indexOf(';');
115                        if (semicolon != -1) {
116                                contentTypeHeader = contentTypeHeader.substring(0, semicolon);
117                        }
118                        encoding = EncodingEnum.forContentType(contentTypeHeader);
119                }
120
121                if (encoding == null) {
122                        throw new InvalidRequestException("Request contins non-FHIR conent-type header value: " + contentTypeHeader);
123                }
124
125                IParser parser = encoding.newParser(getContext());
126                return parser;
127        }
128
129        protected Object[] createMethodParams(RequestDetails theRequest) {
130                Object[] params = new Object[getParameters().size()];
131                for (int i = 0; i < getParameters().size(); i++) {
132                        IParameter param = getParameters().get(i);
133                        if (param != null) {
134                                params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
135                        }
136                }
137                return params;
138        }
139
140        protected Object[] createParametersForServerRequest(RequestDetails theRequest) {
141                Object[] params = new Object[getParameters().size()];
142                for (int i = 0; i < getParameters().size(); i++) {
143                        IParameter param = getParameters().get(i);
144                        if (param == null) {
145                                continue;
146                        }
147                        params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this);
148                }
149                return params;
150        }
151
152        /**
153         * Subclasses may override to declare that they apply to all resource types
154         */
155        public boolean isGlobalMethod() {
156                return false;
157        }
158
159        public List<Class<?>> getAllowableParamAnnotations() {
160                return null;
161        }
162
163        public FhirContext getContext() {
164                return myContext;
165        }
166
167        public Set<String> getIncludes() {
168                Set<String> retVal = new TreeSet<String>();
169                for (IParameter next : myParameters) {
170                        if (next instanceof IncludeParameter) {
171                                retVal.addAll(((IncludeParameter) next).getAllow());
172                        }
173                }
174                return retVal;
175        }
176
177        public Method getMethod() {
178                return myMethod;
179        }
180
181        public List<IParameter> getParameters() {
182                return myParameters;
183        }
184
185        public Object getProvider() {
186                return myProvider;
187        }
188
189        @SuppressWarnings({ "unchecked", "rawtypes" })
190        public Set<Include> getRequestIncludesFromParams(Object[] params) {
191                if (params == null || params.length == 0) {
192                        return null;
193                }
194                int index = 0;
195                boolean match = false;
196                for (IParameter parameter : myParameters) {
197                        if (parameter instanceof IncludeParameter) {
198                                match = true;
199                                break;
200                        }
201                        index++;
202                }
203                if (!match) {
204                        return null;
205                }
206                if (index >= params.length) {
207                        ourLog.warn("index out of parameter range (should never happen");
208                        return null;
209                }
210                if (params[index] instanceof Set) {
211                        return (Set<Include>) params[index];
212                }
213                if (params[index] instanceof Iterable) {
214                        Set includes = new HashSet<Include>();
215                        for (Object o : (Iterable) params[index]) {
216                                if (o instanceof Include) {
217                                        includes.add(o);
218                                }
219                        }
220                        return includes;
221                }
222                ourLog.warn("include params wasn't Set or Iterable, it was {}", params[index].getClass());
223                return null;
224        }
225
226        /**
227         * Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific
228         */
229        public abstract String getResourceName();
230
231        @Nonnull
232        public abstract RestOperationTypeEnum getRestOperationType();
233
234        /**
235         * Determine which operation is being fired for a specific request
236         * 
237         * @param theRequestDetails
238         *           The request
239         */
240        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
241                return getRestOperationType();
242        }
243
244        public abstract boolean incomingServerRequestMatchesMethod(RequestDetails theRequest);
245
246        public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException;
247
248        protected final Object invokeServerMethod(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) {
249                // Handle server action interceptors
250                RestOperationTypeEnum operationType = getRestOperationType(theRequest);
251                if (operationType != null) {
252                        ActionRequestDetails details = new ActionRequestDetails(theRequest);
253                        populateActionRequestDetailsForInterceptor(theRequest, details, theMethodParams);
254                        HookParams preHandledParams = new HookParams();
255                        preHandledParams.add(RestOperationTypeEnum.class, operationType);
256                        preHandledParams.add(ActionRequestDetails.class, details);
257                        if (theRequest.getInterceptorBroadcaster() != null) {
258                                theRequest
259                                        .getInterceptorBroadcaster()
260                                        .callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, preHandledParams);
261                        }
262                }
263
264                // Actually invoke the method
265                try {
266                        Method method = getMethod();
267                        return method.invoke(getProvider(), theMethodParams);
268                } catch (InvocationTargetException e) {
269                        if (e.getCause() instanceof BaseServerResponseException) {
270                                throw (BaseServerResponseException) e.getCause();
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.{@link #populateActionRequestDetailsForInterceptor(RequestDetails, ActionRequestDetails, Object[])} to provide method specifics to the
294         * interceptors.
295         * 
296         * @param theRequestDetails
297         *           The server request details
298         * @param theDetails
299         *           The details object to populate
300         * @param theMethodParams
301         *           The method params as generated by the specific method binding
302         */
303        protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
304                // nothing by default
305        }
306
307        protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(int theStatusCode, String theResponseMimeType, Reader theResponseReader) {
308                BaseServerResponseException ex;
309                switch (theStatusCode) {
310                case Constants.STATUS_HTTP_400_BAD_REQUEST:
311                        ex = new InvalidRequestException("Server responded with HTTP 400");
312                        break;
313                case Constants.STATUS_HTTP_404_NOT_FOUND:
314                        ex = new ResourceNotFoundException("Server responded with HTTP 404");
315                        break;
316                case Constants.STATUS_HTTP_405_METHOD_NOT_ALLOWED:
317                        ex = new MethodNotAllowedException("Server responded with HTTP 405");
318                        break;
319                case Constants.STATUS_HTTP_409_CONFLICT:
320                        ex = new ResourceVersionConflictException("Server responded with HTTP 409");
321                        break;
322                case Constants.STATUS_HTTP_412_PRECONDITION_FAILED:
323                        ex = new PreconditionFailedException("Server responded with HTTP 412");
324                        break;
325                case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY:
326                        IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theStatusCode, null);
327                        // TODO: handle if something other than OO comes back
328                        BaseOperationOutcome operationOutcome = (BaseOperationOutcome) parser.parseResource(theResponseReader);
329                        ex = new UnprocessableEntityException(myContext, operationOutcome);
330                        break;
331                default:
332                        ex = new UnclassifiedServerFailureException(theStatusCode, "Server responded with HTTP " + theStatusCode);
333                        break;
334                }
335
336                populateException(ex, theResponseReader);
337                return ex;
338        }
339
340        /** For unit tests only */
341        public void setParameters(List<IParameter> theParameters) {
342                myParameters = theParameters;
343        }
344
345        protected IBundleProvider toResourceList(Object response) throws InternalErrorException {
346                if (response == null) {
347                        return BundleProviders.newEmptyList();
348                } else if (response instanceof IBundleProvider) {
349                        return (IBundleProvider) response;
350                } else if (response instanceof IBaseResource) {
351                        return BundleProviders.newList((IBaseResource) response);
352                } else if (response instanceof Collection) {
353                        List<IBaseResource> retVal = new ArrayList<IBaseResource>();
354                        for (Object next : ((Collection<?>) response)) {
355                                retVal.add((IBaseResource) next);
356                        }
357                        return BundleProviders.newList(retVal);
358                } else if (response instanceof MethodOutcome) {
359                        IBaseResource retVal = ((MethodOutcome) response).getOperationOutcome();
360                        if (retVal == null) {
361                                retVal = getContext().getResourceDefinition("OperationOutcome").newInstance();
362                        }
363                        return BundleProviders.newList(retVal);
364                } else {
365                        throw new InternalErrorException("Unexpected return type: " + response.getClass().getCanonicalName());
366                }
367        }
368
369        @SuppressWarnings("unchecked")
370        public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
371                Read read = theMethod.getAnnotation(Read.class);
372                Search search = theMethod.getAnnotation(Search.class);
373                Metadata conformance = theMethod.getAnnotation(Metadata.class);
374                Create create = theMethod.getAnnotation(Create.class);
375                Update update = theMethod.getAnnotation(Update.class);
376                Delete delete = theMethod.getAnnotation(Delete.class);
377                History history = theMethod.getAnnotation(History.class);
378                Validate validate = theMethod.getAnnotation(Validate.class);
379                AddTags addTags = theMethod.getAnnotation(AddTags.class);
380                DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class);
381                Transaction transaction = theMethod.getAnnotation(Transaction.class);
382                Operation operation = theMethod.getAnnotation(Operation.class);
383                GetPage getPage = theMethod.getAnnotation(GetPage.class);
384                Patch patch = theMethod.getAnnotation(Patch.class);
385                GraphQL graphQL = theMethod.getAnnotation(GraphQL.class);
386
387                // ** if you add another annotation above, also add it to the next line:
388                if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, addTags, deleteTags, transaction, operation, getPage, patch, graphQL)) {
389                        return null;
390                }
391
392                if (getPage != null) {
393                        return new PageMethodBinding(theContext, theMethod);
394                }
395
396                if (graphQL != null) {
397                        return new GraphQLMethodBinding(theMethod, theContext, theProvider);
398                }
399
400                Class<? extends IBaseResource> returnType;
401
402                Class<? extends IBaseResource> returnTypeFromRp = null;
403                if (theProvider instanceof IResourceProvider) {
404                        returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
405                        if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
406                                throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned "
407                                                + toLogString(returnTypeFromRp) + " - Must return a resource type");
408                        }
409                }
410
411                Class<?> returnTypeFromMethod = theMethod.getReturnType();
412                if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) {
413                        // returns a method outcome
414                } else if (IBundleProvider.class.equals(returnTypeFromMethod)) {
415                        // returns a bundle provider
416                } else if (void.class.equals(returnTypeFromMethod)) {
417                        // returns a bundle
418                } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
419                        returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
420                        if (returnTypeFromMethod == null) {
421                                ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod);
422                        } else if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !isResourceInterface(returnTypeFromMethod)) {
423                                throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
424                                                + " returns a collection with generic type " + toLogString(returnTypeFromMethod)
425                                                + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )");
426                        }
427                } else {
428                        if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
429                                throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
430                                                + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, Bundle, " + IBundleProvider.class.getSimpleName()
431                                                + ", etc., see the documentation for more details)");
432                        }
433                }
434
435                Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class;
436                if (read != null) {
437                        returnTypeFromAnnotation = read.type();
438                } else if (search != null) {
439                        returnTypeFromAnnotation = search.type();
440                } else if (history != null) {
441                        returnTypeFromAnnotation = history.type();
442                } else if (delete != null) {
443                        returnTypeFromAnnotation = delete.type();
444                } else if (patch != null) {
445                        returnTypeFromAnnotation = patch.type();
446                } else if (create != null) {
447                        returnTypeFromAnnotation = create.type();
448                } else if (update != null) {
449                        returnTypeFromAnnotation = update.type();
450                } else if (validate != null) {
451                        returnTypeFromAnnotation = validate.type();
452                } else if (addTags != null) {
453                        returnTypeFromAnnotation = addTags.type();
454                } else if (deleteTags != null) {
455                        returnTypeFromAnnotation = deleteTags.type();
456                }
457
458                if (returnTypeFromRp != null) {
459                        if (returnTypeFromAnnotation != null && !isResourceInterface(returnTypeFromAnnotation)) {
460                                if (returnTypeFromMethod != null && !returnTypeFromRp.isAssignableFrom(returnTypeFromMethod)) {
461                                        throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type "
462                                                        + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
463                                }
464                                if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
465                                        throw new ConfigurationException(
466                                                        "Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName()
467                                                                        + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
468                                }
469                                returnType = returnTypeFromAnnotation;
470                        } else {
471                                returnType = returnTypeFromRp;
472                        }
473                } else {
474                        if (!isResourceInterface(returnTypeFromAnnotation)) {
475                                if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
476                                        throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
477                                                        + " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type");
478                                }
479                                returnType = returnTypeFromAnnotation;
480                        } else {
481                                returnType = (Class<? extends IBaseResource>) returnTypeFromMethod;
482                        }
483                }
484
485                if (read != null) {
486                        return new ReadMethodBinding(returnType, theMethod, theContext, theProvider);
487                } else if (search != null) {
488                        return new SearchMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider);
489                } else if (conformance != null) {
490                        return new ConformanceMethodBinding(theMethod, theContext, theProvider);
491                } else if (create != null) {
492                        return new CreateMethodBinding(theMethod, theContext, theProvider);
493                } else if (update != null) {
494                        return new UpdateMethodBinding(theMethod, theContext, theProvider);
495                } else if (delete != null) {
496                        return new DeleteMethodBinding(theMethod, theContext, theProvider);
497                } else if (patch != null) {
498                        return new PatchMethodBinding(theMethod, theContext, theProvider);
499                } else if (history != null) {
500                        return new HistoryMethodBinding(theMethod, theContext, theProvider);
501                } else if (validate != null) {
502                        return new ValidateMethodBindingDstu2Plus(returnType, returnTypeFromRp, theMethod, theContext, theProvider, validate);
503                } else if (transaction != null) {
504                        return new TransactionMethodBinding(theMethod, theContext, theProvider);
505                } else if (operation != null) {
506                        return new OperationMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation);
507                } else {
508                        throw new ConfigurationException("Did not detect any FHIR annotations on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
509                }
510
511        }
512
513        private static boolean isResourceInterface(Class<?> theReturnTypeFromMethod) {
514                return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class);
515        }
516
517        private static void populateException(BaseServerResponseException theEx, Reader theResponseReader) {
518                try {
519                        String responseText = IOUtils.toString(theResponseReader);
520                        theEx.setResponseBody(responseText);
521                } catch (IOException e) {
522                        ourLog.debug("Failed to read response", e);
523                }
524        }
525
526        private static String toLogString(Class<?> theType) {
527                if (theType == null) {
528                        return null;
529                }
530                return theType.getCanonicalName();
531        }
532
533        private static boolean verifyIsValidResourceReturnType(Class<?> theReturnType) {
534                if (theReturnType == null) {
535                        return false;
536                }
537                if (!IBaseResource.class.isAssignableFrom(theReturnType)) {
538                        return false;
539                }
540                return true;
541        }
542
543        public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) {
544                Object obj1 = null;
545                for (Object object : theAnnotations) {
546                        if (object != null) {
547                                if (obj1 == null) {
548                                        obj1 = object;
549                                } else {
550                                        throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @"
551                                                        + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
552                                }
553
554                        }
555                }
556                if (obj1 == null) {
557                        return false;
558                }
559                return true;
560        }
561
562}