/**
* 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.themes;
import static java.lang.String.format;
import static org.opencastproject.composer.layout.Offset.offset;
import org.opencastproject.composer.layout.AbsolutePositionLayoutSpec;
import org.opencastproject.composer.layout.AnchorOffset;
import org.opencastproject.composer.layout.Anchors;
import org.opencastproject.composer.layout.Serializer;
import org.opencastproject.job.api.JobContext;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElement.Type;
import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.series.api.SeriesException;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.staticfiles.api.StaticFileService;
import org.opencastproject.themes.Theme;
import org.opencastproject.themes.ThemesServiceDatabase;
import org.opencastproject.themes.persistence.ThemesServiceDatabaseException;
import org.opencastproject.util.MimeType;
import org.opencastproject.util.MimeTypes;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.UnknownFileTypeException;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
import org.opencastproject.workflow.api.WorkflowOperationResult;
import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
import org.opencastproject.workspace.api.Workspace;
import com.entwinemedia.fn.Fn;
import com.entwinemedia.fn.Stream;
import com.entwinemedia.fn.data.Opt;
import com.entwinemedia.fn.fns.Strings;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
/**
* The workflow definition for handling "theme" operations
*/
public class ThemeWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
private static final String BUMPER_FLAVOR = "bumper-flavor";
private static final String BUMPER_TAGS = "bumper-tags";
private static final String TRAILER_FLAVOR = "trailer-flavor";
private static final String TRAILER_TAGS = "trailer-tags";
private static final String TITLE_SLIDE_FLAVOR = "title-slide-flavor";
private static final String TITLE_SLIDE_TAGS = "title-slide-tags";
private static final String LICENSE_SLIDE_FLAVOR = "license-slide-flavor";
private static final String LICENSE_SLIDE_TAGS = "license-slide-tags";
private static final String WATERMARK_FLAVOR = "watermark-flavor";
private static final String WATERMARK_TAGS = "watermark-tags";
private static final String WATERMARK_LAYOUT = "watermark-layout";
private static final String WATERMARK_LAYOUT_VARIABLE = "watermark-layout-variable";
/** Workflow property names */
private static final String THEME_ACTIVE = "theme_active";
private static final String THEME_BUMPER_ACTIVE = "theme_bumper_active";
private static final String THEME_TRAILER_ACTIVE = "theme_trailer_active";
private static final String THEME_TITLE_SLIDE_ACTIVE = "theme_title_slide_active";
private static final String THEME_TITLE_SLIDE_UPLOADED = "theme_title_slide_uploaded";
private static final String THEME_WATERMARK_ACTIVE = "theme_watermark_active";
/** The series theme property name */
private static final String THEME_PROPERTY_NAME = "theme";
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(ThemeWorkflowOperationHandler.class);
/** The configuration options for this handler */
private static final SortedMap<String, String> CONFIG_OPTIONS;
private static final MediaPackageElementBuilderFactory elementBuilderFactory = MediaPackageElementBuilderFactory
.newInstance();
static {
CONFIG_OPTIONS = new TreeMap<String, String>();
CONFIG_OPTIONS.put(BUMPER_FLAVOR, "The flavor to apply to the added bumper element");
CONFIG_OPTIONS.put(BUMPER_TAGS, "The tags to apply to the added bumper element");
CONFIG_OPTIONS.put(TRAILER_FLAVOR, "The flavor to apply to the added trailer element");
CONFIG_OPTIONS.put(TRAILER_TAGS, "The tags to apply to the added trailer element");
CONFIG_OPTIONS.put(TITLE_SLIDE_FLAVOR, "The flavor to apply to the added title slide element");
CONFIG_OPTIONS.put(TITLE_SLIDE_TAGS, "The tags to apply to the added title slide element");
CONFIG_OPTIONS.put(LICENSE_SLIDE_FLAVOR, "The flavor to apply to the added license slide element");
CONFIG_OPTIONS.put(LICENSE_SLIDE_TAGS, "The tags to apply to the added license slide element");
CONFIG_OPTIONS.put(WATERMARK_FLAVOR, "The flavor to apply to the added watermark element");
CONFIG_OPTIONS.put(WATERMARK_TAGS, "The tags to apply to the added watermark element");
CONFIG_OPTIONS.put(WATERMARK_LAYOUT, "The layout to adjust by the watermark position");
CONFIG_OPTIONS.put(WATERMARK_LAYOUT_VARIABLE, "The workflow variable where the adjusted layout is stored");
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.WorkflowOperationHandler#getConfigurationOptions()
*/
@Override
public SortedMap<String, String> getConfigurationOptions() {
return CONFIG_OPTIONS;
}
/** The series service */
private SeriesService seriesService;
/** The themes database service */
private ThemesServiceDatabase themesServiceDatabase;
/** The static file service */
private StaticFileService staticFileService;
/** The workspace */
private Workspace workspace;
/** OSGi callback for the series service. */
public void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
/** OSGi callback for the themes database service. */
public void setThemesServiceDatabase(ThemesServiceDatabase themesServiceDatabase) {
this.themesServiceDatabase = themesServiceDatabase;
}
/** OSGi callback for the static file service. */
public void setStaticFileService(StaticFileService staticFileService) {
this.staticFileService = staticFileService;
}
/** OSGi callback for the workspace. */
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(org.opencastproject.workflow.api.WorkflowInstance,
* JobContext)
*/
@Override
public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
throws WorkflowOperationException {
logger.debug("Running theme workflow operation on workflow {}", workflowInstance.getId());
final MediaPackageElementFlavor bumperFlavor = getOptConfig(workflowInstance, BUMPER_FLAVOR).map(
toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "bumper"));
final MediaPackageElementFlavor trailerFlavor = getOptConfig(workflowInstance, TRAILER_FLAVOR).map(
toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "trailer"));
final MediaPackageElementFlavor titleSlideFlavor = getOptConfig(workflowInstance, TITLE_SLIDE_FLAVOR).map(
toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "title-slide"));
final MediaPackageElementFlavor licenseSlideFlavor = getOptConfig(workflowInstance, LICENSE_SLIDE_FLAVOR).map(
toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "license-slide"));
final MediaPackageElementFlavor watermarkFlavor = getOptConfig(workflowInstance, WATERMARK_FLAVOR).map(
toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "watermark"));
final List<String> bumperTags = asList(workflowInstance.getConfiguration(BUMPER_TAGS));
final List<String> trailerTags = asList(workflowInstance.getConfiguration(TRAILER_TAGS));
final List<String> titleSlideTags = asList(workflowInstance.getConfiguration(TITLE_SLIDE_TAGS));
final List<String> licenseSlideTags = asList(workflowInstance.getConfiguration(LICENSE_SLIDE_TAGS));
final List<String> watermarkTags = asList(workflowInstance.getConfiguration(WATERMARK_TAGS));
Opt<String> layoutStringOpt = getOptConfig(workflowInstance, WATERMARK_LAYOUT);
Opt<String> watermarkLayoutVariable = getOptConfig(workflowInstance, WATERMARK_LAYOUT_VARIABLE);
List<String> layoutList = new ArrayList<>(Stream.$(layoutStringOpt).bind(Strings.split(";")).toList());
try {
MediaPackage mediaPackage = workflowInstance.getMediaPackage();
String series = mediaPackage.getSeries();
if (series == null) {
logger.info("Skipping theme workflow operation, no series assigned to mediapackage {}",
mediaPackage.getIdentifier());
return createResult(Action.SKIP);
}
Long themeId;
try {
themeId = Long.parseLong(seriesService.getSeriesProperty(series, THEME_PROPERTY_NAME));
} catch (NotFoundException e) {
logger.info("Skipping theme workflow operation, no theme assigned to series {} on mediapackage {}.", series,
mediaPackage.getIdentifier());
return createResult(Action.SKIP);
} catch (UnauthorizedException e) {
logger.warn("Skipping theme workflow operation, user not authorized to perform operation: {}",
ExceptionUtils.getStackTrace(e));
return createResult(Action.SKIP);
}
Theme theme;
try {
theme = themesServiceDatabase.getTheme(themeId);
} catch (NotFoundException e) {
logger.warn("Skipping theme workflow operation, no theme with id {} found.", themeId);
return createResult(Action.SKIP);
}
logger.info("Applying theme {} to mediapackage {}", themeId, mediaPackage.getIdentifier());
/* Make theme settings available to workflow instance */
workflowInstance.setConfiguration(THEME_ACTIVE, Boolean.toString(
theme.isBumperActive()
|| theme.isTrailerActive()
|| theme.isTitleSlideActive()
|| theme.isWatermarkActive()
)
);
workflowInstance.setConfiguration(THEME_BUMPER_ACTIVE, Boolean.toString(theme.isBumperActive()));
workflowInstance.setConfiguration(THEME_TRAILER_ACTIVE, Boolean.toString(theme.isTrailerActive()));
workflowInstance.setConfiguration(THEME_TITLE_SLIDE_ACTIVE, Boolean.toString(theme.isTitleSlideActive()));
workflowInstance.setConfiguration(THEME_TITLE_SLIDE_UPLOADED, Boolean.toString(StringUtils.isNotBlank(theme.getTitleSlideBackground())));
workflowInstance.setConfiguration(THEME_WATERMARK_ACTIVE, Boolean.toString(theme.isWatermarkActive()));
if (theme.isBumperActive() && StringUtils.isNotBlank(theme.getBumperFile())) {
try (InputStream bumper = staticFileService.getFile(theme.getBumperFile())) {
addElement(mediaPackage, bumperFlavor, bumperTags, bumper,
staticFileService.getFileName(theme.getBumperFile()), Type.Track);
} catch (NotFoundException e) {
logger.warn("Bumper file {} not found in static file service, skip applying it", theme.getBumperFile());
}
}
if (theme.isTrailerActive() && StringUtils.isNotBlank(theme.getTrailerFile())) {
try (InputStream trailer = staticFileService.getFile(theme.getTrailerFile())) {
addElement(mediaPackage, trailerFlavor, trailerTags, trailer,
staticFileService.getFileName(theme.getTrailerFile()), Type.Track);
} catch (NotFoundException e) {
logger.warn("Trailer file {} not found in static file service, skip applying it", theme.getTrailerFile());
}
}
if (theme.isTitleSlideActive()) {
if (StringUtils.isNotBlank(theme.getTitleSlideBackground())) {
try (InputStream titleSlideBackground = staticFileService.getFile(theme.getTitleSlideBackground())) {
addElement(mediaPackage, titleSlideFlavor, titleSlideTags, titleSlideBackground,
staticFileService.getFileName(theme.getTitleSlideBackground()), Type.Attachment);
} catch (NotFoundException e) {
logger.warn("Title slide file {} not found in static file service, skip applying it",
theme.getTitleSlideBackground());
}
}
// TODO add the title slide metadata to the workflow properties to be used by the cover-image WOH
// String titleSlideMetadata = theme.getTitleSlideMetadata();
}
if (theme.isLicenseSlideActive()) {
if (StringUtils.isNotBlank(theme.getLicenseSlideBackground())) {
try (InputStream licenseSlideBackground = staticFileService.getFile(theme.getLicenseSlideBackground())) {
addElement(mediaPackage, licenseSlideFlavor, licenseSlideTags, licenseSlideBackground,
staticFileService.getFileName(theme.getLicenseSlideBackground()), Type.Attachment);
} catch (NotFoundException e) {
logger.warn("License slide file {} not found in static file service, skip applying it",
theme.getLicenseSlideBackground());
}
} else {
// TODO define what to do here (maybe extract image as background)
}
// TODO add the license slide description to the workflow properties to be used by the cover-image WOH
// String licenseSlideDescription = theme.getLicenseSlideDescription();
}
if (theme.isWatermarkActive() && StringUtils.isNotBlank(theme.getWatermarkFile())) {
try (InputStream watermark = staticFileService.getFile(theme.getWatermarkFile())) {
addElement(mediaPackage, watermarkFlavor, watermarkTags, watermark,
staticFileService.getFileName(theme.getWatermarkFile()), Type.Attachment);
} catch (NotFoundException e) {
logger.warn("Watermark file {} not found in static file service, skip applying it", theme.getWatermarkFile());
}
if (layoutStringOpt.isNone() || watermarkLayoutVariable.isNone())
throw new WorkflowOperationException(format("Configuration key '%s' or '%s' is either missing or empty",
WATERMARK_LAYOUT, WATERMARK_LAYOUT_VARIABLE));
AbsolutePositionLayoutSpec watermarkLayout = parseLayout(theme.getWatermarkPosition());
layoutList.set(layoutList.size() - 1, Serializer.json(watermarkLayout).toJson());
layoutStringOpt = Opt.some(Stream.$(layoutList).mkString(";"));
}
if (watermarkLayoutVariable.isSome() && layoutStringOpt.isSome())
workflowInstance.setConfiguration(watermarkLayoutVariable.get(), layoutStringOpt.get());
return createResult(mediaPackage, Action.CONTINUE);
} catch (SeriesException | ThemesServiceDatabaseException | IllegalStateException | IllegalArgumentException
| IOException e) {
throw new WorkflowOperationException(e);
}
}
private AbsolutePositionLayoutSpec parseLayout(String watermarkPosition) {
switch (watermarkPosition) {
case "topLeft":
return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.TOP_LEFT, Anchors.TOP_LEFT, offset(20, 20)));
case "topRight":
return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.TOP_RIGHT, Anchors.TOP_RIGHT, offset(-20, 20)));
case "bottomLeft":
return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.BOTTOM_LEFT, Anchors.BOTTOM_LEFT,
offset(20, -20)));
case "bottomRight":
return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.BOTTOM_RIGHT, Anchors.BOTTOM_RIGHT, offset(-20,
-20)));
default:
throw new IllegalStateException("Unknown watermark position: " + watermarkPosition);
}
}
private void addElement(MediaPackage mediaPackage, final MediaPackageElementFlavor flavor, final List<String> tags,
InputStream file, String filename, Type type) throws IOException {
MediaPackageElement element = elementBuilderFactory.newElementBuilder().newElement(type, flavor);
element.setIdentifier(UUID.randomUUID().toString());
for (String tag : tags) {
element.addTag(tag);
}
URI uri = workspace.put(mediaPackage.getIdentifier().compact(), element.getIdentifier(), filename, file);
element.setURI(uri);
try {
MimeType mimeType = MimeTypes.fromString(filename);
element.setMimeType(mimeType);
} catch (UnknownFileTypeException e) {
logger.warn("Unable to detect the mime type of file {}", filename);
}
mediaPackage.add(element);
}
private static Fn<String, MediaPackageElementFlavor> toMediaPackageElementFlavor = new Fn<String, MediaPackageElementFlavor>() {
@Override
public MediaPackageElementFlavor apply(String flavorString) {
return MediaPackageElementFlavor.parseFlavor(flavorString);
}
};
}