ch.entwine.weblounge.preview.phantomjs.PhantomJsPagePreviewGenerator.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.preview.phantomjs.PhantomJsPagePreviewGenerator.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.phantomjs;

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.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.impl.util.process.ProcessExcecutorException;
import ch.entwine.weblounge.common.impl.util.process.ProcessExecutor;
import ch.entwine.weblounge.common.language.Language;
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.osgi.framework.BundleContext;
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 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.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 java.util.concurrent.atomic.AtomicBoolean;

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

    /** Logger factory */
    private static final Logger logger = LoggerFactory.getLogger(PhantomJsPagePreviewGenerator.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";

    /** Name of the script parameter */
    private static final String PARAM_PREPARE_SCRIPT = "prepare.js";

    /** Name of the script */
    private static final String SCRIPT_FILE = "/phantomjs/render.js";

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

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

    /** The script template */
    private String scriptTemplate = null;

    /** Directory containing temporary files */
    private File phantomTmpDir = null;

    /** The script */
    private File scriptFile = null;

    /**
     * Called by the {@link PhantomJsActivator} on service activation.
     * 
     * @param ctx
     *          the component context
     */
    void activate(ComponentContext ctx) {
        try {
            prepareScript();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        previewGeneratorTracker = new ImagePreviewGeneratorTracker(ctx.getBundleContext());
        previewGeneratorTracker.open();
    }

    /**
     * Called by the {@link PhantomJsActivator} on service inactivation.
     */
    void deactivate() {
        if (previewGeneratorTracker != null) {
            previewGeneratorTracker.close();
        }
        FileUtils.deleteQuietly(phantomTmpDir);
    }

    /**
     * {@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#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 {

        // We don't need the input stream
        IOUtils.closeQuietly(is);

        // Find a suitable image preview generator for scaling
        ImagePreviewGenerator imagePreviewGenerator = null;
        synchronized (previewGenerators) {
            for (ImagePreviewGenerator generator : previewGenerators) {
                if (generator.supports(format)) {
                    imagePreviewGenerator = generator;
                    break;
                }
            }
            if (imagePreviewGenerator == null) {
                logger.debug("Unable to generate page previews since no image renderer is available");
                return;
            }
        }

        // Find the relevant metadata to start the request
        ResourceURI uri = resource.getURI();
        long version = resource.getVersion();
        Site site = uri.getSite();

        // Create the url
        URL pageURL = new URL(UrlUtils.concat(site.getHostname(environment).toExternalForm(), PAGE_HANDLER_PREFIX,
                uri.getIdentifier()));
        if (version == Resource.WORK) {
            pageURL = new URL(
                    UrlUtils.concat(pageURL.toExternalForm(), "work_" + language.getIdentifier() + ".html"));
        } else {
            pageURL = new URL(
                    UrlUtils.concat(pageURL.toExternalForm(), "index_" + language.getIdentifier() + ".html"));
        }

        // Create a temporary file
        final File rendererdFile = File.createTempFile("phantomjs-", "." + format, phantomTmpDir);
        final URL finalPageURL = pageURL;
        final AtomicBoolean success = new AtomicBoolean();

        // Call PhantomJS to render the page
        try {
            final PhantomJsProcessExecutor phantomjs = new PhantomJsProcessExecutor(scriptFile.getAbsolutePath(),
                    pageURL.toExternalForm(), rendererdFile.getAbsolutePath()) {
                @Override
                protected void onProcessFinished(int exitCode) throws IOException {
                    super.onProcessFinished(exitCode);
                    switch (exitCode) {
                    case 0:
                        if (rendererdFile.length() > 0) {
                            success.set(true);
                            logger.debug("Page preview of {} created at {}", finalPageURL,
                                    rendererdFile.getAbsolutePath());
                        } else {
                            logger.warn("Error creating page preview of {}", finalPageURL);
                            success.set(false);
                            FileUtils.deleteQuietly(rendererdFile);
                        }
                        break;
                    default:
                        success.set(false);
                        logger.warn("Error creating page preview of {}", finalPageURL);
                        FileUtils.deleteQuietly(rendererdFile);
                    }
                }
            };

            // Finally have PhantomJS create the preview
            logger.debug("Creating preview of {}", finalPageURL);
            phantomjs.execute();

        } catch (ProcessExcecutorException e) {
            logger.warn("Error creating page preview of {}: {}", pageURL, e.getMessage());
            throw new IOException(e);
        } finally {
            // If page preview rendering failed, there is no point in scaling the
            // images
            if (!success.get()) {
                logger.debug("Skipping scaling of failed preview rendering {}", pageURL);
                FileUtils.deleteQuietly(rendererdFile);
                return;
            }
        }

        FileInputStream imageIs = null;

        // Scale the image to the correct size
        try {
            imageIs = new FileInputStream(rendererdFile);
            imagePreviewGenerator.createPreview(resource, environment, language, style, PREVIEW_FORMAT, imageIs,
                    os);
        } catch (IOException e) {
            logger.error("Error reading original page preview from " + rendererdFile, 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(rendererdFile);
        }

    }

    /**
     * {@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;
    }

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

    /**
     * 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(b.getPriority()).compareTo(a.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);
        }
    }

    /**
     * Reads the script from the resource, processes the variables and writes it
     * to the file system so it can be accessed by PhantomJS.
     * 
     * @throws IOException
     *           if reading the template or writing the file failed
     */
    private void prepareScript() throws IOException {
        InputStream is = null;
        InputStream fis = null;
        OutputStream os = null;
        try {
            // Create the temporary directory for everything PhantomJS
            phantomTmpDir = new File(FileUtils.getTempDirectory(), "phantomjs");
            if (!phantomTmpDir.isDirectory() && !phantomTmpDir.mkdirs()) {
                logger.error("Unable to create temp directory for PhantomJS at {}", phantomTmpDir);
                throw new IOException("Unable to create temp directory for PhantomJS at " + phantomTmpDir);
            }

            // Create the script
            is = PhantomJsPagePreviewGenerator.class.getResourceAsStream(SCRIPT_FILE);
            scriptTemplate = IOUtils.toString(is);
            scriptFile = new File(phantomTmpDir, "pagepreview.js");

            // Process templates
            Map<String, String> properties = new HashMap<String, String>();
            properties.put(PARAM_PREPARE_SCRIPT, "return true;");
            String script = ConfigurationUtils.processTemplate(scriptTemplate, properties);

            // Write the processed script to disk
            fis = IOUtils.toInputStream(script);
            os = new FileOutputStream(scriptFile);
            IOUtils.copy(fis, os);

        } catch (IOException e) {
            logger.error("Error reading phantomjs script template from " + SCRIPT_FILE, e);
            FileUtils.deleteQuietly(scriptFile);
            throw e;
        } finally {
            IOUtils.closeQuietly(is);
            IOUtils.closeQuietly(fis);
            IOUtils.closeQuietly(os);
        }
    }

    /**
     * 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 process executor is used to run <code>PhantomJS</code> in the system
     * shell.
     */
    private static class PhantomJsProcessExecutor extends ProcessExecutor<IOException> {

        /**
         * Creates a process executor for the phantom JS rendering process
         * 
         * @param script
         *          path to the javascript
         * @param address
         *          the web page to connect to
         * @param file
         *          the file to write to
         */
        protected PhantomJsProcessExecutor(String script, String address, String file) {
            super("phantomjs", new String[] { script, address, file });
        }

    }

}