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