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