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