/*
* Weblounge: Web Content Management System
* Copyright (c) 2012 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.contentrepository.impl;
import ch.entwine.weblounge.common.content.PreviewGenerator;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceContent;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.ResourceUtils;
import ch.entwine.weblounge.common.content.image.ImagePreviewGenerator;
import ch.entwine.weblounge.common.content.image.ImageStyle;
import ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.repository.ResourceSerializer;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.ImageScalingMode;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
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.util.Date;
import java.util.List;
/**
* Worker implementation that creates a preview in a separate thread.
*/
class PreviewGeneratorWorker implements Runnable {
/** The logging facility */
private final Logger logger = LoggerFactory.getLogger(PreviewGeneratorWorker.class);
/** The content repository */
private AbstractContentRepository contentRepository = null;
/** The resource to render */
private Resource<?> resource = null;
/** The list of image styles to produce */
private List<ImageStyle> styles = null;
/** The environment */
private Environment environment = null;
/** The languages to render */
private List<Language> languages = null;
/** The format */
private String format = null;
/** Flag to indicate a canceled preview generation */
private boolean canceled = false;
/**
* Creates a new preview worker who will create the corresponding previews for
* the given resource and style.
*
* @param resource
* the resource
* @param environment
* the current environment
* @param languages
* the languages
* @param styles
* the image styles
*/
public PreviewGeneratorWorker(AbstractContentRepository repository,
Resource<?> resource, Environment environment, List<Language> languages,
List<ImageStyle> styles, String format) {
if (languages == null || languages.size() == 0)
throw new IllegalArgumentException("At least one language must be provided");
if (styles == null || styles.size() == 0)
throw new IllegalArgumentException("At least one preview style must be provided");
this.contentRepository = repository;
this.resource = resource;
this.environment = environment;
this.languages = languages;
this.styles = styles;
this.format = format;
}
/**
* Indicates to this preview generation worker to cancel the current
* operation.
*/
public void cancel() {
this.canceled = true;
}
/**
* {@inheritDoc}
*
* @see java.lang.Runnable#run()
*/
public void run() {
ResourceURI resourceURI = resource.getURI();
String resourceType = resourceURI.getType();
try {
// Find the resource serializer
ResourceSerializer<?, ?> serializer = contentRepository.getSerializerByType(resourceType);
if (serializer == null) {
logger.warn("Unable to index resources of type '{}': no resource serializer found", resourceType);
return;
}
// Does the serializer come with a preview generator?
PreviewGenerator previewGenerator = serializer.getPreviewGenerator(resource);
if (previewGenerator == null) {
logger.debug("Resource type '{}' does not support previews", resourceType);
return;
}
// Create the scaled images
String mimeType = "image/" + format;
ResourceSerializer<?, ?> s = contentRepository.getSerializerByMimeType(mimeType);
if (s == null) {
logger.warn("No resource serializer is capable of dealing with resources of format '{}'", mimeType);
return;
} else if (!(s instanceof ImageResourceSerializer)) {
logger.warn("Resource serializer lookup for format '{}' returned {}", format, s.getClass());
return;
}
// Find us an image serializer
ImageResourceSerializer irs = (ImageResourceSerializer) s;
ImagePreviewGenerator imagePreviewGenerator = (ImagePreviewGenerator) irs.getPreviewGenerator(format);
if (imagePreviewGenerator == null) {
logger.warn("Image resource serializer {} does not provide support for '{}'", irs, format);
return;
}
// Now scale the original preview according to the existing styles
for (Language l : languages) {
if (!resource.supportsContentLanguage(l))
continue;
// Have we been told to stop doing work in the meantime?
if (canceled)
return;
// Create the original preview image for every language
File originalPreview = null;
if (!resource.supportsContentLanguage(l))
continue;
originalPreview = createPreview(resource, null, l, previewGenerator, format);
if (originalPreview == null || !originalPreview.exists() || originalPreview.length() == 0) {
logger.warn("Preview generation for {} failed", resource);
return;
}
long resourceLastModified = ResourceUtils.getModificationDate(resource, l).getTime();
// Create the remaining styles
for (ImageStyle style : styles) {
// Have we been told to stop doing work in the meantime?
if (canceled)
return;
// The original has been produced already
if (ImageScalingMode.None.equals(style.getScalingMode()))
continue;
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File scaledFile = ImageStyleUtils.createScaledFile(resource, l, style);
// Create the file if it doesn't exist or if it is out dated. Note
// that the last modified date of a file has a precision of seconds
if (!scaledFile.isFile() || FileUtils.isFileOlder(scaledFile, new Date(resourceLastModified))) {
logger.info("Creating preview at {}", scaledFile.getAbsolutePath());
fis = new FileInputStream(originalPreview);
fos = new FileOutputStream(scaledFile);
imagePreviewGenerator.createPreview(originalPreview, environment, l, style, format, fis, fos);
scaledFile.setLastModified(Math.max(new Date().getTime(), resourceLastModified));
// Store the style definition used while creating the preview
File baseDir = ImageStyleUtils.getDirectory(resource.getURI().getSite(), style);
File definitionFile = new File(baseDir, "style.xml");
if (!definitionFile.isFile()) {
logger.debug("Storing style definition at {}", definitionFile);
definitionFile.createNewFile();
FileUtils.copyInputStreamToFile(IOUtils.toInputStream(style.toXml(), "UTF-8"), definitionFile);
}
} else {
logger.debug("Skipping creation of existing '{}' preview of {}", style, resource);
}
} catch (Throwable t) {
logger.error("Error scaling {}: {}", originalPreview, t.getMessage());
continue;
} finally {
IOUtils.closeQuietly(fis);
IOUtils.closeQuietly(fos);
}
}
}
} finally {
if (canceled)
logger.debug("Preview operation for {} has been canceled", resource.getIdentifier());
contentRepository.previewCreated(resource);
}
}
/**
* Creates the actual preview.
*
* @param resource
* the resource
* @param style
* the image style
* @param language
* the language
* @param previewGenerator
* the preview generator
* @param the
* preview format
* @return returns the preview file
*/
private File createPreview(Resource<?> resource, ImageStyle style,
Language language, PreviewGenerator previewGenerator, String format) {
ResourceURI resourceURI = resource.getURI();
String resourceType = resourceURI.getType();
// Create the filename
ResourceContent content = resource.getContent(language);
// Initiate creation of previews
InputStream resourceInputStream = null;
InputStream contentRepositoryIs = null;
FileOutputStream fos = null;
File scaledResourceFile = null;
try {
scaledResourceFile = ImageStyleUtils.getScaledFile(resource, language, style);
// Find the modification date
long lastModified = ResourceUtils.getModificationDate(resource, language).getTime();
// Create the file if it doesn't exist or if it is out dated. Note that
// the last modified date of a file has a precision of seconds
if (!scaledResourceFile.isFile() || FileUtils.isFileOlder(scaledResourceFile, new Date(lastModified))) {
contentRepositoryIs = contentRepository.getContent(resourceURI, language);
// Is this local content?
if (contentRepositoryIs == null && content != null && content.getExternalLocation() != null) {
contentRepositoryIs = content.getExternalLocation().openStream();
}
// Create the parent directory
File scaledResourceDir = scaledResourceFile.getParentFile();
if (!scaledResourceDir.isDirectory() && !scaledResourceDir.mkdirs()) {
AbstractContentRepository.logger.warn("Error creating parent directory of preview file {}", scaledResourceFile.getAbsolutePath());
return null;
}
// Create the file if it doesn't exist
if (!scaledResourceFile.isFile() && !scaledResourceFile.createNewFile()) {
AbstractContentRepository.logger.warn("Error creating preview file {}", scaledResourceFile.getAbsolutePath());
return null;
}
// Create the preview
fos = new FileOutputStream(scaledResourceFile);
AbstractContentRepository.logger.debug("Creating preview of '{}' at {}", resource, scaledResourceFile);
previewGenerator.createPreview(resource, environment, language, style, format, contentRepositoryIs, fos);
}
} catch (ContentRepositoryException e) {
AbstractContentRepository.logger.error("Error loading {} {} '{}' from {}: {}", new Object[] {
language,
resourceType,
resource,
this,
e.getMessage() });
AbstractContentRepository.logger.error(e.getMessage(), e);
IOUtils.closeQuietly(resourceInputStream);
} catch (IOException e) {
AbstractContentRepository.logger.warn("Error creating preview for {} '{}': {}", new Object[] {
resourceType,
resourceURI,
e.getMessage() });
IOUtils.closeQuietly(resourceInputStream);
} catch (Throwable t) {
AbstractContentRepository.logger.warn("Error creating preview for {} '{}': {}", new Object[] {
resourceType,
resourceURI,
t.getMessage() });
IOUtils.closeQuietly(resourceInputStream);
} finally {
IOUtils.closeQuietly(contentRepositoryIs);
IOUtils.closeQuietly(fos);
// Make sure corrupted preview images are being deleted
File f = scaledResourceFile;
if (f != null && f.length() == 0) {
FileUtils.deleteQuietly(f);
f = f.getParentFile();
while (f != null && f.isDirectory() && (f.listFiles() == null || f.listFiles().length == 0)) {
FileUtils.deleteQuietly(f);
f = f.getParentFile();
}
}
}
return scaledResourceFile;
}
}