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 }