ch.entwine.weblounge.preview.xhtmlrenderer.XhtmlRendererPagePreviewGenerator.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.preview.xhtmlrenderer.XhtmlRendererPagePreviewGenerator.java

Source

/*
 *  Weblounge: Web Content Management System
 *  Copyright (c) 2011 The Weblounge Team
 *  http://weblounge.o2it.ch
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software Foundation
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package ch.entwine.weblounge.preview.xhtmlrenderer;

import ch.entwine.weblounge.common.content.PreviewGenerator;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.image.ImagePreviewGenerator;
import ch.entwine.weblounge.common.content.image.ImageStyle;
import ch.entwine.weblounge.common.content.page.Page;
import ch.entwine.weblounge.common.content.page.PagePreviewGenerator;
import ch.entwine.weblounge.common.impl.testing.MockHttpServletRequest;
import ch.entwine.weblounge.common.impl.testing.MockHttpServletResponse;
import ch.entwine.weblounge.common.impl.util.html.HTMLUtils;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.UrlUtils;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.Serializer;
import org.htmlcleaner.SimpleXmlSerializer;
import org.htmlcleaner.TagNode;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xhtmlrenderer.swing.Java2DRenderer;
import org.xhtmlrenderer.swing.NaiveUserAgent;
import org.xhtmlrenderer.util.FSImageWriter;
import org.xhtmlrenderer.util.XRLog;
import org.xhtmlrenderer.util.XRRuntimeException;

import java.awt.HeadlessException;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

/**
 * A <code>PreviewGenerator</code> that will generate previews for pages.
 */
public class XhtmlRendererPagePreviewGenerator implements PagePreviewGenerator {

    /** Logger factory */
    private static final Logger logger = LoggerFactory.getLogger(XhtmlRendererPagePreviewGenerator.class);

    /** Page request handler path prefix */
    protected static final String PAGE_HANDLER_PREFIX = "/weblounge-pages/";

    /** Format for the preview images */
    private static final String PREVIEW_FORMAT = "png";

    /** Format for the preview images */
    private static final String PREVIEW_CONTENT_TYPE = "image/png";

    /** Default width for taking screenshots */
    private static final int DEFAULT_SCREENSHOT_WIDTH = 1024;

    /** Default height for taking screenshots */
    private static final int DEFAULT_SCREENSHOT_HEIGHT = 768;

    /** The site servlets */
    private static Map<String, Servlet> siteServlets = new HashMap<String, Servlet>();

    /** The preview generators */
    private List<ImagePreviewGenerator> previewGenerators = new ArrayList<ImagePreviewGenerator>();

    /** The user agents per site */
    private static Map<String, WebloungeUserAgent> userAgents = new HashMap<String, WebloungeUserAgent>();

    /** Warning flags */
    private boolean isRenderingEnvironmentSane = true;

    /** The site servlet service tracker */
    private ServiceTracker siteServletTracker = null;

    /** The preview generator service tracker */
    private ServiceTracker previewGeneratorTracker = null;

    /** Filter expression used to look up site servlets */
    private static final String serviceFilter = "(&(objectclass=" + Servlet.class.getName() + ")("
            + Site.class.getName().toLowerCase() + "=*))";

