1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.interceptor;
6
7 import java.util.Iterator;
8 import java.util.List;
9
10 import com.opensymphony.xwork2.ActionInvocation;
11 import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
12 import com.opensymphony.xwork2.util.logging.Logger;
13 import com.opensymphony.xwork2.util.logging.LoggerFactory;
14
15 /**
16 * <!-- START SNIPPET: description -->
17 *
18 * This interceptor forms the core functionality of the exception handling feature. Exception handling allows you to map
19 * an exception to a result code, just as if the action returned a result code instead of throwing an unexpected
20 * exception. When an exception is encountered, it is wrapped with an {@link ExceptionHolder} and pushed on the stack,
21 * providing easy access to the exception from within your result.
22 *
23 * <b>Note:</b> While you can configure exception mapping in your configuration file at any point, the configuration
24 * will not have any effect if this interceptor is not in the interceptor stack for your actions. It is recommended that
25 * you make this interceptor the first interceptor on the stack, ensuring that it has full access to catch any
26 * exception, even those caused by other interceptors.
27 *
28 * <!-- END SNIPPET: description -->
29 *
30 * <p/> <u>Interceptor parameters:</u>
31 *
32 * <!-- START SNIPPET: parameters -->
33 *
34 * <ul>
35 *
36 * <li>logEnabled (optional) - Should exceptions also be logged? (boolean true|false)</li>
37 *
38 * <li>logLevel (optional) - what log level should we use (<code>trace, debug, info, warn, error, fatal</code>)? - defaut is <code>debug</code></li>
39 *
40 * <li>logCategory (optional) - If provided we would use this category (eg. <code>com.mycompany.app</code>).
41 * Default is to use <code>com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor</code>.</li>
42 *
43 * </ul>
44 *
45 * The parameters above enables us to log all thrown exceptions with stacktace in our own logfile,
46 * and present a friendly webpage (with no stacktrace) to the end user.
47 *
48 * <!-- END SNIPPET: parameters -->
49 *
50 * <p/> <u>Extending the interceptor:</u>
51 *
52 * <p/>
53 *
54 * <!-- START SNIPPET: extending -->
55 *
56 * If you want to add custom handling for publishing the Exception, you may override
57 * {@link #publishException(com.opensymphony.xwork2.ActionInvocation, ExceptionHolder)}. The default implementation
58 * pushes the given ExceptionHolder on value stack. A custom implementation could add additional logging etc.
59 *
60 * <!-- END SNIPPET: extending -->
61 *
62 * <p/> <u>Example code:</u>
63 *
64 * <pre>
65 * <!-- START SNIPPET: example -->
66 * <xwork>
67 * <package name="default" extends="xwork-default">
68 * <global-results>
69 * <result name="error" type="freemarker">error.ftl</result>
70 * </global-results>
71 *
72 * <global-exception-mappings>
73 * <exception-mapping exception="java.lang.Exception" result="error"/>
74 * </global-exception-mappings>
75 *
76 * <action name="test">
77 * <interceptor-ref name="exception"/>
78 * <interceptor-ref name="basicStack"/>
79 * <exception-mapping exception="com.acme.CustomException" result="custom_error"/>
80 * <result name="custom_error">custom_error.ftl</result>
81 * <result name="success" type="freemarker">test.ftl</result>
82 * </action>
83 * </package>
84 * </xwork>
85 * <!-- END SNIPPET: example -->
86 * </pre>
87 *
88 * <p/>
89 * This second example will also log the exceptions using our own category
90 * <code>com.mycompany.app.unhandled<code> at WARN level.
91 *
92 * <pre>
93 * <!-- START SNIPPET: example2 -->
94 * <xwork>
95 * <package name="something" extends="xwork-default">
96 * <interceptors>
97 * <interceptor-stack name="exceptionmappingStack">
98 * <interceptor-ref name="exception">
99 * <param name="logEnabled">true</param>
100 * <param name="logCategory">com.mycompany.app.unhandled</param>
101 * <param name="logLevel">WARN</param>
102 * </interceptor-ref>
103 * <interceptor-ref name="i18n"/>
104 * <interceptor-ref name="staticParams"/>
105 * <interceptor-ref name="params"/>
106 * <interceptor-ref name="validation">
107 * <param name="excludeMethods">input,back,cancel,browse</param>
108 * </interceptor-ref>
109 * </interceptor-stack>
110 * </interceptors>
111 *
112 * <default-interceptor-ref name="exceptionmappingStack"/>
113 *
114 * <global-results>
115 * <result name="unhandledException">/unhandled-exception.jsp</result>
116 * </global-results>
117 *
118 * <global-exception-mappings>
119 * <exception-mapping exception="java.lang.Exception" result="unhandledException"/>
120 * </global-exception-mappings>
121 *
122 * <action name="exceptionDemo" class="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingAction">
123 * <exception-mapping exception="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingException"
124 * result="damm"/>
125 * <result name="input">index.jsp</result>
126 * <result name="success">success.jsp</result>
127 * <result name="damm">damm.jsp</result>
128 * </action>
129 *
130 * </package>
131 * </xwork>
132 * <!-- END SNIPPET: example2 -->
133 * </pre>
134 *
135 * @author Matthew E. Porter (matthew dot porter at metissian dot com)
136 * @author Claus Ibsen
137 */
138 public class ExceptionMappingInterceptor extends AbstractInterceptor {
139
140 protected static final Logger LOG = LoggerFactory.getLogger(ExceptionMappingInterceptor.class);
141
142 protected Logger categoryLogger;
143 protected boolean logEnabled = false;
144 protected String logCategory;
145 protected String logLevel;
146
147
148 public boolean isLogEnabled() {
149 return logEnabled;
150 }
151
152 public void setLogEnabled(boolean logEnabled) {
153 this.logEnabled = logEnabled;
154 }
155
156 public String getLogCategory() {
157 return logCategory;
158 }
159
160 public void setLogCategory(String logCatgory) {
161 this.logCategory = logCatgory;
162 }
163
164 public String getLogLevel() {
165 return logLevel;
166 }
167
168 public void setLogLevel(String logLevel) {
169 this.logLevel = logLevel;
170 }
171
172 public String intercept(ActionInvocation invocation) throws Exception {
173 String result;
174
175 try {
176 result = invocation.invoke();
177 } catch (Exception e) {
178 if (isLogEnabled()) {
179 handleLogging(e);
180 }
181 List exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings();
182 String mappedResult = this.findResultFromExceptions(exceptionMappings, e);
183 if (mappedResult != null) {
184 result = mappedResult;
185 publishException(invocation, new ExceptionHolder(e));
186 } else {
187 throw e;
188 }
189 }
190
191 return result;
192 }
193
194 /**
195 * Handles the logging of the exception.
196 *
197 * @param e the exception to log.
198 */
199 protected void handleLogging(Exception e) {
200 if (logCategory != null) {
201 if (categoryLogger == null) {
202 // init category logger
203 categoryLogger = LoggerFactory.getLogger(logCategory);
204 }
205 doLog(categoryLogger, e);
206 } else {
207 doLog(LOG, e);
208 }
209 }
210
211 /**
212 * Performs the actual logging.
213 *
214 * @param logger the provided logger to use.
215 * @param e the exception to log.
216 */
217 protected void doLog(Logger logger, Exception e) {
218 if (logLevel == null) {
219 logger.debug(e.getMessage(), e);
220 return;
221 }
222
223 if ("trace".equalsIgnoreCase(logLevel)) {
224 logger.trace(e.getMessage(), e);
225 } else if ("debug".equalsIgnoreCase(logLevel)) {
226 logger.debug(e.getMessage(), e);
227 } else if ("info".equalsIgnoreCase(logLevel)) {
228 logger.info(e.getMessage(), e);
229 } else if ("warn".equalsIgnoreCase(logLevel)) {
230 logger.warn(e.getMessage(), e);
231 } else if ("error".equalsIgnoreCase(logLevel)) {
232 logger.error(e.getMessage(), e);
233 } else if ("fatal".equalsIgnoreCase(logLevel)) {
234 logger.fatal(e.getMessage(), e);
235 } else {
236 throw new IllegalArgumentException("LogLevel [" + logLevel + "] is not supported");
237 }
238 }
239
240 private String findResultFromExceptions(List exceptionMappings, Throwable t) {
241 String result = null;
242
243 // Check for specific exception mappings.
244 if (exceptionMappings != null) {
245 int deepest = Integer.MAX_VALUE;
246 for (Iterator iter = exceptionMappings.iterator(); iter.hasNext();) {
247 ExceptionMappingConfig exceptionMappingConfig = (ExceptionMappingConfig) iter.next();
248 int depth = getDepth(exceptionMappingConfig.getExceptionClassName(), t);
249 if (depth >= 0 && depth < deepest) {
250 deepest = depth;
251 result = exceptionMappingConfig.getResult();
252 }
253 }
254 }
255
256 return result;
257 }
258
259 /**
260 * Return the depth to the superclass matching. 0 means ex matches exactly. Returns -1 if there's no match.
261 * Otherwise, returns depth. Lowest depth wins.
262 *
263 * @param exceptionMapping the mapping classname
264 * @param t the cause
265 * @return the depth, if not found -1 is returned.
266 */
267 public int getDepth(String exceptionMapping, Throwable t) {
268 return getDepth(exceptionMapping, t.getClass(), 0);
269 }
270
271 private int getDepth(String exceptionMapping, Class exceptionClass, int depth) {
272 if (exceptionClass.getName().indexOf(exceptionMapping) != -1) {
273 // Found it!
274 return depth;
275 }
276 // If we've gone as far as we can go and haven't found it...
277 if (exceptionClass.equals(Throwable.class)) {
278 return -1;
279 }
280 return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
281 }
282
283 /**
284 * Default implementation to handle ExceptionHolder publishing. Pushes given ExceptionHolder on the stack.
285 * Subclasses may override this to customize publishing.
286 *
287 * @param invocation The invocation to publish Exception for.
288 * @param exceptionHolder The exceptionHolder wrapping the Exception to publish.
289 */
290 protected void publishException(ActionInvocation invocation, ExceptionHolder exceptionHolder) {
291 invocation.getStack().push(exceptionHolder);
292 }
293 }