/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.coverimage.impl;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import org.opencastproject.coverimage.CoverImageException;
import org.opencastproject.coverimage.CoverImageService;
import org.opencastproject.job.api.AbstractJobProducer;
import org.opencastproject.job.api.Job;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.MediaPackageElement.Type;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.workspace.api.Workspace;
import org.apache.batik.apps.rasterizer.DestinationType;
import org.apache.batik.apps.rasterizer.SVGConverter;
import org.apache.batik.apps.rasterizer.SVGConverterException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
/**
* Service for creating cover images
*/
public abstract class AbstractCoverImageService extends AbstractJobProducer implements CoverImageService {
protected static final String COVERIMAGE_WORKSPACE_COLLECTION = "coverimage";
/** List of available operations on jobs */
protected enum Operation {
Generate
}
/** The workspace service */
protected Workspace workspace = null;
/** The service registry service */
protected ServiceRegistry serviceRegistry;
/** The security service */
protected SecurityService securityService;
/** The user directory service */
protected UserDirectoryService userDirectoryService;
/** The organization directory service */
protected OrganizationDirectoryService organizationDirectoryService;
/** The logging facility */
private static final Logger log = LoggerFactory.getLogger(AbstractCoverImageService.class);
/** Creates a new composer service instance. */
public AbstractCoverImageService() {
super(JOB_TYPE);
}
@Override
protected String process(Job job) throws Exception {
List<String> arguments = job.getArguments();
String xml = arguments.get(0);
String xsl = arguments.get(1);
int width = Integer.valueOf(arguments.get(2));
int height = Integer.valueOf(arguments.get(3));
String posterImage = arguments.get(4);
String targetFlavor = arguments.get(5);
Operation op = null;
op = Operation.valueOf(job.getOperation());
switch (op) {
case Generate:
Attachment result = generateCoverImageInternal(job, xml, xsl, width, height, posterImage, targetFlavor);
return MediaPackageElementParser.getAsXml(result);
default:
throw new IllegalStateException("Don't know how to handle operation '" + job.getOperation() + "'");
}
}
@Override
public Job generateCoverImage(String xml, String xsl, String width, String height, String posterImageUri,
String targetFlavor) throws CoverImageException {
// Null values are not passed to the arguments list
if (posterImageUri == null)
posterImageUri = "";
try {
return serviceRegistry.createJob(JOB_TYPE, Operation.Generate.toString(),
Arrays.asList(xml, xsl, width, height, posterImageUri, targetFlavor));
} catch (ServiceRegistryException e) {
throw new CoverImageException("Unable to create a job", e);
}
}
protected Attachment generateCoverImageInternal(Job job, String xml, String xsl, int width, int height,
String posterImage, String targetFlavor) throws CoverImageException {
URI result;
File tempSvg = null;
File tempPng = null;
StringReader xmlReader = null;
try {
Document xslDoc = parseXsl(xsl);
// Create temp SVG file for transformation result
tempSvg = createTempFile(job, ".svg");
Result svg = new StreamResult(tempSvg);
// Load Metadata (from resources)
xmlReader = new StringReader(xml);
Source xmlSource = new StreamSource(xmlReader);
// Transform XML metadata with stylesheet to SVG
transformSvg(svg, xmlSource, xslDoc, width, height, posterImage);
// Rasterize SVG to PNG
tempPng = createTempFile(job, ".png");
rasterizeSvg(tempSvg, tempPng);
FileInputStream in = null;
try {
in = new FileInputStream(tempPng);
result = workspace.putInCollection(COVERIMAGE_WORKSPACE_COLLECTION, job.getId() + "_coverimage.png", in);
log.debug("Put the cover image into the workspace ({})", result);
} catch (FileNotFoundException e) {
// should never happen...
throw new CoverImageException(e);
} catch (IOException e) {
log.warn("Error while putting resulting image into workspace collection '{}': {}",
COVERIMAGE_WORKSPACE_COLLECTION, e);
throw new CoverImageException("Error while putting resulting image into workspace collection", e);
} finally {
IOUtils.closeQuietly(in);
}
} finally {
FileUtils.deleteQuietly(tempSvg);
FileUtils.deleteQuietly(tempPng);
log.debug("Removed temporary files");
IOUtils.closeQuietly(xmlReader);
}
return (Attachment) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
.elementFromURI(result, Type.Attachment, MediaPackageElementFlavor.parseFlavor(targetFlavor));
}
protected static Document parseXsl(String xsl) throws CoverImageException {
if (StringUtils.isBlank(xsl))
throw new IllegalArgumentException("XSL string must not be empty");
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
dbFactory.setNamespaceAware(true);
Document xslDoc;
try {
log.debug("Parse given XSL to a org.w3c.dom.Document object");
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
xslDoc = dBuilder.parse(new InputSource(new ByteArrayInputStream(xsl.getBytes("utf-8"))));
} catch (ParserConfigurationException e) {
// this should never happen...
throw new CoverImageException("The XSLT parser has serious configuration errors", e);
} catch (SAXException e) {
log.warn("Error while parsing the XSLT stylesheet: {}", e.getMessage());
throw new CoverImageException("Error while parsing the XSLT stylesheet", e);
} catch (IOException e) {
log.warn("Error while reading the XSLT stylesheet: {}", e.getMessage());
throw new CoverImageException("Error while reading the XSLT stylesheet", e);
}
return xslDoc;
}
protected static void transformSvg(Result svg, Source xmlSource, Document xslDoc, int width, int height,
String posterImage) throws TransformerFactoryConfigurationError, CoverImageException {
if (svg == null || xmlSource == null || xslDoc == null)
throw new IllegalArgumentException("Neither svg nor xmlSource nor xslDoc must be null");
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer;
try {
transformer = factory.newTransformer(new DOMSource(xslDoc));
} catch (TransformerConfigurationException e) {
// this should never happen...
throw new CoverImageException("The XSL transformer factory has serious configuration errors", e);
}
transformer.setParameter("width", width);
transformer.setParameter("height", height);
if (isNotBlank(posterImage))
transformer.setParameter("posterimage", posterImage);
Thread thread = Thread.currentThread();
ClassLoader loader = thread.getContextClassLoader();
thread.setContextClassLoader(AbstractCoverImageService.class.getClassLoader());
try {
log.debug("Transform XML source to SVG");
transformer.transform(xmlSource, svg);
} catch (TransformerException e) {
log.warn("Error while transforming SVG to image: {}", e.getMessage());
throw new CoverImageException("Error while transforming SVG to image", e);
} finally {
thread.setContextClassLoader(loader);
}
}
protected File createTempFile(Job job, String suffix) throws CoverImageException {
File tempFile;
try {
tempFile = File.createTempFile(COVERIMAGE_WORKSPACE_COLLECTION, Long.toString(job.getId()) + "_" + suffix);
log.debug("Created temporary file {}", tempFile);
} catch (IOException e) {
log.warn("Error creating temporary file: {}", e);
throw new CoverImageException("Error creating temporary file", e);
}
return tempFile;
}
protected static void rasterizeSvg(File svgSource, File pngResult) throws CoverImageException {
SVGConverter converter = new SVGConverter();
converter.setDestinationType(DestinationType.PNG);
converter.setDst(pngResult);
converter.setSources(new String[] { svgSource.getAbsolutePath() });
try {
log.debug("Start converting SVG to PNG");
converter.execute();
} catch (SVGConverterException e) {
log.warn("Error while converting the SVG to a PNG: {}", e.getMessage());
throw new CoverImageException("Error while converting the SVG to a PNG", e);
}
}
}