    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}