org.mayocat.shop.front.views.WebViewMessageBodyWriter.java Source code

Java tutorial

Introduction

Here is the source code for org.mayocat.shop.front.views.WebViewMessageBodyWriter.java

Source

/*
 * Copyright (c) 2012, Mayocat <hello@mayocat.org>
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package org.mayocat.shop.front.views;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.MessageBodyWriter;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.mayocat.context.WebContext;
import org.mayocat.shop.front.WebDataSupplier;
import org.mayocat.theme.TemplateNotFoundException;
import org.mayocat.theme.ThemeFileResolver;
import org.mayocat.theme.ThemeManager;
import org.mayocat.views.Template;
import org.mayocat.views.TemplateEngine;
import org.mayocat.views.TemplateEngineException;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.collect.Maps;
import com.google.common.io.Resources;

/**
 * @version $Id: decc1e38ba54d0531eb6daad4b8e2c327ce3d7ec $
 */
@Component("webViewMessageBodyWriter")
public class WebViewMessageBodyWriter implements MessageBodyWriter<WebView>, org.mayocat.rest.Provider {
    @Inject
    private Provider<TemplateEngine> engine;

    @Inject
    private ThemeManager themeManager;

    @Inject
    private ThemeFileResolver themeFileResolver;

    @Inject
    private Logger logger;

    @Inject
    private WebContext webContext;

