1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.interceptor;
6
7 import com.opensymphony.xwork2.ActionInvocation;
8 import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
9 import com.opensymphony.xwork2.util.logging.Logger;
10 import com.opensymphony.xwork2.util.logging.LoggerFactory;
11
12 import java.util.List;
13
14 /**
15 * <!-- START SNIPPET: description -->
16 *
17 * This interceptor forms the core functionality of the exception handling feature. Exception handling allows you to map
18 * an exception to a result code, just as if the action returned a result code instead of throwing an unexpected
19 * exception. When an exception is encountered, it is wrapped with an {@link ExceptionHolder} and pushed on the stack,
20 * providing easy access to the exception from within your result.
21 *
22 * <b>Note:</b> While you can configure exception mapping in your configuration file at any point, the configuration
23 * will not have any effect if this interceptor is not in the interceptor stack for your actions. It is recommended that
24 * you make this interceptor the first interceptor on the stack, ensuring that it has full access to catch any
25 * exception, even those caused by other interceptors.
26 *
27 * <!-- END SNIPPET: description -->
28 *
29 * <p/> <u>Interceptor parameters:</u>
30 *
31 * <!-- START SNIPPET: parameters -->
32 *
33 * <ul>
34 *
35 * <li>logEnabled (optional) - Should exceptions also be logged? (boolean true|false)</li>
36 *
37 * <li>logLevel (optional) - what log level should we use (<code>trace, debug, info, warn, error, fatal</code>)? - defaut is <code>debug</code></li>
38 *
39 * <li>logCategory (optional) - If provided we would use this category (eg. <code>com.mycompany.app</code>).
40 * Default is to use <code>com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor</code>.</li>
41 *
42 * </ul>
43 *
44 * The parameters above enables us to log all thrown exceptions with stacktace in our own logfile,
45 * and present a friendly webpage (with no stacktrace) to the end user.
46 *
47 * <!-- END SNIPPET: parameters -->
48 *
49 * <p/> <u>Extending the interceptor:</u>
50 *
51 * <p/>
52 *
53 * <!-- START SNIPPET: extending -->
54 *
55 * If you want to add custom handling for publishing the Exception, you may override
56 * {@link #publishException(com.opensymphony.xwork2.ActionInvocation, ExceptionHolder)}. The default implementation
57 * pushes the given ExceptionHolder on value stack. A custom implementation could add additional logging etc.
58 *
59 * <!-- END SNIPPET: extending -->
60 *
61 * <p/> <u>Example code:</u>
62 *
63 * <pre>
64 * <!-- START SNIPPET: example -->
65 * <xwork>
66 * <package name="default" extends="xwork-default">
67 * <global-results>
68 * <result name="error" type="freemarker">error.ftl</result>
69 * </global-results>
70 *
71 * <global-exception-mappings>
72 * <exception-mapping exception="java.lang.Exception" result="error"/>
73 * </global-exception-mappings>
74 *
75 * <action name="test">
76 * <interceptor-ref name="exception"/>
77 * <interceptor-ref name="basicStack"/>
78 * <exception-mapping exception="com.acme.CustomException" result="custom_error"/>
79 * <result name="custom_error">custom_error.ftl</result>
80 * <result name="success" type="freemarker">test.ftl</result>
81 * </action>
82 * </package>
83 * </xwork>
84 * <!-- END SNIPPET: example -->
85 * </pre>
86 *
87 * <p/>
88 * This second example will also log the exceptions using our own category
89 * <code>com.mycompany.app.unhandled<code> at WARN level.
90 *
91 * <pre>
92 * <!-- START SNIPPET: example2 -->
93 * <xwork>
94 * <package name="something" extends="xwork-default">
95 * <interceptors>
96 * <interceptor-stack name="exceptionmappingStack">
97 * <interceptor-ref name="exception">
98 * <param name="logEnabled">true</param>
99 * <param name="logCategory">com.mycompany.app.unhandled</param>
100 * <param name="logLevel">WARN</param>
101 * </interceptor-ref>
102 * <interceptor-ref name="i18n"/>
103 * <interceptor-ref name="staticParams"/>
104 * <interceptor-ref name="params"/>
105 * <interceptor-ref name="validation">
106 * <param name="excludeMethods">input,back,cancel,browse</param>
107 * </interceptor-ref>
108 * </interceptor-stack>
109 * </interceptors>
110 *
111 * <default-interceptor-ref name="exceptionmappingStack"/>
112 *
113 * <global-results>
114 * <result name="unhandledException">/unhandled-exception.jsp</result>
115 * </global-results>
116 *
117 * <global-exception-mappings>
118 * <exception-mapping exception="java.lang.Exception" result="unhandledException"/>
119 * </global-exception-mappings>
120 *
121 * <action name="exceptionDemo" class="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingAction">
122 * <exception-mapping exception="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingException"
123 * result="damm"/>
124 * <result name="input">index.jsp</result>
125 * <result name="success">success.jsp</result>
126 * <result name="damm">damm.jsp</result>
127 * </action>
128 *
129 * </package>
130 * </xwork>
131 * <!-- END SNIPPET: example2 -->
132 * </pre>
133 *
134 * @author Matthew E. Porter (matthew dot porter at metissian dot com)
135 * @author Claus Ibsen
136 */
137 public class ExceptionMappingInterceptor extends AbstractInterceptor {
138
139 protected static final Logger LOG = LoggerFactory.getLogger(ExceptionMappingInterceptor.class);
140
141 protected Logger categoryLogger;
142 protected boolean logEnabled = false;
143 protected String logCategory;
144 protected String logLevel;
145
146
147 public boolean isLogEnabled() {
148 return logEnabled;
149 }
150
151 public void setLogEnabled(boolean logEnabled) {
152 this.logEnabled = logEnabled;
153 }
154
155 public String getLogCategory() {
156 return logCategory;
157 }
158
159 public void setLogCategory(String logCatgory) {
160 this.logCategory = logCatgory;
161 }
162
163 public String getLogLevel() {
164 return logLevel;
165 }
166
167 public void setLogLevel(String logLevel) {
168 this.logLevel = logLevel;
169 }
170
171 @Override
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<ExceptionMappingConfig> 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 protected String findResultFromExceptions(List<ExceptionMappingConfig> 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 (Object exceptionMapping : exceptionMappings) {
247 ExceptionMappingConfig exceptionMappingConfig = (ExceptionMappingConfig) exceptionMapping;
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().contains(exceptionMapping)) {
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 }