001package ca.uhn.fhir.rest.server.interceptor;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
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="http://jamesagnew.github.io/hapi-fhir/doc_rest_server_interceptor.html">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        class ActionRequestDetails {
295                private final FhirContext myContext;
296                private final IIdType myId;
297                private final String myResourceType;
298                private RequestDetails myRequestDetails;
299                private IBaseResource myResource;
300
301                public ActionRequestDetails(RequestDetails theRequestDetails) {
302                        myId = theRequestDetails.getId();
303                        myResourceType = theRequestDetails.getResourceName();
304                        myContext = theRequestDetails.getServer().getFhirContext();
305                        myRequestDetails = theRequestDetails;
306                }
307
308                public ActionRequestDetails(RequestDetails theRequestDetails, FhirContext theContext, IBaseResource theResource) {
309                        this(theRequestDetails, theContext, theContext.getResourceDefinition(theResource).getName(), theResource.getIdElement());
310                        myResource = theResource;
311                }
312
313                public ActionRequestDetails(RequestDetails theRequestDetails, FhirContext theContext, String theResourceType, IIdType theId) {
314                        if (theId != null && isBlank(theId.getValue())) {
315                                myId = null;
316                        } else {
317                                myId = theId;
318                        }
319                        myResourceType = theResourceType;
320                        myContext = theContext;
321                        myRequestDetails = theRequestDetails;
322                }
323
324                public ActionRequestDetails(RequestDetails theRequestDetails, IBaseResource theResource) {
325                        this(theRequestDetails, theRequestDetails.getServer().getFhirContext().getResourceDefinition(theResource).getName(), theResource.getIdElement());
326                        myResource = theResource;
327                }
328
329                public ActionRequestDetails(RequestDetails theRequestDetails, IBaseResource theResource, String theResourceType, IIdType theId) {
330                        this(theRequestDetails, theResourceType, theId);
331                        myResource = theResource;
332                }
333
334                /**
335                 * Constructor
336                 *
337                 * @param theRequestDetails The request details to wrap
338                 * @param theId             The ID of the resource being created (note that the ID should have the resource type populated)
339                 */
340                public ActionRequestDetails(RequestDetails theRequestDetails, IIdType theId) {
341                        this(theRequestDetails, theId.getResourceType(), theId);
342                }
343
344                public ActionRequestDetails(RequestDetails theRequestDetails, String theResourceType, IIdType theId) {
345                        this(theRequestDetails, theRequestDetails.getServer().getFhirContext(), theResourceType, theId);
346                }
347
348                public FhirContext getContext() {
349                        return myContext;
350                }
351
352                /**
353                 * Returns the ID of the incoming request (typically this is from the request URL)
354                 */
355                public IIdType getId() {
356                        return myId;
357                }
358
359                /**
360                 * Returns the request details associated with this request
361                 */
362                public RequestDetails getRequestDetails() {
363                        return myRequestDetails;
364                }
365
366                /**
367                 * For requests where a resource is passed from the client to the server (e.g. create, update, etc.) this method
368                 * will return the resource which was provided by the client. Otherwise, this method will return <code>null</code>
369                 * .
370                 * <p>
371                 * Note that this method is currently only populated if the handling method has a parameter annotated with the
372                 * {@link ResourceParam} annotation.
373                 * </p>
374                 */
375                public IBaseResource getResource() {
376                        return myResource;
377                }
378
379                /**
380                 * This method should not be called by client code
381                 */
382                public void setResource(IBaseResource theObject) {
383                        myResource = theObject;
384                }
385
386                /**
387                 * Returns the resource type this request pertains to, or <code>null</code> if this request is not type specific
388                 * (e.g. server-history)
389                 */
390                public String getResourceType() {
391                        return myResourceType;
392                }
393
394                @Override
395                public String toString() {
396                        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
397                                .append("id", myId)
398                                .append("resourceType", myResourceType)
399                                .append("resource", myResource)
400                                .toString();
401                }
402
403                /**
404                 * Returns the same map which was
405                 */
406                public Map<Object, Object> getUserData() {
407                        if (myRequestDetails == null) {
408                                /*
409                                 * Technically this shouldn't happen.. But some of the unit tests use old IXXXDao methods that don't
410                                 * take in a RequestDetails object. Eventually I guess we should clean that up.
411                                 */
412                                return Collections.emptyMap();
413                        }
414                        return myRequestDetails.getUserData();
415                }
416
417                /**
418                 * This method may be invoked by user code to notify interceptors that a nested
419                 * operation is being invoked which is denoted by this request details.
420                 */
421                public void notifyIncomingRequestPreHandled(RestOperationTypeEnum theOperationType) {
422                        RequestDetails requestDetails = getRequestDetails();
423                        if (requestDetails == null) {
424                                return;
425                        }
426                        IRestfulServerDefaults server = requestDetails.getServer();
427                        if (server == null) {
428                                return;
429                        }
430
431                        IInterceptorService interceptorService = server.getInterceptorService();
432
433                        HookParams params = new HookParams();
434                        params.add(RestOperationTypeEnum.class, theOperationType);
435                        params.add(this);
436                        interceptorService.callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, params);
437
438                }
439
440        }
441
442}