    @Inject
    private Map<String, WebDataSupplier> dataSuppliers;

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return WebView.class.isAssignableFrom(type);
    }

    @Override
    public long getSize(WebView webView, Class<?> type, Type genericType, Annotation[] annotations,
            MediaType mediaType) {
        return -1;
    }

    @Override
    public void writeTo(WebView webView, Class<?> type, Type genericType, Annotation[] annotations,
            MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        try {

            if (!mediaType.equals(MediaType.APPLICATION_JSON_TYPE) && webContext.getTheme() != null
                    && !webContext.getTheme().isValidDefinition()) {
                // Fail fast with invalid theme error page, so that the developer knows ASAP and can correct it.
                writeHttpError("Invalid theme definition", entityStream);
                return;
            }

            Template masterTemplate = null;
            try {
                masterTemplate = themeFileResolver.getIndexTemplate(webContext.getRequest().getBreakpoint());
            } catch (TemplateNotFoundException e) {
                if (!mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) {
                    // For JSON API calls, we don't care if the template is found or not.
                    // For other calls, raise the exception
                    throw e;
                }
            }

            Template template = null;
            String jsonContext = null;

            if (!mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) {
                if (webView.model().isPresent()) {
                    // Check for a model

                    Optional<String> path = themeFileResolver.resolveModelPath(webView.model().get());
                    if (path.isPresent()) {
                        try {
                            template = themeFileResolver.getTemplate(path.get(),
                                    webContext.getRequest().getBreakpoint());
                        } catch (TemplateNotFoundException e) {
                            // Keep going
                        }
                    }
                    // else just fallback on the default model
                }

                if (template == null) {
                    try {
                        template = themeFileResolver.getTemplate(webView.template().toString(),
                                webContext.getRequest().getBreakpoint());
                    } catch (TemplateNotFoundException e) {
                        if (webView.hasOption(WebView.Option.FALLBACK_ON_DEFAULT_THEME)) {
                            try {
                                template = themeFileResolver.getTemplate(themeManager.getDefaultTheme(),
                                        webView.template().toString(), webContext.getRequest().getBreakpoint());
                            } catch (TemplateNotFoundException e1) {
                                // continue
                            }
                        }
                        if (template == null && webView.hasOption(WebView.Option.FALLBACK_ON_GLOBAL_TEMPLATES)) {
                            template = themeFileResolver.getGlobalTemplate(webView.template().toString(),
                                    webContext.getRequest().getBreakpoint());
                        }
                    }
                }
            }

            if (!mediaType.equals(MediaType.APPLICATION_JSON_TYPE)
                    || httpHeaders.containsKey("X-Mayocat-Full-Context")) {
                if (template != null) {
                    webView.data().put("templateContent", template.getId());
                    webView.data().put("template", FilenameUtils.getBaseName(webView.template().toString()));
                }

                for (WebDataSupplier supplier : dataSuppliers.values()) {
                    supplier.supply(webView.data());
                }
            }

            try {
                ObjectMapper mapper = new ObjectMapper();

                if (mediaType.equals(MediaType.APPLICATION_JSON_TYPE)) {
                    mapper.writeValue(entityStream, webView.data());
                    return;
                }

                if (template == null) {
                    throw new TemplateNotFoundException();
                }

                jsonContext = mapper.writeValueAsString(webView.data());
                engine.get().register(template);
                engine.get().register(masterTemplate);
                String rendered = engine.get().render(masterTemplate.getId(), jsonContext);
                entityStream.write(rendered.getBytes());
            } catch (JsonMappingException e) {
                this.logger.warn("Failed to serialize JSON context", e);
                writeDeveloperError(webView, e, entityStream);
            } catch (TemplateEngineException e) {
                writeDeveloperError(webView, e, entityStream);
            }
        } catch (TemplateNotFoundException e) {
            throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity("Template not found : " + webView.template().toString()).build());
        }
    }

    private void writeHttpError(String message, OutputStream entityStream) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
            Template error;
            Template masterTemplate = null;
            try {
                masterTemplate = themeFileResolver.getIndexTemplate(webContext.getRequest().getBreakpoint());
            } catch (TemplateNotFoundException e) {
                // Nothing doing
            }
            try {
                error = themeFileResolver.getTemplate("500.html", webContext.getRequest().getBreakpoint());
            } catch (TemplateNotFoundException notFound) {
                // Fallback on the classpath hosted error 500 file
                error = new Template("500",
                        Resources.toString(Resources.getResource("templates/500.html"), Charsets.UTF_8));
            }
            Map<String, Object> errorContext = Maps.newHashMap();
            errorContext.put("error", message);

            engine.get().register(error);

            String rendered;

            if (masterTemplate != null) {
                errorContext.put("templateContent", error.getId());
                errorContext.put("template", "500");
                engine.get().register(masterTemplate);
                rendered = engine.get().render(masterTemplate.getId(), mapper.writeValueAsString(errorContext));
            } else {
                rendered = engine.get().render(error.getId(), mapper.writeValueAsString(errorContext));
            }

            entityStream.write(rendered.getBytes());
        } catch (Exception e1) {
            throw new RuntimeException(e1);
        }
    }

    private void writeDeveloperError(WebView webView, Exception e, OutputStream entityStream) {
        try {
            // Note:
            // This could be seen as a "server error", but we don't set the Status header to 500 because we want to be
            // able to distinguish between actual server errors (internal Mayocat Shop server error) and theme
            // developers errors (which this is).
            // This is comes at play when setting up monitoring with alerts on a number of 5xx response above a
            // certain threshold.

            // Re-serialize the context as json with indentation for better debugging
            ObjectMapper mapper = new ObjectMapper();
            mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
            Map<String, Object> context = webView.data();
            String jsonContext = mapper.writeValueAsString(context);
            Template error = new Template("developerError",
                    Resources.toString(Resources.getResource("templates/developerError.html"), Charsets.UTF_8));
            Map<String, Object> errorContext = Maps.newHashMap();
            errorContext.put("error", StringEscapeUtils.escapeXml(cleanErrorMessageForDisplay(e.getMessage())));
            errorContext.put("stackTrace", StringEscapeUtils.escapeXml(ExceptionUtils.getStackTrace(e)));
            errorContext.put("context", StringEscapeUtils.escapeXml(jsonContext).trim());
            errorContext.put("rawContext", jsonContext);
            errorContext.put("template", webView.template().toString());

            engine.get().register(error);
            String rendered = engine.get().render(error.getId(), mapper.writeValueAsString(errorContext));
            entityStream.write(rendered.getBytes());
        } catch (Exception e1) {
            throw new RuntimeException(e1);
        }
    }

    private String cleanErrorMessageForDisplay(String errorMessage) {
        String clean = StringUtils.substringAfter(errorMessage,
                "org.mozilla.javascript.JavaScriptException: Error:");
        clean = clean.replaceAll("\\(handlebars\\.js#\\d+\\)", "");
        return clean;
    }
}