001/*-
002 * #%L
003 * HAPI FHIR - Client 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.client.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.base.resource.BaseOperationOutcome;
027import ca.uhn.fhir.parser.IParser;
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.History;
034import ca.uhn.fhir.rest.annotation.Metadata;
035import ca.uhn.fhir.rest.annotation.Operation;
036import ca.uhn.fhir.rest.annotation.Patch;
037import ca.uhn.fhir.rest.annotation.Read;
038import ca.uhn.fhir.rest.annotation.Search;
039import ca.uhn.fhir.rest.annotation.Transaction;
040import ca.uhn.fhir.rest.annotation.Update;
041import ca.uhn.fhir.rest.annotation.Validate;
042import ca.uhn.fhir.rest.api.Constants;
043import ca.uhn.fhir.rest.api.EncodingEnum;
044import ca.uhn.fhir.rest.api.MethodOutcome;
045import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
046import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
047import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation;
048import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
049import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
050import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
051import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
052import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
053import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
054import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
055import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
056import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
057import ca.uhn.fhir.util.ReflectionUtil;
058import org.apache.commons.io.IOUtils;
059import org.hl7.fhir.instance.model.api.IAnyResource;
060import org.hl7.fhir.instance.model.api.IBaseResource;
061
062import java.io.IOException;
063import java.io.InputStream;
064import java.lang.reflect.Method;
065import java.util.Collection;
066import java.util.List;
067import java.util.Set;
068import java.util.TreeSet;
069
070public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T> {
071
072        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class);
073        private FhirContext myContext;
074        private Method myMethod;
075        private List<IParameter> myParameters;
076        private Object myProvider;
077        private boolean mySupportsConditional;
078        private boolean mySupportsConditionalMultiple;
079
080        public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
081                assert theMethod != null;
082                assert theContext != null;
083
084                myMethod = theMethod;
085                myContext = theContext;
086                myProvider = theProvider;
087                myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getRestOperationType());
088
089                for (IParameter next : myParameters) {
090                        if (next instanceof ConditionalParamBinder) {
091                                mySupportsConditional = true;
092                                if (((ConditionalParamBinder) next).isSupportsMultiple()) {
093                                        mySupportsConditionalMultiple = true;
094                                }
095                                break;
096                        }
097                }
098        }
099
100        protected IParser createAppropriateParserForParsingResponse(
101                        String theResponseMimeType,
102                        InputStream theResponseInputStream,
103                        int theResponseStatusCode,
104                        List<Class<? extends IBaseResource>> thePreferTypes) {
105                EncodingEnum encoding = EncodingEnum.forContentType(theResponseMimeType);
106                if (encoding == null) {
107                        NonFhirResponseException ex = NonFhirResponseException.newInstance(
108                                        theResponseStatusCode, theResponseMimeType, theResponseInputStream);
109                        populateException(ex, theResponseInputStream);
110                        throw ex;
111                }
112
113                IParser parser = encoding.newParser(getContext());
114
115                parser.setPreferTypes(thePreferTypes);
116
117                return parser;
118        }
119
120        public List<Class<?>> getAllowableParamAnnotations() {
121                return null;
122        }
123
124        public FhirContext getContext() {
125                return myContext;
126        }
127
128        public Set<String> getIncludes() {
129                Set<String> retVal = new TreeSet<String>();
130                for (IParameter next : myParameters) {
131                        if (next instanceof IncludeParameter) {
132                                retVal.addAll(((IncludeParameter) next).getAllow());
133                        }
134                }
135                return retVal;
136        }
137
138        public Method getMethod() {
139                return myMethod;
140        }
141
142        public List<IParameter> getParameters() {
143                return myParameters;
144        }
145
146        public Object getProvider() {
147                return myProvider;
148        }
149
150        /**
151         * Returns the name of the resource this method handles, or <code>null</code> if this method is not resource specific
152         */
153        public abstract String getResourceName();
154
155        public abstract RestOperationTypeEnum getRestOperationType();
156
157        public abstract BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException;
158
159        /**
160         * 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.
161         */
162        public boolean isSupportsConditional() {
163                return mySupportsConditional;
164        }
165
166        /**
167         * Does this method support conditional operations over multiple objects (basically for conditional delete)
168         */
169        public boolean isSupportsConditionalMultiple() {
170                return mySupportsConditionalMultiple;
171        }
172
173        protected BaseServerResponseException processNon2xxResponseAndReturnExceptionToThrow(
174                        int theStatusCode, String theResponseMimeType, InputStream theResponseInputStream) {
175                BaseServerResponseException ex;
176                switch (theStatusCode) {
177                        case Constants.STATUS_HTTP_400_BAD_REQUEST:
178                                ex = new InvalidRequestException("Server responded with HTTP 400");
179                                break;
180                        case Constants.STATUS_HTTP_404_NOT_FOUND:
181                                ex = new ResourceNotFoundException("Server responded with HTTP 404");
182                                break;
183                        case Constants.STATUS_HTTP_405_METHOD_NOT_ALLOWED:
184                                ex = new MethodNotAllowedException("Server responded with HTTP 405");
185                                break;
186                        case Constants.STATUS_HTTP_409_CONFLICT:
187                                ex = new ResourceVersionConflictException("Server responded with HTTP 409");
188                                break;
189                        case Constants.STATUS_HTTP_412_PRECONDITION_FAILED:
190                                ex = new PreconditionFailedException("Server responded with HTTP 412");
191                                break;
192                        case Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY:
193                                IParser parser = createAppropriateParserForParsingResponse(
194                                                theResponseMimeType, theResponseInputStream, theStatusCode, null);
195                                // TODO: handle if something other than OO comes back
196                                BaseOperationOutcome operationOutcome =
197                                                (BaseOperationOutcome) parser.parseResource(theResponseInputStream);
198                                ex = new UnprocessableEntityException(myContext, operationOutcome);
199                                break;
200                        default:
201                                ex = new UnclassifiedServerFailureException(
202                                                theStatusCode, "Server responded with HTTP " + theStatusCode);
203                                break;
204                }
205
206                populateException(ex, theResponseInputStream);
207                return ex;
208        }
209
210        /** For unit tests only */
211        public void setParameters(List<IParameter> theParameters) {
212                myParameters = theParameters;
213        }
214
215        @SuppressWarnings("unchecked")
216        public static BaseMethodBinding<?> bindMethod(Method theMethod, FhirContext theContext, Object theProvider) {
217                Read read = theMethod.getAnnotation(Read.class);
218                Search search = theMethod.getAnnotation(Search.class);
219                Metadata conformance = theMethod.getAnnotation(Metadata.class);
220                Create create = theMethod.getAnnotation(Create.class);
221                Update update = theMethod.getAnnotation(Update.class);
222                Delete delete = theMethod.getAnnotation(Delete.class);
223                History history = theMethod.getAnnotation(History.class);
224                Validate validate = theMethod.getAnnotation(Validate.class);
225                AddTags addTags = theMethod.getAnnotation(AddTags.class);
226                DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class);
227                Transaction transaction = theMethod.getAnnotation(Transaction.class);
228                Operation operation = theMethod.getAnnotation(Operation.class);
229                GetPage getPage = theMethod.getAnnotation(GetPage.class);
230                Patch patch = theMethod.getAnnotation(Patch.class);
231
232                // ** if you add another annotation above, also add it to the next line:
233                if (!verifyMethodHasZeroOrOneOperationAnnotation(
234                                theMethod,
235                                read,
236                                search,
237                                conformance,
238                                create,
239                                update,
240                                delete,
241                                history,
242                                validate,
243                                addTags,
244                                deleteTags,
245                                transaction,
246                                operation,
247                                getPage,
248                                patch)) {
249                        return null;
250                }
251
252                if (getPage != null) {
253                        return new PageMethodBinding(theContext, theMethod);
254                }
255
256                Class<? extends IBaseResource> returnType;
257
258                Class<? extends IBaseResource> returnTypeFromRp = null;
259
260                Class<?> returnTypeFromMethod = theMethod.getReturnType();
261                if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) {
262                        // returns a method outcome
263                } else if (void.class.equals(returnTypeFromMethod)) {
264                        // returns a bundle
265                } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
266                        returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
267                        if (returnTypeFromMethod == null) {
268                                ourLog.trace("Method {} returns a non-typed list, can't verify return type", theMethod);
269                        } else if (!verifyIsValidResourceReturnType(returnTypeFromMethod)
270                                        && !isResourceInterface(returnTypeFromMethod)) {
271                                throw new ConfigurationException(
272                                                Msg.code(1427) + "Method '" + theMethod.getName() + "' from client type "
273                                                                + theMethod.getDeclaringClass().getCanonicalName()
274                                                                + " returns a collection with generic type " + toLogString(returnTypeFromMethod)
275                                                                + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IBaseResource> )");
276                        }
277                } else {
278                        if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
279                                throw new ConfigurationException(Msg.code(1428) + "Method '" + theMethod.getName()
280                                                + "' from client type " + theMethod.getDeclaringClass().getCanonicalName()
281                                                + " returns " + toLogString(returnTypeFromMethod)
282                                                + " - Must return a resource type (eg Patient, Bundle"
283                                                + ", etc., see the documentation for more details)");
284                        }
285                }
286
287                Class<? extends IBaseResource> returnTypeFromAnnotation = IBaseResource.class;
288                if (read != null) {
289                        returnTypeFromAnnotation = read.type();
290                } else if (search != null) {
291                        returnTypeFromAnnotation = search.type();
292                } else if (history != null) {
293                        returnTypeFromAnnotation = history.type();
294                } else if (delete != null) {
295                        returnTypeFromAnnotation = delete.type();
296                } else if (patch != null) {
297                        returnTypeFromAnnotation = patch.type();
298                } else if (create != null) {
299                        returnTypeFromAnnotation = create.type();
300                } else if (update != null) {
301                        returnTypeFromAnnotation = update.type();
302                } else if (validate != null) {
303                        returnTypeFromAnnotation = validate.type();
304                } else if (addTags != null) {
305                        returnTypeFromAnnotation = addTags.type();
306                } else if (deleteTags != null) {
307                        returnTypeFromAnnotation = deleteTags.type();
308                }
309
310                if (!isResourceInterface(returnTypeFromAnnotation)) {
311                        if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
312                                throw new ConfigurationException(Msg.code(1429) + "Method '" + theMethod.getName()
313                                                + "' from client type " + theMethod.getDeclaringClass().getCanonicalName() + " returns "
314                                                + toLogString(returnTypeFromAnnotation)
315                                                + " according to annotation - Must return a resource type");
316                        }
317                        returnType = returnTypeFromAnnotation;
318                } else {
319                        // if (IRestfulClient.class.isAssignableFrom(theMethod.getDeclaringClass())) {
320                        // Clients don't define their methods in resource specific types, so they can
321                        // infer their resource type from the method return type.
322                        returnType = (Class<? extends IBaseResource>) returnTypeFromMethod;
323                        // } else {
324                        // This is a plain provider method returning a resource, so it should be
325                        // an operation or global search presumably
326                        // returnType = null;
327                }
328
329                if (read != null) {
330                        return new ReadMethodBinding(returnType, theMethod, theContext, theProvider);
331                } else if (search != null) {
332                        return new SearchMethodBinding(returnType, theMethod, theContext, theProvider);
333                } else if (conformance != null) {
334                        return new ConformanceMethodBinding(theMethod, theContext, theProvider);
335                } else if (create != null) {
336                        return new CreateMethodBinding(theMethod, theContext, theProvider);
337                } else if (update != null) {
338                        return new UpdateMethodBinding(theMethod, theContext, theProvider);
339                } else if (delete != null) {
340                        return new DeleteMethodBinding(theMethod, theContext, theProvider);
341                } else if (patch != null) {
342                        return new PatchMethodBinding(theMethod, theContext, theProvider);
343                } else if (history != null) {
344                        return new HistoryMethodBinding(theMethod, theContext, theProvider);
345                } else if (validate != null) {
346                        return new ValidateMethodBindingDstu2Plus(
347                                        returnType, returnTypeFromRp, theMethod, theContext, theProvider, validate);
348                } else if (transaction != null) {
349                        return new TransactionMethodBinding(theMethod, theContext, theProvider);
350                } else if (operation != null) {
351                        return new OperationMethodBinding(
352                                        returnType, returnTypeFromRp, theMethod, theContext, theProvider, operation);
353                } else {
354                        throw new ConfigurationException(
355                                        Msg.code(1430) + "Did not detect any FHIR annotations on method '" + theMethod.getName()
356                                                        + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
357                }
358
359                // // each operation name must have a request type annotation and be
360                // unique
361                // if (null != read) {
362                // return rm;
363                // }
364                //
365                // SearchMethodBinding sm = new SearchMethodBinding();
366                // if (null != search) {
367                // sm.setRequestType(SearchMethodBinding.RequestType.GET);
368                // } else if (null != theMethod.getAnnotation(PUT.class)) {
369                // sm.setRequestType(SearchMethodBinding.RequestType.PUT);
370                // } else if (null != theMethod.getAnnotation(POST.class)) {
371                // sm.setRequestType(SearchMethodBinding.RequestType.POST);
372                // } else if (null != theMethod.getAnnotation(DELETE.class)) {
373                // sm.setRequestType(SearchMethodBinding.RequestType.DELETE);
374                // } else {
375                // return null;
376                // }
377                //
378                // return sm;
379        }
380
381        public static boolean isResourceInterface(Class<?> theReturnTypeFromMethod) {
382                return theReturnTypeFromMethod.equals(IBaseResource.class)
383                                || theReturnTypeFromMethod.equals(IResource.class)
384                                || theReturnTypeFromMethod.equals(IAnyResource.class);
385        }
386
387        private static void populateException(BaseServerResponseException theEx, InputStream theResponseInputStream) {
388                try {
389                        String responseText = IOUtils.toString(theResponseInputStream);
390                        theEx.setResponseBody(responseText);
391                } catch (IOException e) {
392                        ourLog.debug("Failed to read response", e);
393                }
394        }
395
396        private static String toLogString(Class<?> theType) {
397                if (theType == null) {
398                        return null;
399                }
400                return theType.getCanonicalName();
401        }
402
403        private static boolean verifyIsValidResourceReturnType(Class<?> theReturnType) {
404                if (theReturnType == null) {
405                        return false;
406                }
407                if (!IBaseResource.class.isAssignableFrom(theReturnType)) {
408                        return false;
409                }
410                return true;
411                // boolean retVal = Modifier.isAbstract(theReturnType.getModifiers()) == false;
412                // return retVal;
413        }
414
415        public static boolean verifyMethodHasZeroOrOneOperationAnnotation(Method theNextMethod, Object... theAnnotations) {
416                Object obj1 = null;
417                for (Object object : theAnnotations) {
418                        if (object != null) {
419                                if (obj1 == null) {
420                                        obj1 = object;
421                                } else {
422                                        throw new ConfigurationException(Msg.code(1431) + "Method " + theNextMethod.getName() + " on type '"
423                                                        + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @"
424                                                        + obj1.getClass().getSimpleName() + " and @"
425                                                        + object.getClass().getSimpleName() + ". Can not have both.");
426                                }
427                        }
428                }
429                if (obj1 == null) {
430                        return false;
431                        // throw new ConfigurationException(Msg.code(1432) + "Method '" +
432                        // theNextMethod.getName() + "' on type '" +
433                        // theNextMethod.getDeclaringClass().getSimpleName() +
434                        // " has no FHIR method annotations.");
435                }
436                return true;
437        }
438}