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