001/*
002 * Copyright 2002-2015 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.portlet.handler;
018
019import java.util.Enumeration;
020import java.util.Properties;
021import javax.portlet.MimeResponse;
022import javax.portlet.PortletRequest;
023
024import org.springframework.web.portlet.ModelAndView;
025
026/**
027 * {@link org.springframework.web.portlet.HandlerExceptionResolver} implementation
028 * that allows for mapping exception class names to view names, either for a
029 * set of given handlers or for all handlers in the DispatcherPortlet.
030 *
031 * <p>Error views are analogous to error page JSPs, but can be used with any
032 * kind of exception including any checked one, with fine-granular mappings for
033 * specific handlers.
034 *
035 * @author Juergen Hoeller
036 * @author John A. Lewis
037 * @author Arjen Poutsma
038 * @since 2.0
039 */
040public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver {
041
042        /**
043         * The default name of the exception attribute: "exception".
044         */
045        public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception";
046
047        private Properties exceptionMappings;
048
049        private String defaultErrorView;
050
051        private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE;
052
053        /**
054         * Set the mappings between exception class names and error view names.
055         * The exception class name can be a substring, with no wildcard support
056         * at present. A value of "PortletException" would match
057         * {@code javax.portet.PortletException} and subclasses, for example.
058         * <p><b>NB:</b> Consider carefully how specific the pattern is, and whether
059         * to include package information (which isn't mandatory). For example,
060         * "Exception" will match nearly anything, and will probably hide other rules.
061         * "java.lang.Exception" would be correct if "Exception" was meant to define
062         * a rule for all checked exceptions. With more unusual exception names such
063         * as "BaseBusinessException" there's no need to use a FQN.
064         * <p>Follows the same matching algorithm as RuleBasedTransactionAttribute
065         * and RollbackRuleAttribute.
066         * @param mappings exception patterns (can also be fully qualified class names)
067         * as keys, and error view names as values
068         * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
069         * @see org.springframework.transaction.interceptor.RollbackRuleAttribute
070         */
071        public void setExceptionMappings(Properties mappings) {
072                this.exceptionMappings = mappings;
073        }
074
075        /**
076         * Set the name of the default error view.
077         * This view will be returned if no specific mapping was found.
078         * <p>Default is none.
079         */
080        public void setDefaultErrorView(String defaultErrorView) {
081                this.defaultErrorView = defaultErrorView;
082        }
083
084        /**
085         * Set the name of the model attribute as which the exception should
086         * be exposed. Default is "exception".
087         * @see #DEFAULT_EXCEPTION_ATTRIBUTE
088         */
089        public void setExceptionAttribute(String exceptionAttribute) {
090                this.exceptionAttribute = exceptionAttribute;
091        }
092
093        /**
094         * Actually resolve the given exception that got thrown during on handler execution,
095         * returning a ModelAndView that represents a specific error page if appropriate.
096         * @param request current portlet request
097         * @param response current portlet response
098         * @param handler the executed handler, or null if none chosen at the time of
099         * the exception (for example, if multipart resolution failed)
100         * @param ex the exception that got thrown during handler execution
101         * @return a corresponding ModelAndView to forward to, or null for default processing
102         */
103        @Override
104        protected ModelAndView doResolveException(
105                        PortletRequest request, MimeResponse response, Object handler, Exception ex) {
106
107                // Log exception, both at debug log level and at warn level, if desired.
108                if (logger.isDebugEnabled()) {
109                        logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
110                }
111                logException(ex, request);
112
113                // Expose ModelAndView for chosen error view.
114                String viewName = determineViewName(ex, request);
115                if (viewName != null) {
116                        return getModelAndView(viewName, ex, request);
117                }
118                else {
119                        return null;
120                }
121        }
122
123        /**
124         * Determine the view name for the given exception, searching the
125         * {@link #setExceptionMappings "exceptionMappings"}, using the
126         * {@link #setDefaultErrorView "defaultErrorView"} as fallback.
127         * @param ex the exception that got thrown during handler execution
128         * @param request current portlet request (useful for obtaining metadata)
129         * @return the resolved view name, or {@code null} if none found
130         */
131        protected String determineViewName(Exception ex, PortletRequest request) {
132                String viewName = null;
133                // Check for specific exception mappings.
134                if (this.exceptionMappings != null) {
135                        viewName = findMatchingViewName(this.exceptionMappings, ex);
136                }
137                // Return default error view else, if defined.
138                if (viewName == null && this.defaultErrorView != null) {
139                        if (logger.isDebugEnabled()) {
140                                logger.debug("Resolving to default view '" + this.defaultErrorView +
141                                                "' for exception of type [" + ex.getClass().getName() + "]");
142                        }
143                        viewName = this.defaultErrorView;
144                }
145                return viewName;
146        }
147
148        /**
149         * Find a matching view name in the given exception mappings
150         * @param exceptionMappings mappings between exception class names and error view names
151         * @param ex the exception that got thrown during handler execution
152         * @return the view name, or {@code null} if none found
153         * @see #setExceptionMappings
154         */
155        protected String findMatchingViewName(Properties exceptionMappings, Exception ex) {
156                String viewName = null;
157                String dominantMapping = null;
158                int deepest = Integer.MAX_VALUE;
159                for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) {
160                        String exceptionMapping = (String) names.nextElement();
161                        int depth = getDepth(exceptionMapping, ex);
162                        if (depth >= 0 && (depth < deepest || (depth == deepest &&
163                                        dominantMapping != null && exceptionMapping.length() > dominantMapping.length()))) {
164                                deepest = depth;
165                                dominantMapping = exceptionMapping;
166                                viewName = exceptionMappings.getProperty(exceptionMapping);
167                        }
168                }
169                if (viewName != null && logger.isDebugEnabled()) {
170                        logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() +
171                                        "], based on exception mapping [" + dominantMapping + "]");
172                }
173                return viewName;
174        }
175
176        /**
177         * Return the depth to the superclass matching.
178         * <p>0 means ex matches exactly. Returns -1 if there's no match.
179         * Otherwise, returns depth. Lowest depth wins.
180         * <p>Follows the same algorithm as
181         * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}.
182         */
183        protected int getDepth(String exceptionMapping, Exception ex) {
184                return getDepth(exceptionMapping, ex.getClass(), 0);
185        }
186
187        private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) {
188                if (exceptionClass.getName().contains(exceptionMapping)) {
189                        // Found it!
190                        return depth;
191                }
192                // If we've gone as far as we can go and haven't found it...
193                if (exceptionClass == Throwable.class) {
194                        return -1;
195                }
196                return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
197        }
198
199
200        /**
201         * Return a ModelAndView for the given request, view name and exception.
202         * Default implementation delegates to {@code getModelAndView(viewName, ex)}.
203         * @param viewName the name of the error view
204         * @param ex the exception that got thrown during handler execution
205         * @param request current portlet request (useful for obtaining metadata)
206         * @return the ModelAndView instance
207         * @see #getModelAndView(String, Exception)
208         */
209        protected ModelAndView getModelAndView(String viewName, Exception ex, PortletRequest request) {
210                return getModelAndView(viewName, ex);
211        }
212
213        /**
214         * Return a ModelAndView for the given view name and exception.
215         * Default implementation adds the specified exception attribute.
216         * Can be overridden in subclasses.
217         * @param viewName the name of the error view
218         * @param ex the exception that got thrown during handler execution
219         * @return the ModelAndView instance
220         * @see #setExceptionAttribute
221         */
222        protected ModelAndView getModelAndView(String viewName, Exception ex) {
223                ModelAndView mv = new ModelAndView(viewName);
224                if (this.exceptionAttribute != null) {
225                        if (logger.isDebugEnabled()) {
226                                logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'");
227                        }
228                        mv.addObject(this.exceptionAttribute, ex);
229                }
230                return mv;
231        }
232
233}