001package ca.uhn.fhir.rest.server.interceptor;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2021 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}