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