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