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