/**
* 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.workflow.handler.coverimage;
import org.opencastproject.coverimage.CoverImageException;
import org.opencastproject.coverimage.CoverImageService;
import org.opencastproject.job.api.Job;
import org.opencastproject.job.api.JobContext;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.metadata.api.MetadataValue;
import org.opencastproject.metadata.api.StaticMetadata;
import org.opencastproject.metadata.api.StaticMetadataService;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.util.MimeTypes;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.data.Option;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowOperationResult;
import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
import org.opencastproject.workspace.api.Workspace;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
/**
* Base implementation of the cover image workflow operation handler
*/
public abstract class CoverImageWorkflowOperationHandlerBase extends AbstractWorkflowOperationHandler {
private static final String COVERIMAGE_FILENAME = "coverimage.png";
private static final String XSL_FILE_URL = "stylesheet";
private static final String XML_METADATA = "metadata";
private static final String WIDTH = "width";
private static final String HEIGHT = "height";
private static final String POSTERIMAGE_FLAVOR = "posterimage-flavor";
private static final String POSTERIMAGE_URL = "posterimage";
private static final String TARGET_FLAVOR = "target-flavor";
private static final String TARGET_TAGS = "target-tags";
/** The configuration options for this handler */
private static final SortedMap<String, String> CONFIG_OPTIONS;
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(CoverImageWorkflowOperationHandlerBase.class);
static {
CONFIG_OPTIONS = new TreeMap<String, String>();
CONFIG_OPTIONS.put(XSL_FILE_URL, "URL to the XSL stylesheet");
CONFIG_OPTIONS.put(XML_METADATA, "XML metadata");
CONFIG_OPTIONS.put(WIDTH, "Width of the resulting cover image");
CONFIG_OPTIONS.put(HEIGHT, "Height of the resulting cover image");
CONFIG_OPTIONS.put(POSTERIMAGE_FLAVOR, "Poster image flavor");
CONFIG_OPTIONS.put(POSTERIMAGE_URL, "URL to a poster image");
CONFIG_OPTIONS.put(TARGET_FLAVOR, "Target flavor");
CONFIG_OPTIONS.put(TARGET_TAGS, "Target tags");
}
/** Returns a cover image service */
protected abstract CoverImageService getCoverImageService();
/** Returns a workspace service */
protected abstract Workspace getWorkspace();
/** Returns a static metadata service */
protected abstract StaticMetadataService getStaticMetadataService();
/** Returns a dublin core catalog service */
protected abstract DublinCoreCatalogService getDublinCoreCatalogService();
@Override
public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
throws WorkflowOperationException {
MediaPackage mediaPackage = workflowInstance.getMediaPackage();
WorkflowOperationInstance operation = workflowInstance.getCurrentOperation();
logger.info("Cover Image Workflow started for media package '{}'", mediaPackage.getIdentifier());
// User XML metadata from operation configuration, fallback to default metadata
String xml = operation.getConfiguration(XML_METADATA);
if (xml == null) {
xml = getMetadataXml(mediaPackage);
logger.debug("Metadata was not part of operation configuration, using Dublin Core as fallback");
}
logger.debug("Metadata set to: {}", xml);
String xsl = loadXsl(operation);
logger.debug("XSL for transforming metadata to SVG loaded: {}", xsl);
// Read image dimensions
int width = getIntConfiguration(operation, WIDTH);
logger.debug("Image width set to {}px", width);
int height = getIntConfiguration(operation, HEIGHT);
logger.debug("Image height set to {}px", height);
// Read optional poster image flavor
String posterImgUri = getPosterImageFileUrl(operation.getConfiguration(POSTERIMAGE_URL));
if (posterImgUri == null)
posterImgUri = getPosterImageFileUrl(mediaPackage, operation.getConfiguration(POSTERIMAGE_FLAVOR));
if (posterImgUri == null) {
logger.debug("No optional poster image set");
} else {
logger.debug("Poster image found at '{}'", posterImgUri);
}
// Read target flavor
String targetFlavor = operation.getConfiguration(TARGET_FLAVOR);
if (StringUtils.isBlank(targetFlavor)) {
logger.warn("Required configuration key '{}' is blank", TARGET_FLAVOR);
throw new WorkflowOperationException("Configuration key '" + TARGET_FLAVOR + "' must be set");
}
try {
MediaPackageElementFlavor.parseFlavor(targetFlavor);
} catch (IllegalArgumentException e) {
logger.warn("Given target flavor '{}' is not a valid flavor", targetFlavor);
throw new WorkflowOperationException(e);
}
Job generate;
try {
generate = getCoverImageService().generateCoverImage(xml, xsl, String.valueOf(width), String.valueOf(height),
posterImgUri, targetFlavor);
logger.debug("Job for cover image generation created");
if (!waitForStatus(generate).isSuccess()) {
throw new WorkflowOperationException("'Cover image' job did not successfuly end");
}
generate = serviceRegistry.getJob(generate.getId());
Attachment coverImage = (Attachment) MediaPackageElementParser.getFromXml(generate.getPayload());
URI attachmentUri = getWorkspace().moveTo(coverImage.getURI(), mediaPackage.getIdentifier().compact(),
UUID.randomUUID().toString(), COVERIMAGE_FILENAME);
coverImage.setURI(attachmentUri);
coverImage.setMimeType(MimeTypes.PNG);
// Add tags
final String targetTags = StringUtils.trimToNull(operation.getConfiguration(TARGET_TAGS));
if (targetTags != null) {
for (String tag : asList(targetTags)) {
logger.trace("Tagging image with '{}'", tag);
if (StringUtils.trimToNull(tag) != null)
coverImage.addTag(tag);
}
}
mediaPackage.add(coverImage);
} catch (MediaPackageException e) {
throw new WorkflowOperationException(e);
} catch (NotFoundException e) {
throw new WorkflowOperationException(e);
} catch (ServiceRegistryException e) {
throw new WorkflowOperationException(e);
} catch (CoverImageException e) {
throw new WorkflowOperationException(e);
} catch (IllegalArgumentException e) {
throw new WorkflowOperationException(e);
} catch (IOException e) {
throw new WorkflowOperationException(e);
}
logger.info("Cover Image Workflow finished successfully for media package '{}' within {}ms",
mediaPackage.getIdentifier(), generate.getQueueTime());
return createResult(mediaPackage, Action.CONTINUE, generate.getQueueTime());
}
protected String getPosterImageFileUrl(MediaPackage mediaPackage, String posterimageFlavor)
throws WorkflowOperationException {
if (posterimageFlavor == null) {
logger.debug("Optional configuration key '{}' not set", POSTERIMAGE_FLAVOR);
return null;
}
MediaPackageElementFlavor flavor;
try {
flavor = MediaPackageElementFlavor.parseFlavor(posterimageFlavor);
} catch (IllegalArgumentException e) {
logger.warn("'{}' is not a valid flavor", posterimageFlavor);
throw new WorkflowOperationException(e);
}
Attachment[] atts = mediaPackage.getAttachments(flavor);
if (atts.length > 1) {
logger.warn("More than one attachment with the flavor '{}' found in media package '{}'", posterimageFlavor,
mediaPackage.getIdentifier());
throw new WorkflowOperationException("More than one attachment with the flavor'" + posterimageFlavor + "' found.");
} else if (atts.length == 0) {
logger.warn("No attachment with the flavor '{}' found in media package '{}'", posterimageFlavor,
mediaPackage.getIdentifier());
return null;
}
try {
return getWorkspace().get(atts[0].getURI()).getAbsolutePath();
} catch (NotFoundException e) {
throw new WorkflowOperationException(e);
} catch (IOException e) {
throw new WorkflowOperationException(e);
}
}
protected String getPosterImageFileUrl(String posterimageUrlOpt) {
if (StringUtils.isBlank(posterimageUrlOpt))
return null;
URL url = null;
try {
url = new URL(posterimageUrlOpt);
} catch (Exception e) {
logger.debug("Given poster image URI '{}' is not valid", posterimageUrlOpt);
}
if (url == null)
return null;
if ("file".equals(url.getProtocol()))
return url.toExternalForm();
try {
File coverImageFile = getWorkspace().get(url.toURI());
return coverImageFile.getPath();
} catch (NotFoundException e) {
logger.warn("Poster image could not be found at '{}'", url);
return null;
} catch (IOException e) {
logger.warn("Error getting poster image: {}", e.getMessage());
return null;
} catch (URISyntaxException e) {
logger.warn("Given URL '{}' is not a valid URI", url);
return null;
}
}
protected int getIntConfiguration(WorkflowOperationInstance operation, String key) throws WorkflowOperationException {
String confString = operation.getConfiguration(key);
int confValue = 0;
if (StringUtils.isBlank(confString))
throw new WorkflowOperationException("Configuration key '" + key + "' must be set");
try {
confValue = Integer.parseInt(confString);
if (confValue < 1)
throw new WorkflowOperationException("Configuration key '" + key
+ "' must be set to a valid positive integer value");
} catch (NumberFormatException e) {
throw new WorkflowOperationException("Configuration key '" + key
+ "' must be set to a valid positive integer value");
}
return confValue;
}
protected String loadXsl(WorkflowOperationInstance operation) throws WorkflowOperationException {
String xslUriString = operation.getConfiguration(XSL_FILE_URL);
if (StringUtils.isBlank(xslUriString))
throw new WorkflowOperationException("Configuration option '" + XSL_FILE_URL + "' must not be empty");
FileReader reader = null;
try {
URI xslUri = new URI(xslUriString);
File xslFile = new File(xslUri);
reader = new FileReader(xslFile);
return IOUtils.toString(reader);
} catch (FileNotFoundException e) {
logger.warn("There is no (xsl) file at the given uri '{}': {}", xslUriString, e.getMessage());
throw new WorkflowOperationException("There is no (XSL) file at the given URI", e);
} catch (URISyntaxException e) {
logger.warn("Given XSL file URI ({}) is not valid: {}", xslUriString, e.getMessage());
throw new WorkflowOperationException("Given XSL file URI is not valid", e);
} catch (IOException e) {
logger.warn("Error while reading XSL file ({}): {}", xslUriString, e.getMessage());
throw new WorkflowOperationException("Error while reading XSL file", e);
} finally {
IOUtils.closeQuietly(reader);
}
}
protected String getMetadataXml(MediaPackage mp) {
StaticMetadata metadata = getStaticMetadataService().getMetadata(mp);
StringBuilder xml = new StringBuilder();
xml.append("<metadata xmlns:dcterms=\"http://purl.org/dc/terms/\">");
for (String title : getFirstMetadataValue(metadata.getTitles()))
appendXml(xml, "title", title);
for (String description : getFirstMetadataValue(metadata.getDescription()))
appendXml(xml, "description", description);
for (String language : metadata.getLanguage())
appendXml(xml, "language", language);
for (Date created : metadata.getCreated())
/* Method formatDate of org.apache.xalan.lib.ExsltDatetime requires the format CCYY-MM-DDThh:mm:ss */
appendXml(xml, "date", new SimpleDateFormat("YYYY-MM-dd'T'HH:mm:ss").format(created));
for (Date[] period : metadata.getTemporalPeriod()) {
if (period[0] != null) {
appendXml(xml, "start", new SimpleDateFormat("YYYY-MM-dd'T'HH:mm:ss").format(period[0]));
}
if (period[1] != null) {
appendXml(xml, "end", new SimpleDateFormat("YYYY-MM-dd'T'HH:mm:ss").format(period[1]));
}
}
for (Date instant : metadata.getTemporalInstant())
appendXml(xml, "start", new SimpleDateFormat("YYYY-MM-dd'T'HH:mm:ss").format(instant));
for (Long duration : metadata.getTemporalDuration())
appendXml(xml, "duration", new SimpleDateFormat("HH:mm:ss").format(new Date(duration)));
for (String license : getFirstMetadataValue(metadata.getLicenses()))
appendXml(xml, "license", license);
for (String isPartOf : metadata.getIsPartOf())
appendXml(xml, "series", isPartOf);
for (String contributors : getFirstMetadataValue(metadata.getContributors()))
appendXml(xml, "contributors", contributors);
for (String creators : getFirstMetadataValue(metadata.getCreators()))
appendXml(xml, "creators", creators);
for (String subjects : getFirstMetadataValue(metadata.getSubjects()))
appendXml(xml, "subjects", subjects);
xml.append("</metadata>");
return xml.toString();
}
protected Option<String> getFirstMetadataValue(List<MetadataValue<String>> list) {
for (MetadataValue<String> data : list) {
if (DublinCore.LANGUAGE_UNDEFINED.equals(data.getLanguage()))
return Option.some(data.getValue());
}
return Option.<String> none();
}
protected void appendXml(StringBuilder xml, String name, String body) {
if (StringUtils.isBlank(body) || StringUtils.isBlank(name))
return;
xml.append("<");
xml.append(name);
xml.append(">");
xml.append(StringEscapeUtils.escapeXml(body));
xml.append("</");
xml.append(name);
xml.append(">");
}
}