001/*-
002 * #%L
003 * HAPI FHIR - Client Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.client.method;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.model.api.IResource;
026import ca.uhn.fhir.model.valueset.BundleTypeEnum;
027import ca.uhn.fhir.parser.IParser;
028import ca.uhn.fhir.rest.api.Constants;
029import ca.uhn.fhir.rest.api.MethodOutcome;
030import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException;
031import ca.uhn.fhir.util.BundleUtil;
032import ca.uhn.fhir.util.ReflectionUtil;
033import org.hl7.fhir.instance.model.api.IBaseBundle;
034import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036
037import java.io.IOException;
038import java.io.InputStream;
039import java.lang.reflect.Method;
040import java.lang.reflect.Modifier;
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.HashSet;
045import java.util.Iterator;
046import java.util.List;
047import java.util.Map;
048import java.util.Set;
049
050public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> {
051        protected static final Set<String> ALLOWED_PARAMS;
052        private static final org.slf4j.Logger ourLog =
053                        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(
078                        Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
079                super(theMethod, theContext, theProvider);
080
081                Class<?> methodReturnType = theMethod.getReturnType();
082                if (Collection.class.isAssignableFrom(methodReturnType)) {
083
084                        myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
085                        Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
086                        if (collectionType != null) {
087                                if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) {
088                                        throw new ConfigurationException(Msg.code(1458) + "Method "
089                                                        + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName()
090                                                        + " returns an invalid collection generic type: " + collectionType);
091                                }
092                        }
093                        myResourceListCollectionType = collectionType;
094
095                } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) {
096                        if (Modifier.isAbstract(methodReturnType.getModifiers()) == false
097                                        && theContext
098                                                        .getResourceDefinition((Class<? extends IBaseResource>) methodReturnType)
099                                                        .isBundle()) {
100                                myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE;
101                        } else {
102                                myMethodReturnType = MethodReturnTypeEnum.RESOURCE;
103                        }
104                } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) {
105                        myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME;
106                } else {
107                        throw new ConfigurationException(Msg.code(1459) + "Invalid return type '"
108                                        + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: "
109                                        + theMethod.getDeclaringClass().getCanonicalName());
110                }
111
112                if (theReturnResourceType != null) {
113                        if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) {
114                                if (Modifier.isAbstract(theReturnResourceType.getModifiers())
115                                                || Modifier.isInterface(theReturnResourceType.getModifiers())) {
116                                        // If we're returning an abstract type, that's ok
117                                } else {
118                                        myResourceType = (Class<? extends IResource>) theReturnResourceType;
119                                        myResourceName = theContext.getResourceType(myResourceType);
120                                }
121                        }
122                }
123
124                myPreferTypesList = createPreferTypesList();
125        }
126
127        public MethodReturnTypeEnum getMethodReturnType() {
128                return myMethodReturnType;
129        }
130
131        @Override
132        public String getResourceName() {
133                return myResourceName;
134        }
135
136        /**
137         * If the response is a bundle, this type will be placed in the root of the bundle (can be null)
138         */
139        protected abstract BundleTypeEnum getResponseBundleType();
140
141        public abstract ReturnTypeEnum getReturnType();
142
143        @Override
144        public Object invokeClient(
145                        String theResponseMimeType,
146                        InputStream theResponseInputStream,
147                        int theResponseStatusCode,
148                        Map<String, List<String>> theHeaders)
149                        throws IOException {
150
151                if (Constants.STATUS_HTTP_204_NO_CONTENT == theResponseStatusCode) {
152                        return toReturnType(null);
153                }
154
155                IParser parser = createAppropriateParserForParsingResponse(
156                                theResponseMimeType, theResponseInputStream, theResponseStatusCode, myPreferTypesList);
157
158                switch (getReturnType()) {
159                        case BUNDLE: {
160                                IBaseBundle bundle;
161                                List<? extends IBaseResource> listOfResources;
162                                Class<? extends IBaseResource> type =
163                                                getContext().getResourceDefinition("Bundle").getImplementingClass();
164                                bundle = (IBaseBundle) parser.parseResource(type, theResponseInputStream);
165                                listOfResources = BundleUtil.toListOfResources(getContext(), bundle);
166
167                                switch (getMethodReturnType()) {
168                                        case BUNDLE_RESOURCE:
169                                                return bundle;
170                                        case LIST_OF_RESOURCES:
171                                                if (myResourceListCollectionType != null) {
172                                                        for (Iterator<? extends IBaseResource> iter = listOfResources.iterator();
173                                                                        iter.hasNext(); ) {
174                                                                IBaseResource next = iter.next();
175                                                                if (!myResourceListCollectionType.isAssignableFrom(next.getClass())) {
176                                                                        ourLog.debug(
177                                                                                        "Not returning resource of type {} because it is not a subclass or instance of {}",
178                                                                                        next.getClass(),
179                                                                                        myResourceListCollectionType);
180                                                                        iter.remove();
181                                                                }
182                                                        }
183                                                }
184                                                return listOfResources;
185                                        case RESOURCE:
186                                                List<IBaseResource> list = BundleUtil.toListOfResources(getContext(), bundle);
187                                                if (list.size() == 0) {
188                                                        return null;
189                                                } else if (list.size() == 1) {
190                                                        return list.get(0);
191                                                } else {
192                                                        throw new InvalidResponseException(
193                                                                        Msg.code(1460)
194                                                                                        + "FHIR server call returned a bundle with multiple resources, but this method is only able to returns one.",
195                                                                        theResponseStatusCode);
196                                                }
197                                        default:
198                                                break;
199                                }
200                                break;
201                        }
202                        case RESOURCE: {
203                                IBaseResource resource;
204                                if (myResourceType != null) {
205                                        resource = parser.parseResource(myResourceType, theResponseInputStream);
206                                } else {
207                                        resource = parser.parseResource(theResponseInputStream);
208                                }
209
210                                MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource);
211
212                                return toReturnType(resource);
213                        }
214                }
215
216                throw new IllegalStateException(Msg.code(1461) + "Should not get here!");
217        }
218
219        private Object toReturnType(IBaseResource resource) {
220                Object retVal = null;
221
222                switch (getMethodReturnType()) {
223                        case LIST_OF_RESOURCES:
224                                retVal = Collections.emptyList();
225                                if (resource != null) {
226                                        retVal = Collections.singletonList(resource);
227                                }
228                                break;
229                        case RESOURCE:
230                                retVal = resource;
231                                break;
232                        case BUNDLE_RESOURCE:
233                                retVal = resource;
234                                break;
235                        case METHOD_OUTCOME:
236                                MethodOutcome outcome = new MethodOutcome();
237                                outcome.setOperationOutcome((IBaseOperationOutcome) resource);
238                                retVal = outcome;
239                                break;
240                }
241                return retVal;
242        }
243
244        @SuppressWarnings("unchecked")
245        private List<Class<? extends IBaseResource>> createPreferTypesList() {
246                List<Class<? extends IBaseResource>> preferTypes = null;
247                if (myResourceType != null && !BaseMethodBinding.isResourceInterface(myResourceType)) {
248                        preferTypes = new ArrayList<Class<? extends IBaseResource>>(1);
249                        preferTypes.add(myResourceType);
250                } else if (myResourceListCollectionType != null
251                                && IBaseResource.class.isAssignableFrom(myResourceListCollectionType)
252                                && !BaseMethodBinding.isResourceInterface(myResourceListCollectionType)) {
253                        preferTypes = new ArrayList<Class<? extends IBaseResource>>(1);
254                        preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType);
255                }
256                return preferTypes;
257        }
258
259        /**
260         * 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
261         */
262        protected boolean isAddContentLocationHeader() {
263                return true;
264        }
265
266        protected void setResourceName(String theResourceName) {
267                myResourceName = theResourceName;
268        }
269
270        public enum MethodReturnTypeEnum {
271                BUNDLE_RESOURCE,
272                LIST_OF_RESOURCES,
273                METHOD_OUTCOME,
274                RESOURCE
275        }
276
277        public static class ResourceOrDstu1Bundle {
278
279                private final IBaseResource myResource;
280
281                public ResourceOrDstu1Bundle(IBaseResource theResource) {
282                        myResource = theResource;
283                }
284
285                public IBaseResource getResource() {
286                        return myResource;
287                }
288        }
289
290        public enum ReturnTypeEnum {
291                BUNDLE,
292                RESOURCE
293        }
294}