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