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