
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2023 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 if (response == null) { 281 ResponseDetails responseDetails = new ResponseDetails(); 282 responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK); 283 callOutgoingResponseHook(theRequest, responseDetails); 284 return null; 285 } else { 286 Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); 287 ResponseDetails responseDetails = new ResponseDetails(); 288 responseDetails.setResponseResource(response); 289 responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK); 290 if (!callOutgoingResponseHook(theRequest, responseDetails)) { 291 return null; 292 } 293 294 return RestfulServerUtils.streamResponseAsResource( 295 theServer, 296 responseDetails.getResponseResource(), 297 summaryMode, 298 responseDetails.getResponseCode(), 299 isAddContentLocationHeader(), 300 theRequest.isRespondGzip(), 301 theRequest, 302 null, 303 null); 304 } 305 } 306 307 public abstract Object invokeServer( 308 IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) 309 throws InvalidRequestException, InternalErrorException; 310 311 /** 312 * 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 313 */ 314 protected boolean isAddContentLocationHeader() { 315 return true; 316 } 317 318 public enum MethodReturnTypeEnum { 319 BUNDLE, 320 BUNDLE_PROVIDER, 321 BUNDLE_RESOURCE, 322 LIST_OF_RESOURCES, 323 METHOD_OUTCOME, 324 VOID, 325 RESOURCE 326 } 327 328 public enum ReturnTypeEnum { 329 BUNDLE, 330 RESOURCE 331 } 332 333 public static boolean callOutgoingResponseHook(RequestDetails theRequest, ResponseDetails theResponseDetails) { 334 HttpServletRequest servletRequest = null; 335 HttpServletResponse servletResponse = null; 336 if (theRequest instanceof ServletRequestDetails) { 337 servletRequest = ((ServletRequestDetails) theRequest).getServletRequest(); 338 servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); 339 } 340 341 HookParams responseParams = new HookParams(); 342 responseParams.add(RequestDetails.class, theRequest); 343 responseParams.addIfMatchesType(ServletRequestDetails.class, theRequest); 344 responseParams.add(IBaseResource.class, theResponseDetails.getResponseResource()); 345 responseParams.add(ResponseDetails.class, theResponseDetails); 346 responseParams.add(HttpServletRequest.class, servletRequest); 347 responseParams.add(HttpServletResponse.class, servletResponse); 348 if (theRequest.getInterceptorBroadcaster() != null) { 349 return theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_RESPONSE, responseParams); 350 } 351 return true; 352 } 353 354 public static void callOutgoingFailureOperationOutcomeHook( 355 RequestDetails theRequestDetails, IBaseOperationOutcome theOperationOutcome) { 356 HookParams responseParams = new HookParams(); 357 responseParams.add(RequestDetails.class, theRequestDetails); 358 responseParams.addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 359 responseParams.add(IBaseOperationOutcome.class, theOperationOutcome); 360 361 if (theRequestDetails.getInterceptorBroadcaster() != null) { 362 theRequestDetails 363 .getInterceptorBroadcaster() 364 .callHooks(Pointcut.SERVER_OUTGOING_FAILURE_OPERATIONOUTCOME, responseParams); 365 } 366 } 367}