001package ca.uhn.fhir.rest.server.method;
002
003import ca.uhn.fhir.context.ConfigurationException;
004import ca.uhn.fhir.context.FhirContext;
005import ca.uhn.fhir.context.RuntimeResourceDefinition;
006import ca.uhn.fhir.i18n.Msg;
007import ca.uhn.fhir.interceptor.api.HookParams;
008import ca.uhn.fhir.interceptor.api.Pointcut;
009import ca.uhn.fhir.model.api.IResource;
010import ca.uhn.fhir.model.api.Include;
011import ca.uhn.fhir.model.valueset.BundleTypeEnum;
012import ca.uhn.fhir.rest.api.BundleLinks;
013import ca.uhn.fhir.rest.api.Constants;
014import ca.uhn.fhir.rest.api.EncodingEnum;
015import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
016import ca.uhn.fhir.rest.api.MethodOutcome;
017import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
018import ca.uhn.fhir.rest.api.SummaryEnum;
019import ca.uhn.fhir.rest.api.server.IBundleProvider;
020import ca.uhn.fhir.rest.api.server.IRestfulServer;
021import ca.uhn.fhir.rest.api.server.RequestDetails;
022import ca.uhn.fhir.rest.api.server.ResponseDetails;
023import ca.uhn.fhir.rest.server.IPagingProvider;
024import ca.uhn.fhir.rest.server.RestfulServerUtils;
025import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
026import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
027import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
028import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
029import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
030import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
031import ca.uhn.fhir.util.ReflectionUtil;
032import org.apache.commons.lang3.Validate;
033import org.hl7.fhir.instance.model.api.IBaseBundle;
034import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.hl7.fhir.instance.model.api.IPrimitiveType;
037
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040import java.io.IOException;
041import java.lang.reflect.Method;
042import java.lang.reflect.Modifier;
043import java.util.ArrayList;
044import java.util.Collection;
045import java.util.Collections;
046import java.util.Date;
047import java.util.List;
048import java.util.Objects;
049import java.util.Set;
050
051import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
052import static org.apache.commons.lang3.StringUtils.isBlank;
053import static org.apache.commons.lang3.StringUtils.isNotBlank;
054
055/*
056 * #%L
057 * HAPI FHIR - Server Framework
058 * %%
059 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
060 * %%
061 * Licensed under the Apache License, Version 2.0 (the "License");
062 * you may not use this file except in compliance with the License.
063 * You may obtain a copy of the License at
064 *
065 * http://www.apache.org/licenses/LICENSE-2.0
066 *
067 * Unless required by applicable law or agreed to in writing, software
068 * distributed under the License is distributed on an "AS IS" BASIS,
069 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
070 * See the License for the specific language governing permissions and
071 * limitations under the License.
072 * #L%
073 */
074
075public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding {
076        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class);
077
078        private MethodReturnTypeEnum myMethodReturnType;
079        private String myResourceName;
080
081        @SuppressWarnings("unchecked")
082        public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
083                super(theMethod, theContext, theProvider);
084
085                Class<?> methodReturnType = theMethod.getReturnType();
086
087                Set<Class<?>> expectedReturnTypes = provideExpectedReturnTypes();
088                if (expectedReturnTypes != null) {
089
090                        Validate.isTrue(expectedReturnTypes.contains(methodReturnType), "Unexpected method return type on %s - Allowed: %s", theMethod, expectedReturnTypes);
091
092                } else if (Collection.class.isAssignableFrom(methodReturnType)) {
093
094                        myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
095                        Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
096                        if (collectionType != null) {
097                                if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) {
098                                        throw new ConfigurationException(Msg.code(433) + "Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " + collectionType);
099                                }
100                        }
101
102                } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) {
103
104                        if ( IBaseBundle.class.isAssignableFrom(methodReturnType)) {
105                                myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE;
106                        } else {
107                                myMethodReturnType = MethodReturnTypeEnum.RESOURCE;
108                        }
109                } else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) {
110                        myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER;
111                } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) {
112                        myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME;
113                } else if (void.class.equals(methodReturnType)) {
114                        myMethodReturnType = MethodReturnTypeEnum.VOID;
115                } else {
116                        throw new ConfigurationException(Msg.code(434) + "Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName());
117                }
118
119                if (theReturnResourceType != null) {
120                        if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) {
121
122                                // If we're returning an abstract type, that's ok, but if we know the resource
123                                // type let's grab it
124                                if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) && !Modifier.isInterface(theReturnResourceType.getModifiers())) {
125                                        Class<? extends IBaseResource> resourceType = (Class<? extends IResource>) theReturnResourceType;
126                                        RuntimeResourceDefinition resourceDefinition = theContext.getResourceDefinition(resourceType);
127                                        myResourceName = resourceDefinition.getName();
128                                }
129                        }
130                }
131
132        }
133
134        /**
135         * Subclasses may override
136         */
137        protected Set<Class<?>> provideExpectedReturnTypes() {
138                return null;
139        }
140
141        IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
142                                                                                                                                IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
143                IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
144                final Integer offset;
145                Integer limit = theLimit;
146
147                if (theResult.getCurrentPageOffset() != null) {
148                        offset = theResult.getCurrentPageOffset();
149                        limit = theResult.getCurrentPageSize();
150                        Validate.notNull(limit, "IBundleProvider returned a non-null offset, but did not return a non-null page size");
151                } else {
152                        offset = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_OFFSET);
153                }
154
155                int numToReturn;
156                String searchId = null;
157                List<IBaseResource> resourceList;
158                Integer numTotalResults = theResult.size();
159
160                int pageSize;
161                if (offset != null || !theServer.canStoreSearchResults()) {
162                        if (limit != null) {
163                                pageSize = limit;
164                        } else {
165                                if (theServer.getDefaultPageSize() != null) {
166                                        pageSize = theServer.getDefaultPageSize();
167                                } else {
168                                        pageSize = numTotalResults != null ? numTotalResults : Integer.MAX_VALUE;
169                                }
170                        }
171                        numToReturn = pageSize;
172
173                        if ((offset != null && !isOffsetModeHistory()) || theResult.getCurrentPageOffset() != null) {
174                                // When offset query is done theResult already contains correct amount (+ their includes etc.) so return everything
175                                resourceList = theResult.getResources(0, Integer.MAX_VALUE);
176                        } else if (numToReturn > 0) {
177                                resourceList = theResult.getResources(0, numToReturn);
178                        } else {
179                                resourceList = Collections.emptyList();
180                        }
181                        RestfulServerUtils.validateResourceListNotNull(resourceList);
182
183                } else {
184                        IPagingProvider pagingProvider = theServer.getPagingProvider();
185                        if (limit == null || ((Integer) limit).equals(0)) {
186                                pageSize = pagingProvider.getDefaultPageSize();
187                        } else {
188                                pageSize = Math.min(pagingProvider.getMaximumPageSize(), limit);
189                        }
190                        numToReturn = pageSize;
191
192                        if (numTotalResults != null) {
193                                numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
194                        }
195
196                        if (numToReturn > 0 || theResult.getCurrentPageId() != null) {
197                                resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
198                        } else {
199                                resourceList = Collections.emptyList();
200                        }
201                        RestfulServerUtils.validateResourceListNotNull(resourceList);
202
203                        if (numTotalResults == null) {
204                                numTotalResults = theResult.size();
205                        }
206
207                        if (theSearchId != null) {
208                                searchId = theSearchId;
209                        } else {
210                                if (numTotalResults == null || numTotalResults > numToReturn) {
211                                        searchId = pagingProvider.storeResultList(theRequest, theResult);
212                                        if (isBlank(searchId)) {
213                                                ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults);
214                                                searchId = null;
215                                        }
216                                }
217                        }
218                }
219
220                /*
221                 * Remove any null entries in the list - This generally shouldn't happen but can if
222                 * data has been manually purged from the JPA database
223                 */
224                boolean hasNull = false;
225                for (IBaseResource next : resourceList) {
226                        if (next == null) {
227                                hasNull = true;
228                                break;
229                        }
230                }
231                if (hasNull) {
232                        resourceList.removeIf(Objects::isNull);
233                }
234
235                /*
236                 * Make sure all returned resources have an ID (if not, this is a bug
237                 * in the user server code)
238                 */
239                for (IBaseResource next : resourceList) {
240                        if (next.getIdElement() == null || next.getIdElement().isEmpty()) {
241                                if (!(next instanceof IBaseOperationOutcome)) {
242                                        throw new InternalErrorException(Msg.code(435) + "Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)");
243                                }
244                        }
245                }
246
247                BundleLinks links = new BundleLinks(theRequest.getFhirServerBase(), theIncludes, RestfulServerUtils.prettyPrintResponse(theServer, theRequest), theBundleType);
248                links.setSelf(theLinkSelf);
249
250                if (theResult.getCurrentPageOffset() != null) {
251
252                        if (isNotBlank(theResult.getNextPageId())) {
253                                links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), offset + limit, limit, theRequest.getParameters()));
254                        }
255                        if (isNotBlank(theResult.getPreviousPageId())) {
256                                links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), Math.max(offset - limit, 0), limit, theRequest.getParameters()));
257                        }
258
259                }
260
261                if (offset != null || (!theServer.canStoreSearchResults() && !isEverythingOperation(theRequest)) || isOffsetModeHistory()) {
262                        // Paging without caching
263                        // We're doing offset pages
264                        int requestedToReturn = numToReturn;
265                        if (theServer.getPagingProvider() == null && offset != null) {
266                                // There is no paging provider at all, so assume we're querying up to all the results we need every time
267                                requestedToReturn += offset;
268                        }
269                        if (numTotalResults == null || requestedToReturn < numTotalResults) {
270                                if (!resourceList.isEmpty()) {
271                                        links.setNext(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), defaultIfNull(offset, 0) + numToReturn, numToReturn, theRequest.getParameters()));
272                                }
273                        }
274                        if (offset != null && offset > 0) {
275                                int start = Math.max(0, theOffset - pageSize);
276                                links.setPrev(RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(), theRequest.getTenantId(), start, pageSize, theRequest.getParameters()));
277                        }
278                } else if (isNotBlank(theResult.getCurrentPageId())) {
279                        // We're doing named pages
280                        searchId = theResult.getUuid();
281                        if (isNotBlank(theResult.getNextPageId())) {
282                                links.setNext(RestfulServerUtils.createPagingLink(links, theRequest, searchId, theResult.getNextPageId(), theRequest.getParameters()));
283                        }
284                        if (isNotBlank(theResult.getPreviousPageId())) {
285                                links.setPrev(RestfulServerUtils.createPagingLink(links, theRequest, searchId, theResult.getPreviousPageId(), theRequest.getParameters()));
286                        }
287                } else if (searchId != null) {
288                        /*
289                         * We're doing offset pages - Note that we only return paging links if we actually
290                         * included some results in the response. We do this to avoid situations where
291                         * people have faked the offset number to some huge number to avoid them getting
292                         * back paging links that don't make sense.
293                         */
294                        if (resourceList.size() > 0) {
295                                if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
296                                        links.setNext((RestfulServerUtils.createPagingLink(links, theRequest, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters())));
297                                }
298                                if (theOffset > 0) {
299                                        int start = Math.max(0, theOffset - pageSize);
300                                        links.setPrev(RestfulServerUtils.createPagingLink(links, theRequest, searchId, start, pageSize, theRequest.getParameters()));
301                                }
302                        }
303                }
304
305                bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), links, theResult.size(), theResult.getPublished());
306                bundleFactory.addResourcesToBundle(new ArrayList<>(resourceList), theBundleType, links.serverBase, theServer.getBundleInclusionRule(), theIncludes);
307
308                return bundleFactory.getResourceBundle();
309
310        }
311
312        protected boolean isOffsetModeHistory() {
313                return false;
314        }
315
316        private boolean isEverythingOperation(RequestDetails theRequest) {
317                return (theRequest.getRestOperationType() == RestOperationTypeEnum.EXTENDED_OPERATION_TYPE
318                        || theRequest.getRestOperationType() == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE)
319                        && theRequest.getOperation() != null && theRequest.getOperation().equals("$everything");
320        }
321
322        public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) {
323                Object[] params = createMethodParams(theRequest);
324
325                Object resultObj = invokeServer(theServer, theRequest, params);
326                if (resultObj == null) {
327                        return null;
328                }
329
330                Integer count = RestfulServerUtils.extractCountParameter(theRequest);
331
332                final IBaseResource responseObject;
333
334                switch (getReturnType()) {
335                        case BUNDLE: {
336
337                                /*
338                                 * Figure out the self-link for this request
339                                 */
340
341                                BundleLinks bundleLinks = new BundleLinks(theRequest.getServerBaseForRequest(), null, RestfulServerUtils.prettyPrintResponse(theServer, theRequest), getResponseBundleType());
342                                bundleLinks.setSelf(RestfulServerUtils.createLinkSelf(theRequest.getFhirServerBase(), theRequest));
343
344                                if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE_RESOURCE) {
345                                        IBaseResource resource;
346                                        IPrimitiveType<Date> lastUpdated;
347                                        if (resultObj instanceof IBundleProvider) {
348                                                IBundleProvider result = (IBundleProvider) resultObj;
349                                                resource = result.getResources(0, 1).get(0);
350                                                lastUpdated = result.getPublished();
351                                        } else {
352                                                resource = (IBaseResource) resultObj;
353                                                lastUpdated = theServer.getFhirContext().getVersion().getLastUpdated(resource);
354                                        }
355
356                                        /*
357                                         * 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.
358                                         */
359                                        IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
360                                        bundleFactory.initializeWithBundleResource(resource);
361                                        bundleFactory.addRootPropertiesToBundle(null, bundleLinks, count, lastUpdated);
362
363                                        responseObject = resource;
364                                } else {
365                                        Set<Include> includes = getRequestIncludesFromParams(params);
366
367                                        IBundleProvider result = (IBundleProvider) resultObj;
368                                        if (count == null) {
369                                                count = result.preferredPageSize();
370                                        }
371
372                                        Integer offset = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET);
373                                        if (offset == null || offset < 0) {
374                                                offset = 0;
375                                        }
376
377                                        Integer resultSize = result.size();
378                                        int start = offset;
379                                        if (resultSize != null) {
380                                                start = Math.max(0, Math.min(offset, resultSize));
381                                        }
382
383                                        ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding());
384                                        EncodingEnum linkEncoding = theRequest.getParameters().containsKey(Constants.PARAM_FORMAT) && responseEncoding != null ? responseEncoding.getEncoding() : null;
385
386                                        responseObject = createBundleFromBundleProvider(theServer, theRequest, count, RestfulServerUtils.createLinkSelf(theRequest.getFhirServerBase(), theRequest), includes, result, start, getResponseBundleType(), linkEncoding, null);
387                                }
388                                break;
389                        }
390                        case RESOURCE: {
391                                IBundleProvider result = (IBundleProvider) resultObj;
392                                if (result.size() == 0) {
393                                        throw new ResourceNotFoundException(Msg.code(436) + "Resource " + theRequest.getId() + " is not known");
394                                } else if (result.size() > 1) {
395                                        throw new InternalErrorException(Msg.code(437) + "Method returned multiple resources");
396                                }
397
398                                IBaseResource resource = result.getResources(0, 1).get(0);
399                                responseObject = resource;
400                                break;
401                        }
402                        default:
403                                throw new IllegalStateException(Msg.code(438)); // should not happen
404                }
405                return responseObject;
406        }
407
408        public MethodReturnTypeEnum getMethodReturnType() {
409                return myMethodReturnType;
410        }
411
412        @Override
413        public String getResourceName() {
414                return myResourceName;
415        }
416
417        protected void setResourceName(String theResourceName) {
418                myResourceName = theResourceName;
419        }
420
421        /**
422         * If the response is a bundle, this type will be placed in the root of the bundle (can be null)
423         */
424        protected abstract BundleTypeEnum getResponseBundleType();
425
426        public abstract ReturnTypeEnum getReturnType();
427
428        @Override
429        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
430                IBaseResource response = doInvokeServer(theServer, theRequest);
431                /*
432                 When we write directly to an HttpServletResponse, the invocation returns null. However, we still want to invoke
433                 the SERVER_OUTGOING_RESPONSE pointcut.
434                */
435                if (response == null) {
436                        ResponseDetails responseDetails = new ResponseDetails();
437                        responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK);
438                        callOutgoingResponseHook(theRequest, responseDetails);
439                        return null;
440                } else {
441                        Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest);
442                        ResponseDetails responseDetails = new ResponseDetails();
443                        responseDetails.setResponseResource(response);
444                        responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK);
445                        if (!callOutgoingResponseHook(theRequest, responseDetails)) {
446                                return null;
447                        }
448                        boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest);
449
450                        return theRequest.getResponse().streamResponseAsResource(responseDetails.getResponseResource(), prettyPrint, summaryMode, responseDetails.getResponseCode(), null, theRequest.isRespondGzip(), isAddContentLocationHeader());
451                }
452        }
453
454        public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException;
455
456        /**
457         * 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
458         */
459        protected boolean isAddContentLocationHeader() {
460                return true;
461        }
462
463        public enum MethodReturnTypeEnum {
464                BUNDLE,
465                BUNDLE_PROVIDER,
466                BUNDLE_RESOURCE,
467                LIST_OF_RESOURCES,
468                METHOD_OUTCOME,
469                VOID,
470                RESOURCE
471        }
472
473        public enum ReturnTypeEnum {
474                BUNDLE,
475                RESOURCE
476        }
477
478        public static boolean callOutgoingResponseHook(RequestDetails theRequest, ResponseDetails theResponseDetails) {
479                HttpServletRequest servletRequest = null;
480                HttpServletResponse servletResponse = null;
481                if (theRequest instanceof ServletRequestDetails) {
482                        servletRequest = ((ServletRequestDetails) theRequest).getServletRequest();
483                        servletResponse = ((ServletRequestDetails) theRequest).getServletResponse();
484                }
485
486                HookParams responseParams = new HookParams();
487                responseParams.add(RequestDetails.class, theRequest);
488                responseParams.addIfMatchesType(ServletRequestDetails.class, theRequest);
489                responseParams.add(IBaseResource.class, theResponseDetails.getResponseResource());
490                responseParams.add(ResponseDetails.class, theResponseDetails);
491                responseParams.add(HttpServletRequest.class, servletRequest);
492                responseParams.add(HttpServletResponse.class, servletResponse);
493                if (theRequest.getInterceptorBroadcaster() != null) {
494                        if (!theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_RESPONSE, responseParams)) {
495                                return false;
496                        }
497                }
498                return true;
499        }
500
501        public static void callOutgoingFailureOperationOutcomeHook(RequestDetails theRequestDetails, IBaseOperationOutcome theOperationOutcome) {
502                HookParams responseParams = new HookParams();
503                responseParams.add(RequestDetails.class, theRequestDetails);
504                responseParams.addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
505                responseParams.add(IBaseOperationOutcome.class, theOperationOutcome);
506
507                if (theRequestDetails.getInterceptorBroadcaster() != null) {
508                        theRequestDetails.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_FAILURE_OPERATIONOUTCOME, responseParams);
509                }
510        }
511}