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