CoverImageWorkflowOperationHandlerBase.java
/*
* 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.Catalog;
import org.opencastproject.mediapackage.EName;
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.StaticMetadataService;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
import org.opencastproject.metadata.dublincore.DublinCoreUtil;
import org.opencastproject.metadata.dublincore.DublinCoreValue;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.util.MimeTypes;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
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.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* Base implementation of the cover image workflow operation handler
*/
public abstract class CoverImageWorkflowOperationHandlerBase extends AbstractWorkflowOperationHandler {
private static final String EPISODE_FLAVOR = "episodeFlavor";
private static final String SERIES_FLAVOR = "seriesFlavor";
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";
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(CoverImageWorkflowOperationHandlerBase.class);
/** 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();
/** Returns value of karaf.etc */
protected abstract String getKarafEtc();
@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, operation);
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);
}
//Get tags and flavors
ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(workflowInstance,
Configuration.none, Configuration.none, Configuration.many, Configuration.one);
// Read target flavor
MediaPackageElementFlavor targetFlavor = tagsAndFlavors.getSingleTargetFlavor();
Job generate;
try {
generate = getCoverImageService().generateCoverImage(xml, xsl, String.valueOf(width), String.valueOf(height),
posterImgUri, targetFlavor.toString());
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().toString(),
UUID.randomUUID().toString(), COVERIMAGE_FILENAME);
coverImage.setURI(attachmentUri);
coverImage.setMimeType(MimeTypes.PNG);
// Add tags
applyTargetTagsToElement(tagsAndFlavors.getTargetTags(), coverImage);
mediaPackage.add(coverImage);
} catch (MediaPackageException | NotFoundException | ServiceRegistryException | CoverImageException
| IllegalArgumentException | 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 | 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 (getKarafEtc() != null) {
xslUriString = xslUriString.replace("${karaf.etc}", getKarafEtc());
}
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, WorkflowOperationInstance operation) {
//get specified episode/series flavor
final String configuredEpisodeFlavor =
Objects.toString(StringUtils.trimToNull(operation.getConfiguration(EPISODE_FLAVOR)),
"dublincore/episode");
final String configuredSeriesFlavor =
Objects.toString(StringUtils.trimToNull(operation.getConfiguration(SERIES_FLAVOR)),
"dublincore/series");
MediaPackageElementFlavor episodeFlavor = MediaPackageElementFlavor.parseFlavor(configuredEpisodeFlavor);
MediaPackageElementFlavor seriesFlavor = MediaPackageElementFlavor.parseFlavor(configuredSeriesFlavor);
//Get episode metadata-catalog
Catalog[] catalogs =
mp.getCatalogs(new MediaPackageElementFlavor(episodeFlavor.getType(),
StringUtils.lowerCase(episodeFlavor.getSubtype())));
//load metadata-catalog
DublinCoreCatalog dc = DublinCoreUtil.loadDublinCore(getWorkspace(), catalogs[0]);
Map<EName, List<DublinCoreValue>> data = dc.getValues();
//build xml from metadata
StringBuilder xml = new StringBuilder();
xml.append("<metadata xmlns:dcterms=\"http://purl.org/dc/terms/\">");
for (Map.Entry<EName, List<DublinCoreValue>> entry : data.entrySet()) {
String currentKey = entry.getKey().getLocalName();
switch(currentKey) {
case "creator":
appendXml(xml, "creators", getValuesAsString(entry));
break;
case "isPartOf":
//get series catalog
Catalog[] seriesCatalogs =
mp.getCatalogs(new MediaPackageElementFlavor(seriesFlavor.getType(), seriesFlavor.getSubtype()));
if (seriesCatalogs.length == 0) {
continue;
}
xml.append("<series>");
//get Series metadata
DublinCoreCatalog dcSeries = DublinCoreUtil.loadDublinCore(getWorkspace(), seriesCatalogs[0]);
Map<EName, List<DublinCoreValue>> seriesMetadata = dcSeries.getValues();
//append series metadata
for (Map.Entry<EName, List<DublinCoreValue>> seriesEntry : seriesMetadata.entrySet()) {
String currentSeriesKey = seriesEntry.getKey().getLocalName();
switch(currentSeriesKey) {
case "created":
String[] date = seriesEntry.getValue().get(0).getValue().split("\\.");
appendXml(xml, "date", date[0]);
break;
case "contributor":
appendXml(xml, "contributors", getValuesAsString(seriesEntry));
break;
default: String key = seriesEntry.getKey().getLocalName();
appendXml(xml, key, getValuesAsString(seriesEntry));
}
}
xml.append("</series>");
break;
case "temporal":
String[] entries = entry.getValue().get(0).getValue().split(";");
entries[0] = entries[0].trim().substring(6);
entries[1] = entries[1].trim().substring(4);
if (entries[0] != null) {
appendXml(xml, "start", entries[0]);
}
if (entries[1] != null) {
appendXml(xml, "end", entries[1]);
}
break;
case "created":
String[] date = entry.getValue().get(0).getValue().split("\\.");
appendXml(xml, "date", date[0]);
break;
case "contributor":
appendXml(xml, "contributors", getValuesAsString(entry));
break;
default: appendXml(xml, entry.getKey().getLocalName(), getValuesAsString(entry));
}
}
xml.append("</metadata>");
return xml.toString();
}
protected String getValuesAsString(Map.Entry<EName, List<DublinCoreValue>> entry) {
List<DublinCoreValue> values = entry.getValue();
String stringValues = "";
try {
stringValues += values.get(0).getValue();
for (int i = 1; i < values.size(); i++) {
stringValues += ", " + values.get(i).getValue();
}
} catch (IndexOutOfBoundsException e) {
logger.warn("Given Key '{}' has no Entries : {}", entry.getKey(), e.getMessage());
}
return stringValues;
}
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(">");
}
}