View Javadoc
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       *   &lt;exception
53       *       key="GlobalExceptionHandler.default"
54       *       type="java.lang.Exception"
55       *       path="/ErrorPage.jsp"&gt;
56       *       &lt;set-property key="INCLUDE_PATH" value="/error.jsp" /&gt;
57       *   &lt;/exception&gt;
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 &lt;exception&gt;
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       *   &lt;exception
81       *       key="GlobalExceptionHandler.default"
82       *       type="java.lang.Exception"
83       *       path="/ErrorPage.jsp"&gt;
84       *       &lt;set-property key="SILENT_IF_COMMITTED" value="true" /&gt;
85       *   &lt;/exception&gt;
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 }