001package ca.uhn.fhir.rest.server.interceptor;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 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 */
022import static org.apache.commons.lang3.StringUtils.isNotBlank;
023
024import java.io.IOException;
025import java.util.Collections;
026import java.util.Map;
027import java.util.Map.Entry;
028
029import javax.servlet.ServletException;
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032
033import org.apache.commons.lang3.exception.ExceptionUtils;
034import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
035
036import ca.uhn.fhir.context.FhirContext;
037import ca.uhn.fhir.rest.api.SummaryEnum;
038import ca.uhn.fhir.rest.method.RequestDetails;
039import ca.uhn.fhir.rest.server.IRestfulResponse;
040import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
041import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
042import ca.uhn.fhir.util.OperationOutcomeUtil;
043
044public class ExceptionHandlingInterceptor extends InterceptorAdapter {
045
046        public static final String PROCESSING = "processing";
047        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptor.class);
048        private Class<?>[] myReturnStackTracesForExceptionTypes;
049
050        @Override
051        public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException {
052                handleException(theRequestDetails, theException);
053                return false;
054        }
055
056        public Object handleException(RequestDetails theRequestDetails, BaseServerResponseException theException)
057                        throws ServletException, IOException {
058                IRestfulResponse response = theRequestDetails.getResponse();
059
060                FhirContext ctx = theRequestDetails.getServer().getFhirContext();
061
062                IBaseOperationOutcome oo = theException.getOperationOutcome();
063                if (oo == null) {
064                        oo = createOperationOutcome(theException, ctx);
065                }
066
067                int statusCode = theException.getStatusCode();
068
069                // Add headers associated with the specific error code
070                Map<String, String[]> additional = theException.getAssociatedHeaders();
071                if (additional != null) {
072                        for (Entry<String, String[]> next : additional.entrySet()) {
073                                if (isNotBlank(next.getKey()) && next.getValue() != null) {
074                                        String nextKey = next.getKey();
075                                        for (String nextValue : next.getValue()) {
076                                                response.addHeader(nextKey, nextValue);
077                                        }
078                                }
079                        }
080                }
081
082                return response.streamResponseAsResource(oo, true, Collections.singleton(SummaryEnum.FALSE), statusCode, false, false);
083                // theResponse.setStatus(statusCode);
084                // theRequestDetails.getServer().addHeadersToResponse(theResponse);
085                // theResponse.setContentType("text/plain");
086                // theResponse.setCharacterEncoding("UTF-8");
087                // theResponse.getWriter().append(theException.getMessage());
088                // theResponse.getWriter().close();
089        }
090
091        @Override
092        public BaseServerResponseException preProcessOutgoingException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) throws ServletException {
093                BaseServerResponseException retVal;
094                if (!(theException instanceof BaseServerResponseException)) {
095                        retVal = new InternalErrorException(theException);
096                } else {
097                        retVal = (BaseServerResponseException) theException;
098                }
099
100                if (retVal.getOperationOutcome() == null) {
101                        retVal.setOperationOutcome(createOperationOutcome(theException, theRequestDetails.getServer().getFhirContext()));
102                }
103
104                return retVal;
105        }
106
107        private IBaseOperationOutcome createOperationOutcome(Throwable theException, FhirContext ctx) throws ServletException {
108                IBaseOperationOutcome oo = null;
109                if (theException instanceof BaseServerResponseException) {
110                        oo = ((BaseServerResponseException) theException).getOperationOutcome();
111                }
112
113                /*
114                 * Generate an OperationOutcome to return, unless the exception throw by the resource provider had one
115                 */
116                if (oo == null) {
117                        try {
118                                oo = OperationOutcomeUtil.newInstance(ctx);
119
120                                if (theException instanceof InternalErrorException) {
121                                        ourLog.error("Failure during REST processing", theException);
122                                        populateDetails(ctx, theException, oo);
123                                } else if (theException instanceof BaseServerResponseException) {
124                                        int statusCode = ((BaseServerResponseException) theException).getStatusCode();
125
126                                        // No stack traces for non-server internal errors
127                                        if (statusCode < 500) {
128                                                ourLog.warn("Failure during REST processing: {}", theException.toString());
129                                        } else {
130                                                ourLog.warn("Failure during REST processing: {}", theException);
131                                        }
132                                        
133                                        BaseServerResponseException baseServerResponseException = (BaseServerResponseException) theException;
134                                        populateDetails(ctx, theException, oo);
135                                        if (baseServerResponseException.getAdditionalMessages() != null) {
136                                                for (String next : baseServerResponseException.getAdditionalMessages()) {
137                                                        OperationOutcomeUtil.addIssue(ctx, oo, "error", next, null, PROCESSING);
138                                                }
139                                        }
140                                } else {
141                                        ourLog.error("Failure during REST processing: " + theException.toString(), theException);
142                                        populateDetails(ctx, theException, oo);
143                                }
144                        } catch (Exception e1) {
145                                ourLog.error("Failed to instantiate OperationOutcome resource instance", e1);
146                                throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1);
147                        }
148                } else {
149                        ourLog.error("Unknown error during processing", theException);
150                }
151                return oo;
152        }
153
154        private void populateDetails(FhirContext theCtx, Throwable theException, IBaseOperationOutcome theOo) {
155                if (myReturnStackTracesForExceptionTypes != null) {
156                        for (Class<?> next : myReturnStackTracesForExceptionTypes) {
157                                if (next.isAssignableFrom(theException.getClass())) {
158                                        String detailsValue = theException.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(theException);
159                                        OperationOutcomeUtil.addIssue(theCtx, theOo, "error", detailsValue, null, PROCESSING);
160                                        return;
161                                }
162                        }
163                }
164
165                OperationOutcomeUtil.addIssue(theCtx, theOo, "error", theException.getMessage(), null, PROCESSING);
166        }
167
168        /**
169         * If any server methods throw an exception which extends any of the given exception types, the exception stack trace will be returned to the user. This can be useful for helping to diagnose
170         * issues, but may not be desirable for production situations.
171         * 
172         * @param theExceptionTypes
173         *           The exception types for which to return the stack trace to the user.
174         * @return Returns an instance of this interceptor, to allow for easy method chaining.
175         */
176        public ExceptionHandlingInterceptor setReturnStackTracesForExceptionTypes(Class<?>... theExceptionTypes) {
177                myReturnStackTracesForExceptionTypes = theExceptionTypes;
178                return this;
179        }
180
181}