001package ca.uhn.fhir.rest.server.interceptor; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.interceptor.api.Hook; 025import ca.uhn.fhir.interceptor.api.HookParams; 026import ca.uhn.fhir.interceptor.api.IInterceptorService; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.model.api.TagList; 029import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; 030import ca.uhn.fhir.rest.annotation.Read; 031import ca.uhn.fhir.rest.annotation.ResourceParam; 032import ca.uhn.fhir.rest.annotation.Search; 033import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 034import ca.uhn.fhir.rest.api.server.RequestDetails; 035import ca.uhn.fhir.rest.api.server.ResponseDetails; 036import ca.uhn.fhir.rest.server.IRestfulServerDefaults; 037import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; 038import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 039import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 040import org.apache.commons.lang3.builder.ToStringBuilder; 041import org.apache.commons.lang3.builder.ToStringStyle; 042import org.hl7.fhir.instance.model.api.IBaseResource; 043import org.hl7.fhir.instance.model.api.IIdType; 044 045import javax.servlet.ServletException; 046import javax.servlet.http.HttpServletRequest; 047import javax.servlet.http.HttpServletResponse; 048import java.io.IOException; 049import java.util.Collections; 050import java.util.Map; 051 052import static org.apache.commons.lang3.StringUtils.isBlank; 053 054/** 055 * Provides methods to intercept requests and responses. Note that implementations of this interface may wish to use 056 * {@link InterceptorAdapter} in order to not need to implement every method. 057 * <p> 058 * <b>See:</b> See the <a href="https://hapifhir.io/hapi-fhir/docs/interceptors/">server 059 * interceptor documentation</a> for more information on how to use this class. 060 * </p> 061 * Note that unless otherwise stated, it is possible to throw any subclass of 062 * {@link BaseServerResponseException} from any interceptor method. 063 */ 064public interface IServerInterceptor { 065 066 /** 067 * This method is called upon any exception being thrown within the server's request processing code. This includes 068 * any exceptions thrown within resource provider methods (e.g. {@link Search} and {@link Read} methods) as well as 069 * any runtime exceptions thrown by the server itself. This also includes any {@link AuthenticationException}s 070 * thrown. 071 * <p> 072 * Implementations of this method may choose to ignore/log/count/etc exceptions, and return <code>true</code>. In 073 * this case, processing will continue, and the server will automatically generate an {@link BaseOperationOutcome 074 * OperationOutcome}. Implementations may also choose to provide their own response to the client. In this case, they 075 * should return <code>false</code>, to indicate that they have handled the request and processing should stop. 076 * </p> 077 * 078 * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the 079 * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been 080 * pulled out of the {@link javax.servlet.http.HttpServletRequest servlet request}. Note that the bean 081 * properties are not all guaranteed to be populated, depending on how early during processing the 082 * exception occurred. 083 * @param theServletRequest The incoming request 084 * @param theServletResponse The response. Note that interceptors may choose to provide a response (i.e. by calling 085 * {@link javax.servlet.http.HttpServletResponse#getWriter()}) but in that case it is important to return 086 * <code>false</code> to indicate that the server itself should not also provide a response. 087 * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. 088 * If your interceptor is providing a response rather than letting HAPI handle the response normally, you 089 * must return <code>false</code>. In this case, no further processing will occur and no further interceptors 090 * will be called. 091 * @throws ServletException If this exception is thrown, it will be re-thrown up to the container for handling. 092 * @throws IOException If this exception is thrown, it will be re-thrown up to the container for handling. 093 */ 094 @Hook(Pointcut.SERVER_HANDLE_EXCEPTION) 095 boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) 096 throws ServletException, IOException; 097 098 /** 099 * This method is called just before the actual implementing server method is invoked. 100 * 101 * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the 102 * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been 103 * pulled out of the {@link HttpServletRequest servlet request}. 104 * @param theRequest The incoming request 105 * @param theResponse The response. Note that interceptors may choose to provide a response (i.e. by calling 106 * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> 107 * to indicate that the server itself should not also provide a response. 108 * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. 109 * If your interceptor is providing a response rather than letting HAPI handle the response normally, you 110 * must return <code>false</code>. In this case, no further processing will occur and no further interceptors 111 * will be called. 112 * @throws AuthenticationException This exception may be thrown to indicate that the interceptor has detected an unauthorized access 113 * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. 114 */ 115 @Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED) 116 boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException; 117 118 /** 119 * Invoked before an incoming request is processed. Note that this method is called 120 * after the server has begin preparing the response to the incoming client request. 121 * As such, it is not able to supply a response to the incoming request in the way that 122 * {@link #incomingRequestPreHandled(RestOperationTypeEnum, ActionRequestDetails)} and 123 * {@link #incomingRequestPostProcessed(RequestDetails, HttpServletRequest, HttpServletResponse)} 124 * are. 125 * <p> 126 * This method may however throw a subclass of {@link BaseServerResponseException}, and processing 127 * will be aborted with an appropriate error returned to the client. 128 * </p> 129 * 130 * @param theOperation The type of operation that the FHIR server has determined that the client is trying to invoke 131 * @param theProcessedRequest An object which will be populated with the details which were extracted from the raw request by the 132 * server, e.g. the FHIR operation type and the parsed resource body (if any). 133 */ 134 @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED) 135 void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theProcessedRequest); 136 137 /** 138 * This method is called before any other processing takes place for each incoming request. It may be used to provide 139 * alternate handling for some requests, or to screen requests before they are handled, etc. 140 * <p> 141 * Note that any exceptions thrown by this method will not be trapped by HAPI (they will be passed up to the server) 142 * </p> 143 * 144 * @param theRequest The incoming request 145 * @param theResponse The response. Note that interceptors may choose to provide a response (i.e. by calling 146 * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> 147 * to indicate that the server itself should not also provide a response. 148 * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. 149 * If your interceptor is providing a response rather than letting HAPI handle the response normally, you 150 * must return <code>false</code>. In this case, no further processing will occur and no further interceptors 151 * will be called. 152 */ 153 @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED) 154 boolean incomingRequestPreProcessed(HttpServletRequest theRequest, HttpServletResponse theResponse); 155 156 /** 157 * Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead 158 * 159 * @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR. 160 */ 161 @Deprecated 162 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 163 boolean outgoingResponse(RequestDetails theRequestDetails); 164 165 /** 166 * Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead 167 * 168 * @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR. 169 */ 170 @Deprecated 171 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 172 boolean outgoingResponse(RequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; 173 174 /** 175 * Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead 176 * 177 * @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR. 178 */ 179 @Deprecated 180 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 181 boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject); 182 183 /** 184 * This method is called after the server implementation method has been called, but before any attempt to stream the 185 * response back to the client. 186 * 187 * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the 188 * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been 189 * pulled out of the {@link HttpServletRequest servlet request}. 190 * @param theResponseObject The actual object which is being streamed to the client as a response. This may be 191 * <code>null</code> if the response does not include a resource. 192 * @param theServletRequest The incoming request 193 * @param theServletResponse The response. Note that interceptors may choose to provide a response (i.e. by calling 194 * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> 195 * to indicate that the server itself should not also provide a response. 196 * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. 197 * If your interceptor is providing a response rather than letting HAPI handle the response normally, you 198 * must return <code>false</code>. In this case, no further processing will occur and no further interceptors 199 * will be called. 200 * @throws AuthenticationException This exception may be thrown to indicate that the interceptor has detected an unauthorized access 201 * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. 202 * @deprecated As of HAPI FHIR 3.3.0, this method has been deprecated in 203 * favour of {@link #outgoingResponse(RequestDetails, ResponseDetails, HttpServletRequest, HttpServletResponse)} 204 * and will be removed in a future version of HAPI FHIR. 205 */ 206 @Deprecated 207 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 208 boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) 209 throws AuthenticationException; 210 211 /** 212 * This method is called after the server implementation method has been called, but before any attempt to stream the 213 * response back to the client. 214 * 215 * @param theRequestDetails A bean containing details about the request that is about to be processed, including details such as the 216 * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been 217 * pulled out of the {@link HttpServletRequest servlet request}. 218 * @param theResponseDetails This object contains details about the response, including 219 * the actual payload that will be returned 220 * @param theServletRequest The incoming request 221 * @param theServletResponse The response. Note that interceptors may choose to provide a response (i.e. by calling 222 * {@link HttpServletResponse#getWriter()}) but in that case it is important to return <code>false</code> 223 * to indicate that the server itself should not also provide a response. 224 * @return Return <code>true</code> if processing should continue normally. This is generally the right thing to do. 225 * If your interceptor is providing a response rather than letting HAPI handle the response normally, you 226 * must return <code>false</code>. In this case, no further processing will occur and no further interceptors 227 * will be called. 228 * @throws AuthenticationException This exception may be thrown to indicate that the interceptor has detected an unauthorized access 229 * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. 230 */ 231 @Hook(Pointcut.SERVER_OUTGOING_RESPONSE) 232 boolean outgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) 233 throws AuthenticationException; 234 235 236 /** 237 * Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead 238 * 239 * @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR. 240 */ 241 @Deprecated 242 boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject); 243 244 /** 245 * Use {@link #outgoingResponse(RequestDetails, IBaseResource, HttpServletRequest, HttpServletResponse)} instead 246 * 247 * @deprecated As of HAPI FHIR 3.2.0, this method is deprecated and will be removed in a future version of HAPI FHIR. 248 */ 249 @Deprecated 250 boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; 251 252 /** 253 * This method is called upon any exception being thrown within the server's request processing code. This includes 254 * any exceptions thrown within resource provider methods (e.g. {@link Search} and {@link Read} methods) as well as 255 * any runtime exceptions thrown by the server itself. This method is invoked for each interceptor (until one of them 256 * returns a non-<code>null</code> response or the end of the list is reached), after which 257 * {@link #handleException(RequestDetails, BaseServerResponseException, HttpServletRequest, HttpServletResponse)} is 258 * called for each interceptor. 259 * <p> 260 * This may be used to add an OperationOutcome to a response, or to convert between exception types for any reason. 261 * </p> 262 * <p> 263 * Implementations of this method may choose to ignore/log/count/etc exceptions, and return <code>null</code>. In 264 * this case, processing will continue, and the server will automatically generate an {@link BaseOperationOutcome 265 * OperationOutcome}. Implementations may also choose to provide their own response to the client. In this case, they 266 * should return a non-<code>null</code>, to indicate that they have handled the request and processing should stop. 267 * </p> 268 * 269 * @return Returns the new exception to use for processing, or <code>null</code> if this interceptor is not trying to 270 * modify the exception. For example, if this interceptor has nothing to do with exception processing, it 271 * should always return <code>null</code>. If this interceptor adds an OperationOutcome to the exception, it 272 * should return an exception. 273 */ 274 @Hook(Pointcut.SERVER_PRE_PROCESS_OUTGOING_EXCEPTION) 275 BaseServerResponseException preProcessOutgoingException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) throws ServletException; 276 277 /** 278 * This method is called after all processing is completed for a request, but only if the 279 * request completes normally (i.e. no exception is thrown). 280 * <p> 281 * This method should not throw any exceptions. Any exception that is thrown by this 282 * method will be logged, but otherwise not acted upon. 283 * </p> 284 * <p> 285 * Note that this individual interceptors will have this method called in the reverse order from the order in 286 * which the interceptors were registered with the server. 287 * </p> 288 * 289 * @param theRequestDetails The request itself 290 */ 291 @Hook(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY) 292 void processingCompletedNormally(ServletRequestDetails theRequestDetails); 293 294 /** 295 * @deprecated This class doesn't bring anything that can't be done with {@link RequestDetails}. That 296 * class should be used instead. Deprecated in 4.0.0 297 */ 298 @Deprecated 299 class ActionRequestDetails { 300 private final FhirContext myContext; 301 private final IIdType myId; 302 private final String myResourceType; 303 private RequestDetails myRequestDetails; 304 private IBaseResource myResource; 305 306 public ActionRequestDetails(RequestDetails theRequestDetails) { 307 myId = theRequestDetails.getId(); 308 myResourceType = theRequestDetails.getResourceName(); 309 myContext = theRequestDetails.getServer().getFhirContext(); 310 myRequestDetails = theRequestDetails; 311 } 312 313 public ActionRequestDetails(RequestDetails theRequestDetails, FhirContext theContext, IBaseResource theResource) { 314 this(theRequestDetails, theContext, theContext.getResourceType(theResource), theResource.getIdElement()); 315 myResource = theResource; 316 } 317 318 public ActionRequestDetails(RequestDetails theRequestDetails, FhirContext theContext, String theResourceType, IIdType theId) { 319 if (theId != null && isBlank(theId.getValue())) { 320 myId = null; 321 } else { 322 myId = theId; 323 } 324 myResourceType = theResourceType; 325 myContext = theContext; 326 myRequestDetails = theRequestDetails; 327 } 328 329 public ActionRequestDetails(RequestDetails theRequestDetails, IBaseResource theResource) { 330 this(theRequestDetails, theRequestDetails.getServer().getFhirContext().getResourceType(theResource), theResource.getIdElement()); 331 myResource = theResource; 332 } 333 334 public ActionRequestDetails(RequestDetails theRequestDetails, IBaseResource theResource, String theResourceType, IIdType theId) { 335 this(theRequestDetails, theResourceType, theId); 336 myResource = theResource; 337 } 338 339 /** 340 * Constructor 341 * 342 * @param theRequestDetails The request details to wrap 343 * @param theId The ID of the resource being created (note that the ID should have the resource type populated) 344 */ 345 public ActionRequestDetails(RequestDetails theRequestDetails, IIdType theId) { 346 this(theRequestDetails, theId.getResourceType(), theId); 347 } 348 349 public ActionRequestDetails(RequestDetails theRequestDetails, String theResourceType, IIdType theId) { 350 this(theRequestDetails, theRequestDetails.getServer().getFhirContext(), theResourceType, theId); 351 } 352 353 public FhirContext getContext() { 354 return myContext; 355 } 356 357 /** 358 * Returns the ID of the incoming request (typically this is from the request URL) 359 */ 360 public IIdType getId() { 361 return myId; 362 } 363 364 /** 365 * Returns the request details associated with this request 366 */ 367 public RequestDetails getRequestDetails() { 368 return myRequestDetails; 369 } 370 371 /** 372 * For requests where a resource is passed from the client to the server (e.g. create, update, etc.) this method 373 * will return the resource which was provided by the client. Otherwise, this method will return <code>null</code> 374 * . 375 * <p> 376 * Note that this method is currently only populated if the handling method has a parameter annotated with the 377 * {@link ResourceParam} annotation. 378 * </p> 379 */ 380 public IBaseResource getResource() { 381 return myResource; 382 } 383 384 /** 385 * This method should not be called by client code 386 */ 387 public void setResource(IBaseResource theObject) { 388 myResource = theObject; 389 } 390 391 /** 392 * Returns the resource type this request pertains to, or <code>null</code> if this request is not type specific 393 * (e.g. server-history) 394 */ 395 public String getResourceType() { 396 return myResourceType; 397 } 398 399 @Override 400 public String toString() { 401 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 402 .append("id", myId) 403 .append("resourceType", myResourceType) 404 .append("resource", myResource) 405 .toString(); 406 } 407 408 /** 409 * Returns the same map which was 410 */ 411 public Map<Object, Object> getUserData() { 412 if (myRequestDetails == null) { 413 /* 414 * Technically this shouldn't happen.. But some of the unit tests use old IXXXDao methods that don't 415 * take in a RequestDetails object. Eventually I guess we should clean that up. 416 */ 417 return Collections.emptyMap(); 418 } 419 return myRequestDetails.getUserData(); 420 } 421 422 /** 423 * This method may be invoked by user code to notify interceptors that a nested 424 * operation is being invoked which is denoted by this request details. 425 */ 426 public void notifyIncomingRequestPreHandled(RestOperationTypeEnum theOperationType) { 427 RequestDetails requestDetails = getRequestDetails(); 428 if (requestDetails == null) { 429 return; 430 } 431 IRestfulServerDefaults server = requestDetails.getServer(); 432 if (server == null) { 433 return; 434 } 435 436 IIdType previousRequestId = requestDetails.getId(); 437 requestDetails.setId(getId()); 438 439 IInterceptorService interceptorService = server.getInterceptorService(); 440 if (interceptorService == null) { 441 return; 442 } 443 444 HookParams params = new HookParams(); 445 params.add(RestOperationTypeEnum.class, theOperationType); 446 params.add(this); 447 params.add(RequestDetails.class, this.getRequestDetails()); 448 params.addIfMatchesType(ServletRequestDetails.class, this.getRequestDetails()); 449 interceptorService.callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, params); 450 451 // Reset the request ID 452 requestDetails.setId(previousRequestId); 453 454 } 455 456 } 457 458}