
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.method; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.model.api.IResource; 029import ca.uhn.fhir.model.api.Include; 030import ca.uhn.fhir.model.valueset.BundleTypeEnum; 031import ca.uhn.fhir.rest.api.BundleLinks; 032import ca.uhn.fhir.rest.api.Constants; 033import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory; 034import ca.uhn.fhir.rest.api.MethodOutcome; 035import ca.uhn.fhir.rest.api.SummaryEnum; 036import ca.uhn.fhir.rest.api.server.IBundleProvider; 037import ca.uhn.fhir.rest.api.server.IRestfulServer; 038import ca.uhn.fhir.rest.api.server.RequestDetails; 039import ca.uhn.fhir.rest.api.server.ResponseDetails; 040import ca.uhn.fhir.rest.server.RestfulServerUtils; 041import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 042import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 043import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 044import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 045import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 046import ca.uhn.fhir.util.ReflectionUtil; 047import jakarta.servlet.http.HttpServletRequest; 048import jakarta.servlet.http.HttpServletResponse; 049import org.apache.commons.lang3.Validate; 050import org.hl7.fhir.instance.model.api.IBaseBundle; 051import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 052import org.hl7.fhir.instance.model.api.IBaseResource; 053import org.hl7.fhir.instance.model.api.IPrimitiveType; 054 055import java.io.IOException; 056import java.lang.reflect.Method; 057import java.lang.reflect.Modifier; 058import java.util.Collection; 059import java.util.Date; 060import java.util.Set; 061 062public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding { 063 protected final ResponseBundleBuilder myResponseBundleBuilder; 064 065 private MethodReturnTypeEnum myMethodReturnType; 066 private String myResourceName; 067 068 @SuppressWarnings("unchecked") 069 public BaseResourceReturningMethodBinding( 070 Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 071 super(theMethod, theContext, theProvider); 072 073 Class<?> methodReturnType = theMethod.getReturnType(); 074 075 Set<Class<?>> expectedReturnTypes = provideExpectedReturnTypes(); 076 if (expectedReturnTypes != null) { 077 078 Validate.isTrue( 079 expectedReturnTypes.contains(methodReturnType), 080 "Unexpected method return type on %s - Allowed: %s", 081 theMethod, 082 expectedReturnTypes); 083 084 } else if (Collection.class.isAssignableFrom(methodReturnType)) { 085 086 myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; 087 Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); 088 if (collectionType != null) { 089 if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) { 090 throw new ConfigurationException(Msg.code(433) + "Method " 091 + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() 092 + " returns an invalid collection generic type: " + collectionType); 093 } 094 } 095 096 } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) { 097 098 if (IBaseBundle.class.isAssignableFrom(methodReturnType)) { 099 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE; 100 } else { 101 myMethodReturnType = MethodReturnTypeEnum.RESOURCE; 102 } 103 } else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) { 104 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER; 105 } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) { 106 myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME; 107 } else if (void.class.equals(methodReturnType)) { 108 myMethodReturnType = MethodReturnTypeEnum.VOID; 109 } else { 110 throw new ConfigurationException(Msg.code(434) + "Invalid return type '" 111 + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " 112 + theMethod.getDeclaringClass().getCanonicalName()); 113 } 114 115 if (theReturnResourceType != null) { 116 if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { 117 118 // If we're returning an abstract type, that's ok, but if we know the resource 119 // type let's grab it 120 if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) 121 && !Modifier.isInterface(theReturnResourceType.getModifiers())) { 122 Class<? extends IBaseResource> resourceType = (Class<? extends IResource>) theReturnResourceType; 123 RuntimeResourceDefinition resourceDefinition = theContext.getResourceDefinition(resourceType); 124 myResourceName = resourceDefinition.getName(); 125 } 126 } 127 } 128 129 myResponseBundleBuilder = new ResponseBundleBuilder(isOffsetModeHistory()); 130 } 131 132 /** 133 * Subclasses may override 134 */ 135 protected Set<Class<?>> provideExpectedReturnTypes() { 136 return null; 137 } 138 139 protected boolean isOffsetModeHistory() { 140 return false; 141 } 142 143 public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) { 144 Object[] params = createMethodParams(theRequest); 145 146 Object resultObj = invokeServer(theServer, theRequest, params); 147 if (resultObj == null) { 148 return null; 149 } 150 151 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 152 153 final IBaseResource responseObject; 154 155 switch (getReturnType()) { 156 case BUNDLE: { 157 158 /* 159 * Figure out the self-link for this request 160 */ 161 162 BundleTypeEnum responseBundleType = getResponseBundleType(); 163 BundleLinks bundleLinks = new BundleLinks( 164 theRequest.getServerBaseForRequest(), 165 null, 166 RestfulServerUtils.prettyPrintResponse(theServer, theRequest), 167 responseBundleType); 168 String linkSelf = RestfulServerUtils.createLinkSelf(theRequest.getFhirServerBase(), theRequest); 169 bundleLinks.setSelf(linkSelf); 170 171 if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE_RESOURCE) { 172 IBaseResource resource; 173 IPrimitiveType<Date> lastUpdated; 174 if (resultObj instanceof IBundleProvider) { 175 IBundleProvider result = (IBundleProvider) resultObj; 176 resource = result.getResources(0, 1).get(0); 177 lastUpdated = result.getPublished(); 178 } else { 179 resource = (IBaseResource) resultObj; 180 lastUpdated = theServer.getFhirContext().getVersion().getLastUpdated(resource); 181 } 182 183 /* 184 * 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. 185 */ 186 IVersionSpecificBundleFactory bundleFactory = 187 theServer.getFhirContext().newBundleFactory(); 188 bundleFactory.initializeWithBundleResource(resource); 189 bundleFactory.addRootPropertiesToBundle(null, bundleLinks, count, lastUpdated); 190 191 responseObject = resource; 192 } else { 193 ResponseBundleRequest responseBundleRequest = buildResponseBundleRequest( 194 theServer, 195 theRequest, 196 params, 197 (IBundleProvider) resultObj, 198 count, 199 responseBundleType, 200 linkSelf); 201 responseObject = myResponseBundleBuilder.buildResponseBundle(responseBundleRequest); 202 } 203 break; 204 } 205 case RESOURCE: { 206 IBundleProvider result = (IBundleProvider) resultObj; 207 Integer size = result.size(); 208 if (size == null || size == 0) { 209 throw new ResourceNotFoundException( 210 Msg.code(436) + "Resource " + theRequest.getId() + " is not known"); 211 } else if (size > 1) { 212 throw new InternalErrorException(Msg.code(437) + "Method returned multiple resources"); 213 } 214 215 responseObject = result.getResources(0, 1).get(0); 216 break; 217 } 218 default: 219 throw new IllegalStateException(Msg.code(438)); // should not happen 220 } 221 return responseObject; 222 } 223 224 private ResponseBundleRequest buildResponseBundleRequest( 225 IRestfulServer<?> theServer, 226 RequestDetails theRequest, 227 Object[] theParams, 228 IBundleProvider theBundleProvider, 229 Integer theCount, 230 BundleTypeEnum theBundleTypeEnum, 231 String theLinkSelf) { 232 Set<Include> includes = getRequestIncludesFromParams(theParams); 233 234 if (theCount == null) { 235 theCount = theBundleProvider.preferredPageSize(); 236 } 237 238 int offset = OffsetCalculator.calculateOffset(theRequest, theBundleProvider); 239 240 return new ResponseBundleRequest( 241 theServer, 242 theBundleProvider, 243 theRequest, 244 offset, 245 theCount, 246 theLinkSelf, 247 includes, 248 theBundleTypeEnum, 249 null); 250 } 251 252 public MethodReturnTypeEnum getMethodReturnType() { 253 return myMethodReturnType; 254 } 255 256 @Override 257 public String getResourceName() { 258 return myResourceName; 259 } 260 261 protected void setResourceName(String theResourceName) { 262 myResourceName = theResourceName; 263 } 264 265 /** 266 * If the response is a bundle, this type will be placed in the root of the bundle (can be null) 267 */ 268 protected abstract BundleTypeEnum getResponseBundleType(); 269 270 public abstract ReturnTypeEnum getReturnType(); 271 272 @Override 273 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) 274 throws BaseServerResponseException, IOException { 275 IBaseResource response = doInvokeServer(theServer, theRequest); 276 /* 277 When we write directly to an HttpServletResponse, the invocation returns null. However, we still want to invoke 278 the SERVER_OUTGOING_RESPONSE pointcut. 279 */ 280 281 // if the response status code is set by the method, respect it. Otherwise, use the default 200. 282 int responseCode = Constants.STATUS_HTTP_200_OK; 283 if (theRequest instanceof ServletRequestDetails) { 284 HttpServletResponse servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); 285 if (servletResponse != null && servletResponse.getStatus() > 0) { 286 responseCode = servletResponse.getStatus(); 287 } 288 } 289 290 if (response == null) { 291 ResponseDetails responseDetails = new ResponseDetails(); 292 responseDetails.setResponseCode(responseCode); 293 callOutgoingResponseHook(theRequest, responseDetails); 294 return null; 295 } else { 296 Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); 297 ResponseDetails responseDetails = new ResponseDetails(); 298 responseDetails.setResponseResource(response); 299 responseDetails.setResponseCode(responseCode); 300 301 if (!callOutgoingResponseHook(theRequest, responseDetails)) { 302 return null; 303 } 304 305 return RestfulServerUtils.streamResponseAsResource( 306 theServer, 307 responseDetails.getResponseResource(), 308 summaryMode, 309 responseDetails.getResponseCode(), 310 isAddContentLocationHeader(), 311 theRequest.isRespondGzip(), 312 theRequest, 313 null, 314 null); 315 } 316 } 317 318 public abstract Object invokeServer( 319 IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) 320 throws InvalidRequestException, InternalErrorException; 321 322 /** 323 * 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 324 */ 325 protected boolean isAddContentLocationHeader() { 326 return true; 327 } 328 329 public enum MethodReturnTypeEnum { 330 BUNDLE, 331 BUNDLE_PROVIDER, 332 BUNDLE_RESOURCE, 333 LIST_OF_RESOURCES, 334 METHOD_OUTCOME, 335 VOID, 336 RESOURCE 337 } 338 339 public enum ReturnTypeEnum { 340 BUNDLE, 341 RESOURCE 342 } 343 344 public static boolean callOutgoingResponseHook(RequestDetails theRequest, ResponseDetails theResponseDetails) { 345 HttpServletRequest servletRequest = null; 346 HttpServletResponse servletResponse = null; 347 if (theRequest instanceof ServletRequestDetails) { 348 servletRequest = ((ServletRequestDetails) theRequest).getServletRequest(); 349 servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); 350 } 351 352 HookParams responseParams = new HookParams(); 353 responseParams.add(RequestDetails.class, theRequest); 354 responseParams.addIfMatchesType(ServletRequestDetails.class, theRequest); 355 responseParams.add(IBaseResource.class, theResponseDetails.getResponseResource()); 356 responseParams.add(ResponseDetails.class, theResponseDetails); 357 responseParams.add(HttpServletRequest.class, servletRequest); 358 responseParams.add(HttpServletResponse.class, servletResponse); 359 if (theRequest.getInterceptorBroadcaster() != null) { 360 return theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_RESPONSE, responseParams); 361 } 362 return true; 363 } 364 365 public static void callOutgoingFailureOperationOutcomeHook( 366 RequestDetails theRequestDetails, IBaseOperationOutcome theOperationOutcome) { 367 HookParams responseParams = new HookParams(); 368 responseParams.add(RequestDetails.class, theRequestDetails); 369 responseParams.addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 370 responseParams.add(IBaseOperationOutcome.class, theOperationOutcome); 371 372 if (theRequestDetails.getInterceptorBroadcaster() != null) { 373 theRequestDetails 374 .getInterceptorBroadcaster() 375 .callHooks(Pointcut.SERVER_OUTGOING_FAILURE_OPERATIONOUTCOME, responseParams); 376 } 377 } 378}