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}