| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package com.opensymphony.xwork2.interceptor; |
| |
| import com.opensymphony.xwork2.ActionInvocation; |
| import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.struts2.dispatcher.HttpParameters; |
| |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * <!-- START SNIPPET: description --> |
| * <p> |
| * This interceptor forms the core functionality of the exception handling feature. Exception handling allows you to map |
| * an exception to a result code, just as if the action returned a result code instead of throwing an unexpected |
| * exception. When an exception is encountered, it is wrapped with an {@link ExceptionHolder} and pushed on the stack, |
| * providing easy access to the exception from within your result. |
| * </p> |
| * |
| * <p> |
| * <b>Note:</b> While you can configure exception mapping in your configuration file at any point, the configuration |
| * will not have any effect if this interceptor is not in the interceptor stack for your actions. It is recommended that |
| * you make this interceptor the first interceptor on the stack, ensuring that it has full access to catch any |
| * exception, even those caused by other interceptors. |
| * </p> |
| * |
| * <!-- END SNIPPET: description --> |
| * |
| * <p><u>Interceptor parameters:</u></p> |
| * |
| * <!-- START SNIPPET: parameters --> |
| * |
| * <ul> |
| * |
| * <li>logEnabled (optional) - Should exceptions also be logged? (boolean true|false)</li> |
| * |
| * <li>logLevel (optional) - what log level should we use (<code>trace, debug, info, warn, error, fatal</code>)? - defaut is <code>debug</code></li> |
| * |
| * <li>logCategory (optional) - If provided we would use this category (eg. <code>com.mycompany.app</code>). |
| * Default is to use <code>com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor</code>.</li> |
| * |
| * </ul> |
| * |
| * <p> |
| * The parameters above enables us to log all thrown exceptions with stacktace in our own logfile, |
| * and present a friendly webpage (with no stacktrace) to the end user. |
| * </p> |
| * |
| * <!-- END SNIPPET: parameters --> |
| * |
| * <p><u>Extending the interceptor:</u></p> |
| * |
| * <!-- START SNIPPET: extending --> |
| * <p> |
| * If you want to add custom handling for publishing the Exception, you may override |
| * {@link #publishException(com.opensymphony.xwork2.ActionInvocation, ExceptionHolder)}. The default implementation |
| * pushes the given ExceptionHolder on value stack. A custom implementation could add additional logging etc. |
| * </p> |
| * <!-- END SNIPPET: extending --> |
| * |
| * <p><u>Example code:</u></p> |
| * |
| * <pre> |
| * <!-- START SNIPPET: example --> |
| * <xwork> |
| * <package name="default" extends="xwork-default"> |
| * <global-results> |
| * <result name="error" type="freemarker">error.ftl</result> |
| * </global-results> |
| * |
| * <global-exception-mappings> |
| * <exception-mapping exception="java.lang.Exception" result="error"/> |
| * </global-exception-mappings> |
| * |
| * <action name="test"> |
| * <interceptor-ref name="exception"/> |
| * <interceptor-ref name="basicStack"/> |
| * <exception-mapping exception="com.acme.CustomException" result="custom_error"/> |
| * <result name="custom_error">custom_error.ftl</result> |
| * <result name="success" type="freemarker">test.ftl</result> |
| * </action> |
| * </package> |
| * </xwork> |
| * <!-- END SNIPPET: example --> |
| * </pre> |
| * |
| * <p> |
| * This second example will also log the exceptions using our own category |
| * <code>com.mycompany.app.unhandled</code> at WARN level. |
| * </p> |
| * |
| * <pre> |
| * <!-- START SNIPPET: example2 --> |
| * <xwork> |
| * <package name="something" extends="xwork-default"> |
| * <interceptors> |
| * <interceptor-stack name="exceptionmappingStack"> |
| * <interceptor-ref name="exception"> |
| * <param name="logEnabled">true</param> |
| * <param name="logCategory">com.mycompany.app.unhandled</param> |
| * <param name="logLevel">WARN</param> |
| * </interceptor-ref> |
| * <interceptor-ref name="i18n"/> |
| * <interceptor-ref name="staticParams"/> |
| * <interceptor-ref name="params"/> |
| * <interceptor-ref name="validation"> |
| * <param name="excludeMethods">input,back,cancel,browse</param> |
| * </interceptor-ref> |
| * </interceptor-stack> |
| * </interceptors> |
| * |
| * <default-interceptor-ref name="exceptionmappingStack"/> |
| * |
| * <global-results> |
| * <result name="unhandledException">/unhandled-exception.jsp</result> |
| * </global-results> |
| * |
| * <global-exception-mappings> |
| * <exception-mapping exception="java.lang.Exception" result="unhandledException"/> |
| * </global-exception-mappings> |
| * |
| * <action name="exceptionDemo" class="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingAction"> |
| * <exception-mapping exception="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingException" |
| * result="damm"/> |
| * <result name="input">index.jsp</result> |
| * <result name="success">success.jsp</result> |
| * <result name="damm">damm.jsp</result> |
| * </action> |
| * |
| * </package> |
| * </xwork> |
| * <!-- END SNIPPET: example2 --> |
| * </pre> |
| * |
| * @author Matthew E. Porter (matthew dot porter at metissian dot com) |
| * @author Claus Ibsen |
| */ |
| public class ExceptionMappingInterceptor extends AbstractInterceptor { |
| |
| private static final Logger LOG = LogManager.getLogger(ExceptionMappingInterceptor.class); |
| |
| protected Logger categoryLogger; |
| protected boolean logEnabled = false; |
| protected String logCategory; |
| protected String logLevel; |
| |
| |
| public boolean isLogEnabled() { |
| return logEnabled; |
| } |
| |
| public void setLogEnabled(boolean logEnabled) { |
| this.logEnabled = logEnabled; |
| } |
| |
| public String getLogCategory() { |
| return logCategory; |
| } |
| |
| public void setLogCategory(String logCatgory) { |
| this.logCategory = logCatgory; |
| } |
| |
| public String getLogLevel() { |
| return logLevel; |
| } |
| |
| public void setLogLevel(String logLevel) { |
| this.logLevel = logLevel; |
| } |
| |
| @Override |
| public String intercept(ActionInvocation invocation) throws Exception { |
| String result; |
| |
| try { |
| result = invocation.invoke(); |
| } catch (Exception e) { |
| if (isLogEnabled()) { |
| handleLogging(e); |
| } |
| List<ExceptionMappingConfig> exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings(); |
| ExceptionMappingConfig mappingConfig = this.findMappingFromExceptions(exceptionMappings, e); |
| if (mappingConfig != null && mappingConfig.getResult()!=null) { |
| Map<String, String> mappingParams = mappingConfig.getParams(); |
| // create a mutable HashMap since some interceptors will remove parameters, and parameterMap is immutable |
| HttpParameters parameters = HttpParameters.create(mappingParams).build(); |
| invocation.getInvocationContext().withParameters(parameters); |
| result = mappingConfig.getResult(); |
| publishException(invocation, new ExceptionHolder(e)); |
| } else { |
| throw e; |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Handles the logging of the exception. |
| * |
| * @param e the exception to log. |
| */ |
| protected void handleLogging(Exception e) { |
| if (logCategory != null) { |
| if (categoryLogger == null) { |
| // init category logger |
| categoryLogger = LogManager.getLogger(logCategory); |
| } |
| doLog(categoryLogger, e); |
| } else { |
| doLog(LOG, e); |
| } |
| } |
| |
| /** |
| * Performs the actual logging. |
| * |
| * @param logger the provided logger to use. |
| * @param e the exception to log. |
| */ |
| protected void doLog(Logger logger, Exception e) { |
| if (logLevel == null) { |
| logger.debug(e.getMessage(), e); |
| return; |
| } |
| |
| if ("trace".equalsIgnoreCase(logLevel)) { |
| logger.trace(e.getMessage(), e); |
| } else if ("debug".equalsIgnoreCase(logLevel)) { |
| logger.debug(e.getMessage(), e); |
| } else if ("info".equalsIgnoreCase(logLevel)) { |
| logger.info(e.getMessage(), e); |
| } else if ("warn".equalsIgnoreCase(logLevel)) { |
| logger.warn(e.getMessage(), e); |
| } else if ("error".equalsIgnoreCase(logLevel)) { |
| logger.error(e.getMessage(), e); |
| } else if ("fatal".equalsIgnoreCase(logLevel)) { |
| logger.fatal(e.getMessage(), e); |
| } else { |
| throw new IllegalArgumentException("LogLevel [" + logLevel + "] is not supported"); |
| } |
| } |
| |
| /** |
| * Try to find appropriate {@link ExceptionMappingConfig} based on provided Throwable |
| * |
| * @param exceptionMappings list of defined exception mappings |
| * @param t caught exception |
| * @return appropriate mapping or null |
| */ |
| protected ExceptionMappingConfig findMappingFromExceptions(List<ExceptionMappingConfig> exceptionMappings, Throwable t) { |
| ExceptionMappingConfig config = null; |
| // Check for specific exception mappings. |
| if (exceptionMappings != null) { |
| int deepest = Integer.MAX_VALUE; |
| for (Object exceptionMapping : exceptionMappings) { |
| ExceptionMappingConfig exceptionMappingConfig = (ExceptionMappingConfig) exceptionMapping; |
| int depth = getDepth(exceptionMappingConfig.getExceptionClassName(), t); |
| if (depth >= 0 && depth < deepest) { |
| deepest = depth; |
| config = exceptionMappingConfig; |
| } |
| } |
| } |
| return config; |
| } |
| |
| /** |
| * Return the depth to the superclass matching. 0 means ex matches exactly. Returns -1 if there's no match. |
| * Otherwise, returns depth. Lowest depth wins. |
| * |
| * @param exceptionMapping the mapping classname |
| * @param t the cause |
| * @return the depth, if not found -1 is returned. |
| */ |
| public int getDepth(String exceptionMapping, Throwable t) { |
| return getDepth(exceptionMapping, t.getClass(), 0); |
| } |
| |
| private int getDepth(String exceptionMapping, Class exceptionClass, int depth) { |
| if (exceptionClass.getName().contains(exceptionMapping)) { |
| // Found it! |
| return depth; |
| } |
| // If we've gone as far as we can go and haven't found it... |
| if (exceptionClass.equals(Throwable.class)) { |
| return -1; |
| } |
| return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); |
| } |
| |
| /** |
| * Default implementation to handle ExceptionHolder publishing. Pushes given ExceptionHolder on the stack. |
| * Subclasses may override this to customize publishing. |
| * |
| * @param invocation The invocation to publish Exception for. |
| * @param exceptionHolder The exceptionHolder wrapping the Exception to publish. |
| */ |
| protected void publishException(ActionInvocation invocation, ExceptionHolder exceptionHolder) { |
| invocation.getStack().push(exceptionHolder); |
| } |
| |
| } |