    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}