1 /*
2 * $Id$
3 *
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21 package org.apache.struts.action;
22
23 import java.io.IOException;
24
25 import jakarta.servlet.RequestDispatcher;
26 import jakarta.servlet.ServletException;
27 import jakarta.servlet.http.HttpServletRequest;
28 import jakarta.servlet.http.HttpServletResponse;
29
30 import org.apache.struts.Globals;
31 import org.apache.struts.config.ExceptionConfig;
32 import org.apache.struts.util.MessageResources;
33 import org.apache.struts.util.ModuleException;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38 * <p>An <strong>ExceptionHandler</strong> is configured in the Struts
39 * configuration file to handle a specific type of exception thrown by an
40 * <code>Action.execute</code> method.</p>
41 *
42 * @since Struts 1.1
43 */
44 public class ExceptionHandler {
45 /**
46 * <p>The name of a configuration property which can be set to specify an
47 * alternative path which should be used when the HttpServletResponse has
48 * already been committed.</p> <p>To use this, in your
49 * <code>struts-config.xml</code> specify the exception handler like
50 * this:
51 * <pre>
52 * <exception
53 * key="GlobalExceptionHandler.default"
54 * type="java.lang.Exception"
55 * path="/ErrorPage.jsp">
56 * <set-property key="INCLUDE_PATH" value="/error.jsp" />
57 * </exception>
58 * </pre>
59 * </p> <p>You would want to use this when your normal ExceptionHandler
60 * path is a Tiles definition or otherwise unsuitable for use in an
61 * <code>include</code> context. If you do not use this, and you do not
62 * specify "SILENT_IF_COMMITTED" then the ExceptionHandler will attempt to
63 * forward to the same path which would be used in normal circumstances,
64 * specified using the "path" attribute in the <exception>
65 * element.</p>
66 *
67 * @since Struts 1.3
68 */
69 public static final String INCLUDE_PATH = "INCLUDE_PATH";
70
71 /**
72 * <p>The name of a configuration property which indicates that Struts
73 * should do nothing if the response has already been committed. This
74 * suppresses the default behavior, which is to use an "include" rather
75 * than a "forward" in this case in hopes of providing some meaningful
76 * information to the browser.</p> <p>To use this, in your
77 * <code>struts-config.xml</code> specify the exception handler like
78 * this:
79 * <pre>
80 * <exception
81 * key="GlobalExceptionHandler.default"
82 * type="java.lang.Exception"
83 * path="/ErrorPage.jsp">
84 * <set-property key="SILENT_IF_COMMITTED" value="true" />
85 * </exception>
86 * </pre>
87 * To be effective, this value must be defined to the literal String
88 * "true". If it is not defined or defined to any other value, the default
89 * behavior will be used. </p> <p>You only need to use this if you do not
90 * want error information displayed in the browser when Struts intercepts
91 * an exception after the response has been committed.</p>
92 *
93 * @since Struts 1.3
94 */
95 public static final String SILENT_IF_COMMITTED = "SILENT_IF_COMMITTED";
96
97 /**
98 * The {@code Log} instance for this class.
99 */
100 private final Logger log =
101 LoggerFactory.getLogger(ExceptionHandler.class);
102
103 /**
104 * <p>The message resources for this package.</p>
105 */
106 private static MessageResources messages =
107 MessageResources.getMessageResources(
108 "org.apache.struts.action.LocalStrings");
109
110 /**
111 * <p> Handle the Exception. Return the ActionForward instance (if any)
112 * returned by the called ExceptionHandler. </p>
113 *
114 * @param ex The exception to handle
115 * @param ae The ExceptionConfig corresponding to the exception
116 * @param mapping The ActionMapping we are processing
117 * @param formInstance The ActionForm we are processing
118 * @param request The servlet request we are processing
119 * @param response The servlet response we are creating
120 * @return The <code>ActionForward</code> instance (if any) returned by
121 * the called <code>ExceptionHandler</code>.
122 * @throws ServletException if a servlet exception occurs
123 * @since Struts 1.1
124 */
125 public ActionForward execute(Exception ex, ExceptionConfig ae,
126 ActionMapping mapping, ActionForm formInstance,
127 HttpServletRequest request, HttpServletResponse response)
128 throws ServletException {
129 log.debug("ExceptionHandler executing for exception ", ex);
130
131 ActionForward forward;
132 ActionMessage error;
133 String property;
134
135 // Build the forward from the exception mapping if it exists
136 // or from the form input
137 if (ae.getPath() != null) {
138 forward = new ActionForward(ae.getPath());
139 } else {
140 forward = mapping.getInputForward();
141 }
142
143 // Figure out the error
144 if (ex instanceof ModuleException) {
145 error = ((ModuleException) ex).getActionMessage();
146 property = ((ModuleException) ex).getProperty();
147 } else {
148 // STR-2924
149 if (ae.getKey() != null) {
150 error = new ActionMessage(ae.getKey(), ex.getMessage());
151 property = error.getKey();
152 } else {
153 error = null;
154 property = null;
155 }
156 }
157
158 this.logException(ex);
159
160 // Store the exception
161 request.setAttribute(Globals.EXCEPTION_KEY, ex);
162 this.storeException(request, property, error, forward, ae.getScope());
163
164 if (!response.isCommitted()) {
165 return forward;
166 }
167
168 log.debug("Response is already committed, so forwarding will not work."
169 + " Attempt alternate handling.");
170
171 if (!silent(ae)) {
172 handleCommittedResponse(ex, ae, mapping, formInstance, request,
173 response, forward);
174 } else {
175 log.warn("ExceptionHandler configured with {}"
176 + " and response is committed.", SILENT_IF_COMMITTED, ex);
177 }
178
179 return null;
180 }
181
182 /**
183 * <p>Attempt to give good information when the response has already been
184 * committed when the exception was thrown. This happens often when Tiles
185 * is used. Base implementation will see if the INCLUDE_PATH property has
186 * been set, or if not, it will attempt to use the same path to which
187 * control would have been forwarded.</p>
188 *
189 * @param ex The exception to handle
190 * @param config The ExceptionConfig we are processing
191 * @param mapping The ActionMapping we are processing
192 * @param formInstance The ActionForm we are processing
193 * @param request The servlet request we are processing
194 * @param response The servlet response we are creating
195 * @param actionForward The ActionForward we are processing
196 * @since Struts 1.3
197 */
198 protected void handleCommittedResponse(Exception ex,
199 ExceptionConfig config, ActionMapping mapping, ActionForm formInstance,
200 HttpServletRequest request, HttpServletResponse response,
201 ActionForward actionForward) {
202 String includePath = determineIncludePath(config, actionForward);
203
204 if (includePath != null) {
205 if (includePath.startsWith("/")) {
206 log.debug("response committed, "
207 + "but attempt to include results "
208 + "of actionForward path");
209
210 RequestDispatcher requestDispatcher =
211 request.getRequestDispatcher(includePath);
212
213 try {
214 requestDispatcher.include(request, response);
215
216 return;
217 } catch (IOException e) {
218 log.error("IOException when trying to include "
219 + "the error page path {}", includePath, e);
220 } catch (ServletException e) {
221 log.error("ServletException when trying to include "
222 + "the error page path {}", includePath, e);
223 }
224 } else {
225 log.warn("Suspicious includePath doesn't seem likely to work, "
226 + "so skipping it: {}"
227 + "; expected path to start with '/'", includePath);
228 }
229 }
230
231 log.debug("Include not available or failed; "
232 + "try writing to the response directly.");
233
234 try {
235 response.getWriter().println("Unexpected error: " + ex);
236 response.getWriter().println("<!-- ");
237 ex.printStackTrace(response.getWriter());
238 response.getWriter().println("-->");
239 } catch (IOException e) {
240 log.error("Error giving minimal information about exception", e);
241 log.error("Original exception: ", ex);
242 }
243 }
244
245 /**
246 * <p>Return a path to which an include should be attempted in the case
247 * when the response was committed before the <code>ExceptionHandler</code>
248 * was invoked. </p> <p>If the <code>ExceptionConfig</code> has the
249 * property <code>INCLUDE_PATH</code> defined, then the value of that
250 * property will be returned. Otherwise, the ActionForward path is
251 * returned. </p>
252 *
253 * @param config Configuration element
254 * @param actionForward Forward to use on error
255 * @return Path of resource to include
256 * @since Struts 1.3
257 */
258 protected String determineIncludePath(ExceptionConfig config,
259 ActionForward actionForward) {
260 String includePath = config.getProperty("INCLUDE_PATH");
261
262 if (includePath == null) {
263 includePath = actionForward.getPath();
264 }
265
266 return includePath;
267 }
268
269 /**
270 * <p>Logs the <code>Exception</code> using commons-logging.</p>
271 *
272 * @param e The Exception to log.
273 * @since Struts 1.2
274 */
275 protected void logException(Exception e) {
276 log.atDebug()
277 .setMessage(() -> messages.getMessage("exception.LOG"))
278 .setCause(e).log();
279 }
280
281 /**
282 * <p>Default implementation for handling an <code>ActionMessage</code>
283 * generated from an <code>Exception</code> during <code>Action</code>
284 * delegation. The default implementation is to set an attribute of the
285 * request or session, as defined by the scope provided (the scope from
286 * the exception mapping), if <code>error</code> is not <code>null</code>.
287 * Otherwise, an <code>ActionMessages</code> instance is created, the error
288 * is added to the collection and the collection is set
289 * under the <code>Globals.ERROR_KEY</code>.</p>
290 *
291 * @param request The request we are handling
292 * @param property The property name to use for this error
293 * @param error The error generated from the exception mapping
294 * @param forward The forward generated from the input path (from the
295 * form or exception mapping)
296 * @param scope The scope of the exception mapping.
297 * @since Struts 1.2
298 */
299 protected void storeException(HttpServletRequest request, String property,
300 ActionMessage error, ActionForward forward, String scope) {
301
302 if (error != null) {
303 ActionMessages errors = new ActionMessages();
304 errors.add(property, error);
305
306 if ("request".equals(scope)) {
307 request.setAttribute(Globals.ERROR_KEY, errors);
308 } else {
309 request.getSession().setAttribute(Globals.ERROR_KEY, errors);
310 }
311 }
312 }
313
314 /**
315 * <p>Indicate whether this Handler has been configured to be silent. In
316 * the base implementation, this is done by specifying the value
317 * <code>"true"</code> for the property "SILENT_IF_COMMITTED" in the
318 * ExceptionConfig.</p>
319 *
320 * @param config The ExceptionConfiguration we are handling
321 * @return True if Handler is silent
322 * @since Struts 1.3
323 */
324 private boolean silent(ExceptionConfig config) {
325 return "true".equals(config.getProperty(SILENT_IF_COMMITTED));
326 }
327 }