001// Copyright 2006-2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services; 016 017import org.apache.tapestry5.ContextAwareException; 018import org.apache.tapestry5.ExceptionHandlerAssistant; 019import org.apache.tapestry5.Link; 020import org.apache.tapestry5.SymbolConstants; 021import org.apache.tapestry5.internal.InternalConstants; 022import org.apache.tapestry5.internal.structure.Page; 023import org.apache.tapestry5.ioc.ServiceResources; 024import org.apache.tapestry5.ioc.annotations.Symbol; 025import org.apache.tapestry5.ioc.internal.OperationException; 026import org.apache.tapestry5.ioc.util.ExceptionUtils; 027import org.apache.tapestry5.json.JSONObject; 028import org.apache.tapestry5.runtime.ComponentEventException; 029import org.apache.tapestry5.services.*; 030import org.slf4j.Logger; 031 032import javax.servlet.http.HttpServletResponse; 033import java.io.IOException; 034import java.io.OutputStream; 035import java.net.URLEncoder; 036import java.util.Arrays; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040import java.util.Map.Entry; 041 042/** 043 * Default implementation of {@link RequestExceptionHandler} that displays the standard ExceptionReport page. Similarly to the 044 * servlet spec's standard error handling, the default exception handler allows configuring handlers for specific types of 045 * exceptions. The error-page/exception-type configuration in web.xml does not work in Tapestry application as errors are 046 * wrapped in Tapestry's exception types (see {@link OperationException} and {@link ComponentEventException} ). 047 * 048 * Configurations are flexible. You can either contribute a {@link ExceptionHandlerAssistant} to use arbitrary complex logic 049 * for error handling or a page class to render for the specific exception. Additionally, exceptions can carry context for the 050 * error page. Exception context is formed either from the name of Exception (e.g. SmtpNotRespondingException -> ServiceFailure mapping 051 * would render a page with URL /servicefailure/smtpnotresponding) or they can implement {@link ContextAwareException} interface. 052 * 053 * If no configured exception type is found, the default exception page {@link SymbolConstants#EXCEPTION_REPORT_PAGE} is rendered. 054 * This fallback exception page must implement the {@link ExceptionReporter} interface. 055 */ 056public class DefaultRequestExceptionHandler implements RequestExceptionHandler 057{ 058 private final RequestPageCache pageCache; 059 060 private final PageResponseRenderer renderer; 061 062 private final Logger logger; 063 064 private final String pageName; 065 066 private final Request request; 067 068 private final Response response; 069 070 private final ComponentClassResolver componentClassResolver; 071 072 private final LinkSource linkSource; 073 074 // should be Class<? extends Throwable>, Object but it's not allowed to configure subtypes 075 private final Map<Class, Object> configuration; 076 077 /** 078 * @param pageCache 079 * @param renderer 080 * @param logger 081 * @param pageName 082 * @param request 083 * @param response 084 * @param componentClassResolver 085 * @param linkSource 086 * @param serviceResources 087 * @param configuration A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class 088 * in which case the instance is autobuilt. 089 */ 090 @SuppressWarnings("rawtypes") 091 public DefaultRequestExceptionHandler(RequestPageCache pageCache, PageResponseRenderer renderer, Logger logger, 092 093 @Symbol(SymbolConstants.EXCEPTION_REPORT_PAGE) 094 String pageName, 095 096 Request request, Response response, ComponentClassResolver componentClassResolver, 097 LinkSource linkSource, ServiceResources serviceResources, Map<Class, Object> configuration) 098 { 099 this.pageCache = pageCache; 100 this.renderer = renderer; 101 this.logger = logger; 102 this.pageName = pageName; 103 this.request = request; 104 this.response = response; 105 this.componentClassResolver = componentClassResolver; 106 this.linkSource = linkSource; 107 108 Map<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant> handlerAssistants = new HashMap<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant>(); 109 110 for (Entry<Class, Object> entry : configuration.entrySet()) 111 { 112 if (!Throwable.class.isAssignableFrom(entry.getKey())) 113 throw new IllegalArgumentException(Throwable.class.getName() + " is the only allowable key type but " + entry.getKey().getName() 114 + " was contributed"); 115 116 if (entry.getValue() instanceof Class && ExceptionHandlerAssistant.class.isAssignableFrom((Class) entry.getValue())) 117 { 118 @SuppressWarnings("unchecked") 119 Class<ExceptionHandlerAssistant> handlerType = (Class<ExceptionHandlerAssistant>) entry.getValue(); 120 ExceptionHandlerAssistant assistant = handlerAssistants.get(handlerType); 121 if (assistant == null) 122 { 123 assistant = (ExceptionHandlerAssistant) serviceResources.autobuild(handlerType); 124 handlerAssistants.put(handlerType, assistant); 125 } 126 entry.setValue(assistant); 127 } 128 } 129 this.configuration = configuration; 130 } 131 132 /** 133 * Handles the exception thrown at some point the request was being processed 134 * 135 * First checks if there was a specific exception handler/page configured for this exception type, it's super class or super-super class. 136 * Renders the default exception page if none was configured. 137 * 138 * @param exception The exception that was thrown 139 * 140 */ 141 @SuppressWarnings({ "rawtypes", "unchecked" }) 142 public void handleRequestException(Throwable exception) throws IOException 143 { 144 // skip handling of known exceptions if there are none configured 145 if (configuration.isEmpty()) 146 { 147 renderException(exception); 148 return; 149 } 150 151 Throwable cause = exception; 152 153 // Depending on where the error was thrown, there could be several levels of wrappers.. 154 // For exceptions in component operations, it's OperationException -> ComponentEventException -> <Target>Exception 155 156 // Throw away the wrapped exceptions first 157 while (cause instanceof OperationException || cause instanceof ComponentEventException) 158 { 159 if (cause.getCause() == null) break; 160 cause = cause.getCause(); 161 } 162 163 Class<?> causeClass = cause.getClass(); 164 if (!configuration.containsKey(causeClass)) 165 { 166 // try at most two level of superclasses before delegating back to the default exception handler 167 causeClass = causeClass.getSuperclass(); 168 if (causeClass == null || !configuration.containsKey(causeClass)) 169 { 170 causeClass = causeClass.getSuperclass(); 171 if (causeClass == null || !configuration.containsKey(causeClass)) 172 { 173 renderException(exception); 174 return; 175 } 176 } 177 } 178 179 Object[] exceptionContext = formExceptionContext(cause); 180 Object value = configuration.get(causeClass); 181 Object page = null; 182 ExceptionHandlerAssistant assistant = null; 183 if (value instanceof ExceptionHandlerAssistant) 184 { 185 assistant = (ExceptionHandlerAssistant) value; 186 // in case the assistant changes the context 187 List context = Arrays.asList(exceptionContext); 188 page = assistant.handleRequestException(exception, context); 189 exceptionContext = context.toArray(); 190 } else if (!(value instanceof Class)) 191 { 192 renderException(exception); 193 return; 194 } else page = value; 195 196 if (page == null) return; 197 198 try 199 { 200 if (page instanceof Class) 201 page = componentClassResolver.resolvePageClassNameToPageName(((Class) page).getName()); 202 203 Link link = page instanceof Link 204 ? (Link) page 205 : linkSource.createPageRenderLink(page.toString(), false, exceptionContext); 206 207 if (request.isXHR()) 208 { 209 OutputStream os = response.getOutputStream("application/json;charset=UTF-8"); 210 211 JSONObject reply = new JSONObject(); 212 reply.in(InternalConstants.PARTIAL_KEY).put("redirectURL", link.toAbsoluteURI()); 213 214 os.write(reply.toCompactString().getBytes("UTF-8")); 215 216 os.close(); 217 218 return; 219 } 220 221 // Normal behavior is just a redirect. 222 223 response.sendRedirect(link); 224 } 225 // The above could throw an exception if we are already on a render request, but it's 226 // user's responsibility not to abuse the mechanism 227 catch (Exception e) 228 { 229 // Nothing to do but delegate 230 renderException(exception); 231 } 232 } 233 234 private void renderException(Throwable exception) throws IOException 235 { 236 logger.error(String.format("Processing of request failed with uncaught exception: %s", exception), exception); 237 238 // TAP5-233: Make sure the client knows that an error occurred. 239 240 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 241 242 String rawMessage = ExceptionUtils.toMessage(exception); 243 244 // Encode it compatibly with the JavaScript escape() function. 245 246 String encoded = URLEncoder.encode(rawMessage, "UTF-8").replace("+", "%20"); 247 248 response.setHeader("X-Tapestry-ErrorMessage", encoded); 249 250 Page page = pageCache.get(pageName); 251 252 ExceptionReporter rootComponent = (ExceptionReporter) page.getRootComponent(); 253 254 // Let the page set up for the new exception. 255 256 rootComponent.reportException(exception); 257 258 renderer.renderPageResponse(page); 259 } 260 261 /** 262 * Form exception context either from the name of the exception, or the context the exception contains if it's of type 263 * {@link ContextAwareException} 264 * 265 * @param exception The exception that the context is formed for 266 * @return Returns an array of objects to be used as the exception context 267 */ 268 @SuppressWarnings({"unchecked", "rawtypes"}) 269 protected Object[] formExceptionContext(Throwable exception) 270 { 271 if (exception instanceof ContextAwareException) return ((ContextAwareException) exception).getContext(); 272 273 Class exceptionClass = exception.getClass(); 274 // pick the first class in the hierarchy that's not anonymous, probably no reason check for array types 275 while ("".equals(exceptionClass.getSimpleName())) 276 exceptionClass = exceptionClass.getSuperclass(); 277 278 // check if exception type is plain runtimeException - yes, we really want the test to be this way 279 if (exceptionClass.isAssignableFrom(RuntimeException.class)) 280 return exception.getMessage() == null ? new Object[0] : new Object[]{exception.getMessage().toLowerCase()}; 281 282 // otherwise, form the context from the exception type name 283 String exceptionType = exceptionClass.getSimpleName(); 284 if (exceptionType.endsWith("Exception")) exceptionType = exceptionType.substring(0, exceptionType.length() - 9); 285 return new Object[]{exceptionType.toLowerCase()}; 286 } 287 288}