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