001package ca.uhn.fhir.rest.client.method;
002
003/*-
004 * #%L
005 * HAPI FHIR - Client Framework
006 * %%
007 * Copyright (C) 2014 - 2021 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.lang.reflect.Method;
026import java.lang.reflect.Modifier;
027import java.util.*;
028
029import org.hl7.fhir.instance.model.api.*;
030
031import ca.uhn.fhir.context.ConfigurationException;
032import ca.uhn.fhir.context.FhirContext;
033import ca.uhn.fhir.model.api.IResource;
034import ca.uhn.fhir.model.valueset.BundleTypeEnum;
035import ca.uhn.fhir.parser.IParser;
036import ca.uhn.fhir.rest.api.Constants;
037import ca.uhn.fhir.rest.api.MethodOutcome;
038import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException;
039import ca.uhn.fhir.util.BundleUtil;
040import ca.uhn.fhir.util.ReflectionUtil;
041
042public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> {
043        protected static final Set<String> ALLOWED_PARAMS;
044        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class);
045
046        static {
047                HashSet<String> set = new HashSet<String>();
048                set.add(Constants.PARAM_FORMAT);
049                set.add(Constants.PARAM_NARRATIVE);
050                set.add(Constants.PARAM_PRETTY);
051                set.add(Constants.PARAM_SORT);
052                set.add(Constants.PARAM_SORT_ASC);
053                set.add(Constants.PARAM_SORT_DESC);
054                set.add(Constants.PARAM_COUNT);
055                set.add(Constants.PARAM_OFFSET);
056                set.add(Constants.PARAM_SUMMARY);
057                set.add(Constants.PARAM_ELEMENTS);
058                ALLOWED_PARAMS = Collections.unmodifiableSet(set);
059        }
060
061        private MethodReturnTypeEnum myMethodReturnType;
062        private Class<?> myResourceListCollectionType;
063        private String myResourceName;
064        private Class<? extends IBaseResource> myResourceType;
065        private List<Class<? extends IBaseResource>> myPreferTypesList;
066
067        @SuppressWarnings("unchecked")
068        public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
069                super(theMethod, theContext, theProvider);
070
071                Class<?> methodReturnType = theMethod.getReturnType();
072                if (Collection.class.isAssignableFrom(methodReturnType)) {
073
074                        myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
075                        Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
076                        if (collectionType != null) {
077                                if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) {
078                                        throw new ConfigurationException(
079                                                        "Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " + collectionType);
080                                }
081                        }
082                        myResourceListCollectionType = collectionType;
083
084                } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) {
085                        if (Modifier.isAbstract(methodReturnType.getModifiers()) == false && theContext.getResourceDefinition((Class<? extends IBaseResource>) methodReturnType).isBundle()) {
086                                myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE;
087                        } else {
088                                myMethodReturnType = MethodReturnTypeEnum.RESOURCE;
089                        }
090                } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) {
091                        myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME;
092                } else {
093                        throw new ConfigurationException(
094                                        "Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
095                }
096
097                if (theReturnResourceType != null) {
098                        if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) {
099                                if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) {
100                                        // If we're returning an abstract type, that's ok
101                                } else {
102                                        myResourceType = (Class<? extends IResource>) theReturnResourceType;
103                                        myResourceName = theContext.getResourceType(myResourceType);
104                                }
105                        }
106                }
107
108                myPreferTypesList = createPreferTypesList();
109        }
110
111        public MethodReturnTypeEnum getMethodReturnType() {
112                return myMethodReturnType;
113        }
114
115        @Override
116        public String getResourceName() {
117                return myResourceName;
118        }
119
120        /**
121         * If the response is a bundle, this type will be placed in the root of the bundle (can be null)
122         */
123        protected abstract BundleTypeEnum getResponseBundleType();
124
125        public abstract ReturnTypeEnum getReturnType();
126
127        @Override
128        public Object invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws IOException {
129                
130                if (Constants.STATUS_HTTP_204_NO_CONTENT == theResponseStatusCode) {
131                        return toReturnType(null);
132                }
133                
134                IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseInputStream, theResponseStatusCode, myPreferTypesList);
135
136                switch (getReturnType()) {
137                case BUNDLE: {
138
139                        IBaseBundle bundle;
140                        List<? extends IBaseResource> listOfResources;
141                        Class<? extends IBaseResource> type = getContext().getResourceDefinition("Bundle").getImplementingClass();
142                        bundle = (IBaseBundle) parser.parseResource(type, theResponseInputStream);
143                        listOfResources = BundleUtil.toListOfResources(getContext(), bundle);
144
145                        switch (getMethodReturnType()) {
146                        case BUNDLE_RESOURCE:
147                                return bundle;
148                        case LIST_OF_RESOURCES:
149                                if (myResourceListCollectionType != null) {
150                                        for (Iterator<? extends IBaseResource> iter = listOfResources.iterator(); iter.hasNext();) {
151                                                IBaseResource next = iter.next();
152                                                if (!myResourceListCollectionType.isAssignableFrom(next.getClass())) {
153                                                        ourLog.debug("Not returning resource of type {} because it is not a subclass or instance of {}", next.getClass(), myResourceListCollectionType);
154                                                        iter.remove();
155                                                }
156                                        }
157                                }
158                                return listOfResources;
159                        case RESOURCE:
160                                List<IBaseResource> list = BundleUtil.toListOfResources(getContext(), bundle);
161                                if (list.size() == 0) {
162                                        return null;
163                                } else if (list.size() == 1) {
164                                        return list.get(0);
165                                } else {
166                                        throw new InvalidResponseException(theResponseStatusCode, "FHIR server call returned a bundle with multiple resources, but this method is only able to returns one.");
167                                }
168                        default:
169                                break;
170                        }
171                        break;
172                }
173                case RESOURCE: {
174                        IBaseResource resource;
175                        if (myResourceType != null) {
176                                resource = parser.parseResource(myResourceType, theResponseInputStream);
177                        } else {
178                                resource = parser.parseResource(theResponseInputStream);
179                        }
180
181                        MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource);
182
183                        return toReturnType(resource);
184                }
185                }
186
187                throw new IllegalStateException("Should not get here!");
188        }
189
190        private Object toReturnType(IBaseResource resource) {
191                Object retVal = null;
192                
193                switch (getMethodReturnType()) {
194                case LIST_OF_RESOURCES:
195                        retVal = Collections.emptyList();
196                        if (resource != null) {
197                                retVal = Collections.singletonList(resource);
198                        }
199                        break;
200                case RESOURCE:
201                        retVal = resource;
202                        break;
203                case BUNDLE_RESOURCE:
204                        retVal = resource;
205                        break;
206                case METHOD_OUTCOME:
207                        MethodOutcome outcome = new MethodOutcome();
208                        outcome.setOperationOutcome((IBaseOperationOutcome) resource);
209                        retVal = outcome;
210                        break;
211                }
212                return retVal;
213        }
214
215        @SuppressWarnings("unchecked")
216        private List<Class<? extends IBaseResource>> createPreferTypesList() {
217                List<Class<? extends IBaseResource>> preferTypes = null;
218                if (myResourceType != null && !BaseMethodBinding.isResourceInterface(myResourceType)) {
219                        preferTypes = new ArrayList<Class<? extends IBaseResource>>(1);
220                        preferTypes.add(myResourceType);
221                } else if (myResourceListCollectionType != null && IBaseResource.class.isAssignableFrom(myResourceListCollectionType) && !BaseMethodBinding.isResourceInterface(myResourceListCollectionType)) {
222                        preferTypes = new ArrayList<Class<? extends IBaseResource>>(1);
223                        preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType);
224                }
225                return preferTypes;
226        }
227
228        /**
229         * Should the response include a Content-Location header. Search method bunding (and any others?) may override this to disable the content-location, since it doesn't make sense
230         */
231        protected boolean isAddContentLocationHeader() {
232                return true;
233        }
234
235        protected void setResourceName(String theResourceName) {
236                myResourceName = theResourceName;
237        }
238
239        public enum MethodReturnTypeEnum {
240                BUNDLE_RESOURCE,
241                LIST_OF_RESOURCES,
242                METHOD_OUTCOME,
243                RESOURCE
244        }
245
246        public static class ResourceOrDstu1Bundle {
247
248                private final IBaseResource myResource;
249
250                public ResourceOrDstu1Bundle(IBaseResource theResource) {
251                        myResource = theResource;
252                }
253
254                public IBaseResource getResource() {
255                        return myResource;
256                }
257
258        }
259
260        public enum ReturnTypeEnum {
261                BUNDLE,
262                RESOURCE
263        }
264
265}