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}