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