/** * 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.distribution; import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace; import org.opencastproject.distribution.api.DistributionException; import org.opencastproject.distribution.api.DownloadDistributionService; import org.opencastproject.job.api.Job; import org.opencastproject.job.api.JobContext; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.mediapackage.Publication; import org.opencastproject.mediapackage.PublicationImpl; import org.opencastproject.mediapackage.selector.SimpleElementSelector; import org.opencastproject.security.api.SecurityService; import org.opencastproject.util.MimeType; import org.opencastproject.util.MimeTypes; import org.opencastproject.util.RequireUtil; import org.opencastproject.util.doc.DocUtil; 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.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; /** * WOH that distributes selected elements to an internal distribution channel and adds reflective publication elements * to the media package. */ public class ConfigurablePublishWorkflowOperationHandler extends ConfigurableWorkflowOperationHandlerBase { /** The logging facility */ private static final Logger logger = LoggerFactory.getLogger(ConfigurablePublishWorkflowOperationHandler.class); /** The template key for adding the mediapackage / event id to the publication path. */ protected static final String EVENT_ID_TEMPLATE_KEY = "event_id"; /** The template key for adding the player location path to the publication path. */ protected static final String PLAYER_PATH_TEMPLATE_KEY = "player_path"; /** The template key for adding the publication id to the publication path. */ protected static final String PUBLICATION_ID_TEMPLATE_KEY = "publication_id"; /** The template key for adding the series id to the publication path. */ protected static final String SERIES_ID_TEMPLATE_KEY = "series_id"; /** The configuration property value for the player location. */ protected static final String PLAYER_PROPERTY = "player"; // service references private DownloadDistributionService distributionService; /** Workflow configuration options */ static final String CHANNEL_ID_KEY = "channel-id"; static final String MIME_TYPE = "mimetype"; static final String SOURCE_TAGS = "source-tags"; static final String SOURCE_FLAVORS = "source-flavors"; static final String WITH_PUBLISHED_ELEMENTS = "with-published-elements"; static final String CHECK_AVAILABILITY = "check-availability"; static final String STRATEGY = "strategy"; static final String MODE = "mode"; /** Known values for mode **/ static final String MODE_SINGLE = "single"; static final String MODE_MIXED = "mixed"; static final String MODE_BULK = "bulk"; static final String[] KNOWN_MODES = { MODE_SINGLE, MODE_MIXED, MODE_BULK }; static final String DEFAULT_MODE = MODE_BULK; /** The workflow configuration key for defining the url pattern. */ static final String URL_PATTERN = "url-pattern"; private SecurityService securityService; /** OSGi DI */ void setDownloadDistributionService(DownloadDistributionService distributionService) { this.distributionService = distributionService; } /** OSGi DI */ protected void setSecurityService(SecurityService securityService) { this.securityService = securityService; } @Override protected DownloadDistributionService getDistributionService() { assert (distributionService != null); return distributionService; } /** * Replace possible variables in the url-pattern configuration for this workflow operation handler. * * @param urlPattern * The operation's template for replacing the variables. * @param mp * The {@link MediaPackage} used to get the event / mediapackage id. * @param pubUUID * The UUID for the published element. * @return The URI of the published element with the variables replaced. * @throws WorkflowOperationException * Thrown if the URI is malformed after replacing the variables. */ public URI populateUrlWithVariables(String urlPattern, MediaPackage mp, String pubUUID) throws WorkflowOperationException { Map<String, Object> values = new HashMap<String, Object>(); values.put(EVENT_ID_TEMPLATE_KEY, mp.getIdentifier().compact()); values.put(PUBLICATION_ID_TEMPLATE_KEY, pubUUID); String playerPath = securityService.getOrganization().getProperties().get(PLAYER_PROPERTY); values.put(PLAYER_PATH_TEMPLATE_KEY, playerPath); values.put(SERIES_ID_TEMPLATE_KEY, StringUtils.trimToEmpty(mp.getSeries())); String uriWithVariables = DocUtil.processTextTemplate("Replacing Variables in Publish URL", urlPattern, values); URI publicationURI; try { publicationURI = new URI(uriWithVariables); } catch (URISyntaxException e) { throw new WorkflowOperationException(String.format( "Unable to create URI from template '%s', replacement was: '%s'", urlPattern, uriWithVariables), e); } return publicationURI; } @Override public WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context) throws WorkflowOperationException { RequireUtil.notNull(workflowInstance, "workflowInstance"); final MediaPackage mp = workflowInstance.getMediaPackage(); final WorkflowOperationInstance op = workflowInstance.getCurrentOperation(); final String channelId = StringUtils.trimToEmpty(op.getConfiguration(CHANNEL_ID_KEY)); if ("".equals(channelId)) { throw new WorkflowOperationException("Unable to publish this mediapackage as the configuration key " + CHANNEL_ID_KEY + " is missing. Unable to determine where to publish these elements."); } final String urlPattern = StringUtils.trimToEmpty(op.getConfiguration(URL_PATTERN)); MimeType mimetype = null; String mimetypeString = StringUtils.trimToEmpty(op.getConfiguration(MIME_TYPE)); if (!"".equals(mimetypeString)) { try { mimetype = MimeTypes.parseMimeType(mimetypeString); } catch (IllegalArgumentException e) { throw new WorkflowOperationException("Unable to parse the provided configuration for " + MIME_TYPE, e); } } final boolean withPublishedElements = BooleanUtils.toBoolean(StringUtils.trimToEmpty( op.getConfiguration(WITH_PUBLISHED_ELEMENTS))); boolean checkAvailability = BooleanUtils.toBoolean(StringUtils.trimToEmpty( op.getConfiguration(CHECK_AVAILABILITY))); if (getPublications(mp, channelId).size() > 0) { final String rePublishStrategy = StringUtils.trimToEmpty(op.getConfiguration(STRATEGY)); switch (rePublishStrategy) { case ("fail"): //fail is a dummy function for further distribution strategies fail(mp); break; case ("merge"): // nothing to do here. other publication strategies can be added to this list later on break; default: retract(mp, channelId); } } String mode = StringUtils.trimToEmpty(op.getConfiguration(MODE)); if ("".equals(mode)) { mode = DEFAULT_MODE; } else if (!ArrayUtils.contains(KNOWN_MODES, mode)) { logger.error("Unknown value for configuration key mode: '{}'", mode); throw new IllegalArgumentException("Unknown value for configuration key mode"); } final String[] sourceFlavors = StringUtils.split(StringUtils.trimToEmpty(op.getConfiguration(SOURCE_FLAVORS)), ","); final String[] sourceTags = StringUtils.split(StringUtils.trimToEmpty(op.getConfiguration(SOURCE_TAGS)), ","); String publicationUUID = UUID.randomUUID().toString(); Publication publication = PublicationImpl.publication(publicationUUID, channelId, null, null); // Configure the element selector final SimpleElementSelector selector = new SimpleElementSelector(); for (String flavor : sourceFlavors) { selector.addFlavor(MediaPackageElementFlavor.parseFlavor(flavor)); } for (String tag : sourceTags) { selector.addTag(tag); } if (sourceFlavors.length > 0 || sourceTags.length > 0) { if (!withPublishedElements) { Set<MediaPackageElement> elements = distribute(selector.select(mp, false), mp, channelId, mode, checkAvailability); if (elements.size() > 0) { for (MediaPackageElement element : elements) { // Make sure the mediapackage is prompted to create a new identifier for this element element.setIdentifier(null); PublicationImpl.addElementToPublication(publication, element); } } else { logger.info("No element found for distribution in media package '{}'", mp); return createResult(mp, Action.CONTINUE); } } else { List<MediaPackageElement> publishedElements = new ArrayList<MediaPackageElement>(); for (Publication alreadyPublished : mp.getPublications()) { publishedElements.addAll(Arrays.asList(alreadyPublished.getAttachments())); publishedElements.addAll(Arrays.asList(alreadyPublished.getCatalogs())); publishedElements.addAll(Arrays.asList(alreadyPublished.getTracks())); } for (MediaPackageElement element : selector.select(publishedElements, false)) { PublicationImpl.addElementToPublication(publication, element); } } } if (!"".equals(urlPattern)) { publication.setURI(populateUrlWithVariables(urlPattern, mp, publicationUUID)); } if (mimetype != null) { publication.setMimeType(mimetype); } mp.add(publication); return createResult(mp, Action.CONTINUE); } private Set<MediaPackageElement> distribute(Collection<MediaPackageElement> elements, MediaPackage mediapackage, String channelId, String mode, boolean checkAvailability) throws WorkflowOperationException { Set<MediaPackageElement> result = new HashSet<MediaPackageElement>(); Set<String> bulkElementIds = new HashSet<String>(); Set<String> singleElementIds = new HashSet<String>(); for (MediaPackageElement element : elements) { if (MODE_BULK.equals(mode) || (MODE_MIXED.equals(mode) && (element.getElementType() != MediaPackageElement.Type.Track))) { bulkElementIds.add(element.getIdentifier()); } else { singleElementIds.add(element.getIdentifier()); } } Set<Job> jobs = new HashSet<Job>(); if (bulkElementIds.size() > 0) { logger.info("Start bulk publishing of {} elements of media package '{}' to publication channel '{}'", new Object[] { bulkElementIds.size(), mediapackage, channelId }); try { Job job = distributionService.distribute(channelId, mediapackage, bulkElementIds, checkAvailability); jobs.add(job); } catch (DistributionException | MediaPackageException e) { logger.error("Creating the distribution job for {} elements of media package '{}' failed: {}", new Object[] { bulkElementIds.size(), mediapackage, getStackTrace(e) }); throw new WorkflowOperationException(e); } } if (singleElementIds.size() > 0) { logger.info("Start single publishing of {} elements of media package '{}' to publication channel '{}'", new Object[] { singleElementIds.size(), mediapackage, channelId }); for (String elementId : singleElementIds) { try { Job job = distributionService.distribute(channelId, mediapackage, elementId, checkAvailability); jobs.add(job); } catch (DistributionException | MediaPackageException e) { logger.error("Creating the distribution job for element '{}' of media package '{}' failed: {}", new Object[] { elementId, mediapackage, getStackTrace(e) }); throw new WorkflowOperationException(e); } } } if (jobs.size() > 0) { if (!waitForStatus(jobs.toArray(new Job[jobs.size()])).isSuccess()) { throw new WorkflowOperationException("At least one of the distribution jobs did not complete successfully"); } for (Job job : jobs) { try { List<? extends MediaPackageElement> elems = MediaPackageElementParser.getArrayFromXml(job.getPayload()); result.addAll(elems); } catch (MediaPackageException e) { logger.error("Job '{}' returned payload ({}) that could not be parsed to media package elements: {}", new Object[] { job, job.getPayload(), ExceptionUtils.getStackTrace(e) }); throw new WorkflowOperationException(e); } } logger.info("Published {} elements of media package {} to publication channel {}", bulkElementIds.size() + singleElementIds.size(), mediapackage, channelId); } return result; } /** * Dummy function for further publication strategies * @param mp * @throws WorkflowOperationException */ private void fail(MediaPackage mp) throws WorkflowOperationException { logger.error("There is already a Published Media, fail Stragy for Mediapackage {}", mp.getIdentifier()); throw new WorkflowOperationException("There is already a Published Media, fail Stragy for Mediapackage "); } }