001/*
002 * #%L
003 * HAPI FHIR - Server 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.server.method;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.api.HookParams;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.model.api.IResource;
029import ca.uhn.fhir.model.api.Include;
030import ca.uhn.fhir.model.valueset.BundleTypeEnum;
031import ca.uhn.fhir.rest.api.BundleLinks;
032import ca.uhn.fhir.rest.api.Constants;
033import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
034import ca.uhn.fhir.rest.api.MethodOutcome;
035import ca.uhn.fhir.rest.api.SummaryEnum;
036import ca.uhn.fhir.rest.api.server.IBundleProvider;
037import ca.uhn.fhir.rest.api.server.IRestfulServer;
038import ca.uhn.fhir.rest.api.server.RequestDetails;
039import ca.uhn.fhir.rest.api.server.ResponseDetails;
040import ca.uhn.fhir.rest.server.RestfulServerUtils;
041import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
042import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
043import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
044import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
045import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
046import ca.uhn.fhir.util.ReflectionUtil;
047import jakarta.servlet.http.HttpServletRequest;
048import jakarta.servlet.http.HttpServletResponse;
049import org.apache.commons.lang3.Validate;
050import org.hl7.fhir.instance.model.api.IBaseBundle;
051import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
052import org.hl7.fhir.instance.model.api.IBaseResource;
053import org.hl7.fhir.instance.model.api.IPrimitiveType;
054
055import java.io.IOException;
056import java.lang.reflect.Method;
057import java.lang.reflect.Modifier;
058import java.util.Collection;
059import java.util.Date;
060import java.util.Set;
061
062public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding {
063        protected final ResponseBundleBuilder myResponseBundleBuilder;
064
065        private MethodReturnTypeEnum myMethodReturnType;
066        private String myResourceName;
067
068        @SuppressWarnings("unchecked")
069        public BaseResourceReturningMethodBinding(
070                        Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
071                super(theMethod, theContext, theProvider);
072
073                Class<?> methodReturnType = theMethod.getReturnType();
074
075                Set<Class<?>> expectedReturnTypes = provideExpectedReturnTypes();
076                if (expectedReturnTypes != null) {
077
078                        Validate.isTrue(
079                                        expectedReturnTypes.contains(methodReturnType),
080                                        "Unexpected method return type on %s - Allowed: %s",
081                                        theMethod,
082                                        expectedReturnTypes);
083
084                } else if (Collection.class.isAssignableFrom(methodReturnType)) {
085
086                        myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
087                        Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
088                        if (collectionType != null) {
089                                if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) {
090                                        throw new ConfigurationException(Msg.code(433) + "Method "
091                                                        + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName()
092                                                        + " returns an invalid collection generic type: " + collectionType);
093                                }
094                        }
095
096                } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) {
097
098                        if (IBaseBundle.class.isAssignableFrom(methodReturnType)) {
099                                myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE;
100                        } else {
101                                myMethodReturnType = MethodReturnTypeEnum.RESOURCE;
102                        }
103                } else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) {
104                        myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER;
105                } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) {
106                        myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME;
107                } else if (void.class.equals(methodReturnType)) {
108                        myMethodReturnType = MethodReturnTypeEnum.VOID;
109                } else {
110                        throw new ConfigurationException(Msg.code(434) + "Invalid return type '"
111                                        + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: "
112                                        + theMethod.getDeclaringClass().getCanonicalName());
113                }
114
115                if (theReturnResourceType != null) {
116                        if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) {
117
118                                // If we're returning an abstract type, that's ok, but if we know the resource
119                                // type let's grab it
120                                if (!Modifier.isAbstract(theReturnResourceType.getModifiers())
121                                                && !Modifier.isInterface(theReturnResourceType.getModifiers())) {
122                                        Class<? extends IBaseResource> resourceType = (Class<? extends IResource>) theReturnResourceType;
123                                        RuntimeResourceDefinition resourceDefinition = theContext.getResourceDefinition(resourceType);
124                                        myResourceName = resourceDefinition.getName();
125                                }
126                        }
127                }
128
129                myResponseBundleBuilder = new ResponseBundleBuilder(isOffsetModeHistory());
130        }
131
132        /**
133         * Subclasses may override
134         */
135        protected Set<Class<?>> provideExpectedReturnTypes() {
136                return null;
137        }
138
139        protected boolean isOffsetModeHistory() {
140                return false;
141        }
142
143        public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {
144                Object[] params = createMethodParams(theRequest);
145
146                Object resultObj = invokeServer(theServer, theRequest, params);
147                if (resultObj == null) {
148                        return null;
149                }
150
151                Integer count = RestfulServerUtils.extractCountParameter(theRequest);
152
153                final IBaseResource responseObject;
154
155                switch (getReturnType()) {
156                        case BUNDLE: {
157
158                                /*
159                                 * Figure out the self-link for this request
160                                 */
161
162                                BundleTypeEnum responseBundleType = getResponseBundleType();
163                                BundleLinks bundleLinks = new BundleLinks(
164                                                theRequest.getServerBaseForRequest(),
165                                                null,
166                                                RestfulServerUtils.prettyPrintResponse(theServer, theRequest),
167                                                responseBundleType);
168                                String linkSelf = RestfulServerUtils.createLinkSelf(theRequest.getFhirServerBase(), theRequest);
169                                bundleLinks.setSelf(linkSelf);
170
171                                if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE_RESOURCE) {
172                                        IBaseResource resource;
173                                        IPrimitiveType<Date> lastUpdated;
174                                        if (resultObj instanceof IBundleProvider) {
175                                                IBundleProvider result = (IBundleProvider) resultObj;
176                                                resource = result.getResources(0, 1).get(0);
177                                                lastUpdated = result.getPublished();
178                                        } else {
179                                                resource = (IBaseResource) resultObj;
180                                                lastUpdated = theServer.getFhirContext().getVersion().getLastUpdated(resource);
181                                        }
182
183                                        /*
184                                         * We assume that the bundle we got back from the handling method may not have everything populated (e.g. self links, bundle type, etc) so we do that here.
185                                         */
186                                        IVersionSpecificBundleFactory bundleFactory =
187                                                        theServer.getFhirContext().newBundleFactory();
188                                        bundleFactory.initializeWithBundleResource(resource);
189                                        bundleFactory.addRootPropertiesToBundle(null, bundleLinks, count, lastUpdated);
190
191                                        responseObject = resource;
192                                } else {
193                                        ResponseBundleRequest responseBundleRequest = buildResponseBundleRequest(
194                                                        theServer,
195                                                        theRequest,
196                                                        params,
197                                                        (IBundleProvider) resultObj,
198                                                        count,
199                                                        responseBundleType,
200                                                        linkSelf);
201                                        responseObject = myResponseBundleBuilder.buildResponseBundle(responseBundleRequest);
202                                }
203                                break;
204                        }
205                        case RESOURCE: {
206                                IBundleProvider result = (IBundleProvider) resultObj;
207                                Integer size = result.size();
208                                if (size == null || size == 0) {
209                                        throw new ResourceNotFoundException(
210                                                        Msg.code(436) + "Resource " + theRequest.getId() + " is not known");
211                                } else if (size > 1) {
212                                        throw new InternalErrorException(Msg.code(437) + "Method returned multiple resources");
213                                }
214
215                                responseObject = result.getResources(0, 1).get(0);
216                                break;
217                        }
218                        default:
219                                throw new IllegalStateException(Msg.code(438)); // should not happen
220                }
221                return responseObject;
222        }
223
224        private ResponseBundleRequest buildResponseBundleRequest(
225                        IRestfulServer<?> theServer,
226                        RequestDetails theRequest,
227                        Object[] theParams,
228                        IBundleProvider theBundleProvider,
229                        Integer theCount,
230                        BundleTypeEnum theBundleTypeEnum,
231                        String theLinkSelf) {
232                Set<Include> includes = getRequestIncludesFromParams(theParams);
233
234                if (theCount == null) {
235                        theCount = theBundleProvider.preferredPageSize();
236                }
237
238                int offset = OffsetCalculator.calculateOffset(theRequest, theBundleProvider);
239
240                return new ResponseBundleRequest(
241                                theServer,
242                                theBundleProvider,
243                                theRequest,
244                                offset,
245                                theCount,
246                                theLinkSelf,
247                                includes,
248                                theBundleTypeEnum,
249                                null);
250        }
251
252        public MethodReturnTypeEnum getMethodReturnType() {
253                return myMethodReturnType;
254        }
255
256        @Override
257        public String getResourceName() {
258                return myResourceName;
259        }
260
261        protected void setResourceName(String theResourceName) {
262                myResourceName = theResourceName;
263        }
264
265        /**
266         * If the response is a bundle, this type will be placed in the root of the bundle (can be null)
267         */
268        protected abstract BundleTypeEnum getResponseBundleType();
269
270        public abstract ReturnTypeEnum getReturnType();
271
272        @Override
273        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest)
274                        throws BaseServerResponseException, IOException {
275                IBaseResource response = doInvokeServer(theServer, theRequest);
276                /*
277                When we write directly to an HttpServletResponse, the invocation returns null. However, we still want to invoke
278                the SERVER_OUTGOING_RESPONSE pointcut.
279                */
280                if (response == null) {
281                        ResponseDetails responseDetails = new ResponseDetails();
282                        responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK);
283                        callOutgoingResponseHook(theRequest, responseDetails);
284                        return null;
285                } else {
286                        Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest);
287                        ResponseDetails responseDetails = new ResponseDetails();
288                        responseDetails.setResponseResource(response);
289                        responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK);
290                        if (!callOutgoingResponseHook(theRequest, responseDetails)) {
291                                return null;
292                        }
293
294                        return RestfulServerUtils.streamResponseAsResource(
295                                        theServer,
296                                        responseDetails.getResponseResource(),
297                                        summaryMode,
298                                        responseDetails.getResponseCode(),
299                                        isAddContentLocationHeader(),
300                                        theRequest.isRespondGzip(),
301                                        theRequest,
302                                        null,
303                                        null);
304                }
305        }
306
307        public abstract Object invokeServer(
308                        IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams)
309                        throws InvalidRequestException, InternalErrorException;
310
311        /**
312         * 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
313         */
314        protected boolean isAddContentLocationHeader() {
315                return true;
316        }
317
318        public enum MethodReturnTypeEnum {
319                BUNDLE,
320                BUNDLE_PROVIDER,
321                BUNDLE_RESOURCE,
322                LIST_OF_RESOURCES,
323                METHOD_OUTCOME,
324                VOID,
325                RESOURCE
326        }
327
328        public enum ReturnTypeEnum {
329                BUNDLE,
330                RESOURCE
331        }
332
333        public static boolean callOutgoingResponseHook(RequestDetails theRequest, ResponseDetails theResponseDetails) {
334                HttpServletRequest servletRequest = null;
335                HttpServletResponse servletResponse = null;
336                if (theRequest instanceof ServletRequestDetails) {
337                        servletRequest = ((ServletRequestDetails) theRequest).getServletRequest();
338                        servletResponse = ((ServletRequestDetails) theRequest).getServletResponse();
339                }
340
341                HookParams responseParams = new HookParams();
342                responseParams.add(RequestDetails.class, theRequest);
343                responseParams.addIfMatchesType(ServletRequestDetails.class, theRequest);
344                responseParams.add(IBaseResource.class, theResponseDetails.getResponseResource());
345                responseParams.add(ResponseDetails.class, theResponseDetails);
346                responseParams.add(HttpServletRequest.class, servletRequest);
347                responseParams.add(HttpServletResponse.class, servletResponse);
348                if (theRequest.getInterceptorBroadcaster() != null) {
349                        return theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_RESPONSE, responseParams);
350                }
351                return true;
352        }
353
354        public static void callOutgoingFailureOperationOutcomeHook(
355                        RequestDetails theRequestDetails, IBaseOperationOutcome theOperationOutcome) {
356                HookParams responseParams = new HookParams();
357                responseParams.add(RequestDetails.class, theRequestDetails);
358                responseParams.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
359                responseParams.add(IBaseOperationOutcome.class, theOperationOutcome);
360
361                if (theRequestDetails.getInterceptorBroadcaster() != null) {
362                        theRequestDetails
363                                        .getInterceptorBroadcaster()
364                                        .callHooks(Pointcut.SERVER_OUTGOING_FAILURE_OPERATIONOUTCOME, responseParams);
365                }
366        }
367}