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