    /**
     * Callback from OSGi declarative services on component startup.
     * 
     * @param ctx
     *          the component context
     */
    public void activate(ComponentContext ctx) {
        try {
            Filter filter = ctx.getBundleContext().createFilter(serviceFilter);
            siteServletTracker = new SiteServletTracker(ctx.getBundleContext(), filter);
            siteServletTracker.open();
            previewGeneratorTracker = new ImagePreviewGeneratorTracker(ctx.getBundleContext());
            previewGeneratorTracker.open();
        } catch (InvalidSyntaxException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Callback from OSGi declarative services on component shutdown.
     */
    public void deactivate() {
        if (siteServletTracker != null) {
            siteServletTracker.close();
        }
        if (previewGeneratorTracker != null) {
            previewGeneratorTracker.close();
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.content.PreviewGenerator#supports(ch.entwine.weblounge.common.content.Resource)
     */
    public boolean supports(Resource<?> resource) {
        return (resource instanceof Page);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.content.PreviewGenerator#supports(java.lang.String)
     */
    public boolean supports(String format) {
        for (ImagePreviewGenerator generator : previewGenerators) {
            if (generator.supports(PREVIEW_FORMAT) && generator.supports(format))
                return true;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.content.PreviewGenerator#getPriority()
     */
    public int getPriority() {
        return 0;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.content.PreviewGenerator#createPreview(ch.entwine.weblounge.common.content.Resource,
     *      ch.entwine.weblounge.common.site.Environment,
     *      ch.entwine.weblounge.common.language.Language,
     *      ch.entwine.weblounge.common.content.image.ImageStyle, String,
     *      java.io.InputStream, java.io.OutputStream)
     */
    public void createPreview(Resource<?> resource, Environment environment, Language language, ImageStyle style,
            String format, InputStream is, OutputStream os) throws IOException {

        if (!isRenderingEnvironmentSane) {
            logger.debug("Skipping page preview rendering as environment is not sane");
            return;
        }

        if (resource == null)
            throw new IllegalArgumentException("Resource cannot be null");

        ImagePreviewGenerator imagePreviewGenerator = null;
        synchronized (previewGenerators) {
            if (previewGenerators.size() == 0) {
                logger.debug("Unable to generate page previews since no image renderer is available");
                return;
            }
            imagePreviewGenerator = previewGenerators.get(0);
        }

        ResourceURI uri = resource.getURI();
        Site site = uri.getSite();
        String html = null;
        try {
            URL pageURL = new URL(UrlUtils.concat(site.getHostname(environment).toExternalForm(),
                    PAGE_HANDLER_PREFIX, uri.getIdentifier()));
            html = render(pageURL, site, environment, language, resource.getVersion());
            if (StringUtils.isBlank(html)) {
                logger.warn("Error rendering preview of page " + uri.getPath());
                return;
            }
            html = HTMLUtils.escapeHtml(HTMLUtils.unescape(html));
        } catch (ServletException e) {
            logger.warn("Error rendering page " + uri.getPath(), e);
            throw new IOException(e);
        }

        // Try to convert html to xhtml
        HtmlCleaner cleaner = new HtmlCleaner();
        CleanerProperties xhtmlProperties = cleaner.getProperties();
        TagNode xhtmlNode = cleaner.clean(html);
        if (xhtmlNode == null) {
            logger.warn("Error creating well-formed document from page {}", resource);
            return;
        }

        File xhtmlFile = null;
        is = new ByteArrayInputStream(html.getBytes("UTF-8"));

        // Write the resource content to disk. This step is needed, as the preview
        // generator can only handle files.
        try {
            xhtmlFile = File.createTempFile("xhtml", ".xml");
            Serializer xhtmlSerializer = new SimpleXmlSerializer(xhtmlProperties);
            xhtmlSerializer.writeToFile(xhtmlNode, xhtmlFile.getAbsolutePath(), "UTF-8");
        } catch (IOException e) {
            logger.error("Error creating temporary copy of file content at " + xhtmlFile, e);
            FileUtils.deleteQuietly(xhtmlFile);
            throw e;
        } finally {
            IOUtils.closeQuietly(is);
        }

        File imageFile = File.createTempFile("xhtml-preview", "." + PREVIEW_FORMAT);
        FileOutputStream imageFos = null;

        // Render the page and write back to client
        try {
            int screenshotWidth = DEFAULT_SCREENSHOT_WIDTH;
            int screenshotHeight = DEFAULT_SCREENSHOT_HEIGHT;
            if (style != null && style.getWidth() > 0 && style.getHeight() > 0) {
                screenshotHeight = (int) ((float) screenshotWidth / (float) style.getWidth() * style.getHeight());
            }

            // Create the renderer. Due to a synchronization bug in the software,
            // this needs to be synchronized
            Java2DRenderer renderer = null;
            try {
                synchronized (this) {
                    renderer = new Java2DRenderer(xhtmlFile, screenshotWidth, screenshotHeight);
                }
            } catch (Throwable t) {
                if (isRenderingEnvironmentSane) {
                    logger.warn("Error creating Java 2D renderer for previews: {}" + t.getMessage());
                    logger.warn("Page preview rendering will be switched off");
                    isRenderingEnvironmentSane = false;
                }
                logger.debug("Error creating Java 2D renderer for preview of page {}: {}" + uri.getPath(),
                        t.getMessage());
                return;
            }

            // Configure the renderer
            renderer.getSharedContext().setBaseURL(site.getHostname().toExternalForm());
            renderer.getSharedContext().setInteractive(false);

            // Make sure the renderer is using a user agent that will correctly
            // resolve urls
            WebloungeUserAgent agent = userAgents.get(site.getIdentifier());
            if (agent == null) {
                agent = new WebloungeUserAgent(site.getHostname().getURL());
                userAgents.put(site.getIdentifier(), agent);
            }
            renderer.getSharedContext().setUserAgentCallback(agent);

            // Render the page to an image
            BufferedImage img = renderer.getImage();
            FSImageWriter imageWriter = new FSImageWriter(PREVIEW_FORMAT);
            imageFos = new FileOutputStream(imageFile);
            imageWriter.write(img, imageFos);

        } catch (IOException e) {
            logger.error("Error creating temporary copy of file content at " + xhtmlFile, e);
            throw e;
        } catch (XRRuntimeException e) {
            logger.warn("Error rendering page content at " + uri + ": " + e.getMessage());
            throw e;
        } catch (HeadlessException e) {
            logger.warn("Headless error while trying to render page preview: " + e.getMessage());
            logger.warn("Page preview rendering will be switched off");
            isRenderingEnvironmentSane = false;
            throw e;
        } catch (Throwable t) {
            logger.warn("Error rendering page content at " + uri + ": " + t.getMessage(), t);
            throw new IOException(t);
        } finally {
            IOUtils.closeQuietly(imageFos);
            FileUtils.deleteQuietly(xhtmlFile);
        }

        FileInputStream imageIs = null;

        // Scale the image to the correct size
        try {
            imageIs = new FileInputStream(imageFile);
            imagePreviewGenerator.createPreview(resource, environment, language, style, PREVIEW_FORMAT, imageIs,
                    os);
        } catch (IOException e) {
            logger.error("Error creating temporary copy of file content at " + xhtmlFile, e);
            throw e;
        } catch (Throwable t) {
            logger.warn("Error scaling page preview at " + uri + ": " + t.getMessage(), t);
            throw new IOException(t);
        } finally {
            IOUtils.closeQuietly(imageIs);
            FileUtils.deleteQuietly(imageFile);
        }

    }

    /**
     * Renders the page located at <code>rendererURL</code> in the given language.
     * 
     * @param rendererURL
     *          the page url
     * @param site
     *          the site
     * @param environment
     *          the environment
     * @param language
     *          the language
     * @param version
     *          the version
     * @return the rendered <code>HTML</code>
     * @throws ServletException
     *           if rendering fails
     * @throws IOException
     *           if reading from the servlet fails
     */
    private String render(URL rendererURL, Site site, Environment environment, Language language, long version)
            throws ServletException, IOException {
        Servlet servlet = siteServlets.get(site.getIdentifier());

        String httpContextURI = UrlUtils.concat("/weblounge-sites", site.getIdentifier());
        int httpContextURILength = httpContextURI.length();
        String url = rendererURL.toExternalForm();
        int uriInPath = url.indexOf(httpContextURI);

        // Are we trying to render a site resource (e. g. a jsp during
        // precompilation)?
        if (uriInPath > 0) {
            String pathInfo = url.substring(uriInPath + httpContextURILength);

            // Prepare the mock request
            MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
            request.setServerName(site.getHostname(environment).getURL().getHost());
            request.setServerPort(site.getHostname(environment).getURL().getPort());
            request.setMethod(site.getHostname(environment).getURL().getProtocol());
            request.setAttribute(WebloungeRequest.LANGUAGE, language);
            request.setPathInfo(pathInfo);
            request.setRequestURI(UrlUtils.concat(httpContextURI, pathInfo));

            MockHttpServletResponse response = new MockHttpServletResponse();
            servlet.service(request, response);
            return response.getContentAsString();
        } else {
            HttpClient httpClient = new DefaultHttpClient();
            httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BEST_MATCH);
            try {
                if (version == Resource.WORK) {
                    rendererURL = new URL(UrlUtils.concat(rendererURL.toExternalForm(),
                            "work_" + language.getIdentifier() + ".html"));
                } else {
                    rendererURL = new URL(UrlUtils.concat(rendererURL.toExternalForm(),
                            "index_" + language.getIdentifier() + ".html"));
                }
                HttpGet getRequest = new HttpGet(rendererURL.toExternalForm());
                getRequest.addHeader(new BasicHeader("X-Weblounge-Special", "Page-Preview"));
                HttpResponse response = httpClient.execute(getRequest);
                if (response.getStatusLine().getStatusCode() != HttpServletResponse.SC_OK)
                    return null;
                String responseText = EntityUtils.toString(response.getEntity(), "utf-8");
                return responseText;
            } finally {
                httpClient.getConnectionManager().shutdown();
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.content.PreviewGenerator#getContentType(ch.entwine.weblounge.common.content.Resource,
     *      ch.entwine.weblounge.common.language.Language,
     *      ch.entwine.weblounge.common.content.image.ImageStyle)
     */
    public String getContentType(Resource<?> resource, Language language, ImageStyle style) {
        return PREVIEW_CONTENT_TYPE;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.content.PreviewGenerator#getSuffix(ch.entwine.weblounge.common.content.Resource,
     *      ch.entwine.weblounge.common.language.Language,
     *      ch.entwine.weblounge.common.content.image.ImageStyle)
     */
    public String getSuffix(Resource<?> resource, Language language, ImageStyle style) {
        return PREVIEW_FORMAT;
    }

    /**
     * Adds the preview generator to the list of registered preview generators.
     * 
     * @param generator
     *          the generator
     */
    void addPreviewGenerator(ImagePreviewGenerator generator) {
        synchronized (previewGenerators) {
            previewGenerators.add(generator);
            Collections.sort(previewGenerators, new Comparator<PreviewGenerator>() {
                public int compare(PreviewGenerator a, PreviewGenerator b) {
                    return Integer.valueOf(a.getPriority()).compareTo(b.getPriority());
                }
            });
        }
    }

    /**
     * Removes the preview generator from the list of registered preview
     * generators.
     * 
     * @param generator
     *          the generator
     */
    void removePreviewGenerator(ImagePreviewGenerator generator) {
        synchronized (previewGenerators) {
            previewGenerators.remove(generator);
        }
    }

    /**
     * Adds the site servlet to the list of servlets.
     * 
     * @param id
     *          the site identifier
     * @param servlet
     *          the site servlet
     */
    void addSiteServlet(String id, Servlet servlet) {
        logger.debug("Site servlet attached to {} workbench", id);
        siteServlets.put(id, servlet);
    }

    /**
     * Removes the site servlet from the list of servlets
     * 
     * @param site
     *          the site identifier
     */
    void removeSiteServlet(String id) {
        logger.debug("Site servlet detached from {} workbench", id);
        siteServlets.remove(id);
        userAgents.remove(id);
    }

    /**
     * Implementation of a <code>ServiceTracker</code> that is tracking instances
     * of type {@link Servlet} with an associated <code>site</code> attribute.
     */
    private class SiteServletTracker extends ServiceTracker {

        /**
         * Creates a new servlet tracker that is using the given bundle context to
         * look up service instances.
         * 
         * @param ctx
         *          the bundle context
         * @param filter
         *          the service filter
         */
        SiteServletTracker(BundleContext ctx, Filter filter) {
            super(ctx, filter, null);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference)
         */
        @Override
        public Object addingService(ServiceReference reference) {
            Servlet servlet = (Servlet) super.addingService(reference);
            String site = (String) reference.getProperty(Site.class.getName().toLowerCase());
            addSiteServlet(site, servlet);
            return servlet;
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference,
         *      java.lang.Object)
         */
        @Override
        public void removedService(ServiceReference reference, Object service) {
            String site = (String) reference.getProperty("site");
            removeSiteServlet(site);
        }

    }

    /**
     * Implementation of a <code>ServiceTracker</code> that is tracking instances
     * of type {@link ImagePreviewGenerator} with an associated <code>site</code>
     * attribute.
     */
    private class ImagePreviewGeneratorTracker extends ServiceTracker {

        /**
         * Creates a new service tracker that is using the given bundle context to
         * look up service instances.
         * 
         * @param ctx
         *          the bundle context
         */
        ImagePreviewGeneratorTracker(BundleContext ctx) {
            super(ctx, ImagePreviewGenerator.class.getName(), null);
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference)
         */
        @Override
        public Object addingService(ServiceReference reference) {
            ImagePreviewGenerator previewGenerator = (ImagePreviewGenerator) super.addingService(reference);
            addPreviewGenerator(previewGenerator);
            return previewGenerator;
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference,
         *      java.lang.Object)
         */
        @Override
        public void removedService(ServiceReference reference, Object service) {
            removePreviewGenerator((ImagePreviewGenerator) service);
        }

    }

    /**
     * This class provides a bug fix to the {@link NaiveUserAgent} class from the
     * xhtml renderer.
     */
    static class WebloungeUserAgent extends NaiveUserAgent {

        /** The base URL */
        private String baseURL = null;

        /**
         * Creates a user agent that will use <code>baseURL</code> to resolve uris
         * without a protocol (paths, that is).
         * 
         * @param baseURL
         *          the base url
         */
        WebloungeUserAgent(URL baseURL) {
            this.baseURL = baseURL.toExternalForm();
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.xhtmlrenderer.swing.NaiveUserAgent#getBaseURL()
         */
        @Override
        public String getBaseURL() {
            return baseURL;
        }

        /**
         * {@inheritDoc}
         * 
         * @see org.xhtmlrenderer.swing.NaiveUserAgent#resolveURI(java.lang.String)
         */
        @Override
        public String resolveURI(String uri) {
            if (uri == null)
                return null;
            try {
                URL result = new URL(uri);
                return result.toExternalForm();
            } catch (MalformedURLException e1) {
                try {
                    URL result = new URL(UrlUtils.concat(baseURL, uri));
                    return result.toString();
                } catch (MalformedURLException e2) {
                    XRLog.exception("The default NaiveUserAgent cannot resolve the URL " + uri + " with base URL "
                            + getBaseURL());
                    return null;
                }
            }
        }

    }

}