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