org.opencastproject.workflow.handler.distribution.PublishEngageWorkflowOperationHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.opencastproject.workflow.handler.distribution.PublishEngageWorkflowOperationHandler.java

Source

/**
 * 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.lang.StringUtils.isBlank;
import static org.opencastproject.systems.MatterhornConstants.SERVER_URL_PROPERTY;
import static org.opencastproject.util.data.Option.option;
import static org.opencastproject.util.data.functions.Strings.toBool;
import static org.opencastproject.util.data.functions.Strings.trimToNone;
import static org.opencastproject.workflow.handler.distribution.EngagePublicationChannel.CHANNEL_ID;

import org.opencastproject.distribution.api.DistributionException;
import org.opencastproject.distribution.api.DistributionService;
import org.opencastproject.distribution.api.DownloadDistributionService;
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.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageReference;
import org.opencastproject.mediapackage.MediaPackageReferenceImpl;
import org.opencastproject.mediapackage.Publication;
import org.opencastproject.mediapackage.PublicationImpl;
import org.opencastproject.mediapackage.selector.SimpleElementSelector;
import org.opencastproject.search.api.SearchException;
import org.opencastproject.search.api.SearchQuery;
import org.opencastproject.search.api.SearchResult;
import org.opencastproject.search.api.SearchService;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.util.MimeTypes;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.UrlSupport;
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.apache.commons.lang.StringUtils;
import org.apache.http.client.utils.URIUtils;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
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.SortedMap;
import java.util.TreeMap;
import java.util.UUID;

/**
 * The workflow definition for handling "engage publication" operations
 */
public class PublishEngageWorkflowOperationHandler extends AbstractWorkflowOperationHandler {

    /** The logging facility */
    private static final Logger logger = LoggerFactory.getLogger(PublishEngageWorkflowOperationHandler.class);

    /** Configuration properties id */
    private static final String ENGAGE_URL_PROPERTY = "org.opencastproject.engage.ui.url";
    private static final String STREAMING_URL_PROPERTY = "org.opencastproject.streaming.url";

    /** Workflow configuration option keys */
    private static final String DOWNLOAD_SOURCE_FLAVORS = "download-source-flavors";
    private static final String DOWNLOAD_TARGET_SUBFLAVOR = "download-target-subflavor";
    private static final String DOWNLOAD_SOURCE_TAGS = "download-source-tags";
    private static final String DOWNLOAD_TARGET_TAGS = "download-target-tags";
    private static final String STREAMING_SOURCE_TAGS = "streaming-source-tags";
    private static final String STREAMING_TARGET_TAGS = "streaming-target-tags";
    private static final String STREAMING_SOURCE_FLAVORS = "streaming-source-flavors";
    private static final String STREAMING_TARGET_SUBFLAVOR = "streaming-target-subflavor";
    private static final String CHECK_AVAILABILITY = "check-availability";

    //itbwpdk start
    /** Distribution delay between elements for engage */
    private static final String DISTRIBUTION_DELAY__PROPERTY = "org.opencastproject.distribution.delay";

    /** Distribution delay between elements for engage */
    private int distributionDelay = 0;
    //itbwpdk end

    /** Workflow configuration option keys to only merge or overwrite element in exiting mediapackage */
    private static final String OPT_MERGE_ONLY = "merge-only";

    /** The streaming distribution service */
    private DistributionService streamingDistributionService = null;

    /** The download distribution service */
    private DownloadDistributionService downloadDistributionService = null;

    /** The search service */
    private SearchService searchService = null;

    /** The server url */
    private URL serverUrl;

    /** Whether to distribute to streaming server */
    private boolean distributeStreaming = false;

    /**
     * Callback for the OSGi declarative services configuration.
     *
     * @param streamingDistributionService
     *          the streaming distribution service
     */
    protected void setStreamingDistributionService(DistributionService streamingDistributionService) {
        this.streamingDistributionService = streamingDistributionService;
    }

    /**
     * Callback for the OSGi declarative services configuration.
     *
     * @param downloadDistributionService
     *          the download distribution service
     */
    protected void setDownloadDistributionService(DownloadDistributionService downloadDistributionService) {
        this.downloadDistributionService = downloadDistributionService;
    }

    /**
     * Callback for declarative services configuration that will introduce us to the search service. Implementation
     * assumes that the reference is configured as being static.
     *
     * @param searchService
     *          an instance of the search service
     */
    protected void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    /** The configuration options for this handler */
    private static final SortedMap<String, String> CONFIG_OPTIONS;

    static {
        CONFIG_OPTIONS = new TreeMap<String, String>();
        CONFIG_OPTIONS.put(DOWNLOAD_SOURCE_FLAVORS,
                "Distribute any mediapackage elements with one of these (comma separated) flavors to download");
        CONFIG_OPTIONS.put(STREAMING_TARGET_SUBFLAVOR,
                "Target subflavor for elements that have been distributed for downloads");
        CONFIG_OPTIONS.put(DOWNLOAD_SOURCE_TAGS,
                "Distribute any mediapackage elements with one of these (comma separated) tags to download.");
        CONFIG_OPTIONS.put(DOWNLOAD_TARGET_TAGS,
                "Add all of these comma separated tags to elements that have been distributed for download.");
        CONFIG_OPTIONS.put(STREAMING_SOURCE_FLAVORS,
                "Distribute any mediapackage elements with one of these (comma separated) flavors to streaming");
        CONFIG_OPTIONS.put(STREAMING_TARGET_SUBFLAVOR,
                "Target subflavor for elements that have been distributed for streaming");
        CONFIG_OPTIONS.put(STREAMING_SOURCE_TAGS,
                "Distribute any mediapackage elements with one of these (comma separated) tags to streaming.");
        CONFIG_OPTIONS.put(STREAMING_TARGET_TAGS,
                "Add all of these comma separated tags to elements that have been distributed for download.");
        CONFIG_OPTIONS.put(CHECK_AVAILABILITY,
                "( true | false ) defaults to true. Check if the distributed download artifact is available at its URL");
        CONFIG_OPTIONS.put(OPT_MERGE_ONLY,
                "Republish only if it can be merged with or replace existing published data");
    }

    @Override
    protected void activate(ComponentContext cc) {
        super.activate(cc);
        BundleContext bundleContext = cc.getBundleContext();

        // Get element distribution delay
        if (StringUtils.isNotBlank(bundleContext.getProperty(DISTRIBUTION_DELAY__PROPERTY))) {
            distributionDelay = Integer.parseInt(bundleContext.getProperty(DISTRIBUTION_DELAY__PROPERTY));
        } else {
            distributionDelay = 0;
        }

        serverUrl = UrlSupport.url(bundleContext.getProperty(SERVER_URL_PROPERTY));

        if (StringUtils.isNotBlank(bundleContext.getProperty(STREAMING_URL_PROPERTY)))
            distributeStreaming = true;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.opencastproject.workflow.api.WorkflowOperationHandler#getConfigurationOptions()
     */
    @Override
    public SortedMap<String, String> getConfigurationOptions() {
        return CONFIG_OPTIONS;
    }

    /**
     * {@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 engage publication workflow operation");

        MediaPackage mediaPackage = workflowInstance.getMediaPackage();
        WorkflowOperationInstance op = workflowInstance.getCurrentOperation();

        // Check which tags have been configured
        String downloadSourceTags = StringUtils.trimToEmpty(op.getConfiguration(DOWNLOAD_SOURCE_TAGS));
        String downloadTargetTags = StringUtils.trimToEmpty(op.getConfiguration(DOWNLOAD_TARGET_TAGS));
        String downloadSourceFlavors = StringUtils.trimToEmpty(op.getConfiguration(DOWNLOAD_SOURCE_FLAVORS));
        String downloadTargetSubflavor = StringUtils.trimToNull(op.getConfiguration(DOWNLOAD_TARGET_SUBFLAVOR));
        String streamingSourceTags = StringUtils.trimToEmpty(op.getConfiguration(STREAMING_SOURCE_TAGS));
        String streamingTargetTags = StringUtils.trimToEmpty(op.getConfiguration(STREAMING_TARGET_TAGS));
        String streamingSourceFlavors = StringUtils.trimToEmpty(op.getConfiguration(STREAMING_SOURCE_FLAVORS));
        String streamingTargetSubflavor = StringUtils.trimToNull(op.getConfiguration(STREAMING_TARGET_SUBFLAVOR));
        boolean checkAvailability = option(op.getConfiguration(CHECK_AVAILABILITY)).bind(trimToNone).map(toBool)
                .getOrElse(true);

        String[] sourceDownloadTags = StringUtils.split(downloadSourceTags, ",");
        String[] targetDownloadTags = StringUtils.split(downloadTargetTags, ",");
        String[] sourceDownloadFlavors = StringUtils.split(downloadSourceFlavors, ",");
        String[] sourceStreamingTags = StringUtils.split(streamingSourceTags, ",");
        String[] targetStreamingTags = StringUtils.split(streamingTargetTags, ",");
        String[] sourceStreamingFlavors = StringUtils.split(streamingSourceFlavors, ",");

        if (sourceDownloadTags.length == 0 && sourceDownloadFlavors.length == 0 && sourceStreamingTags.length == 0
                && sourceStreamingFlavors.length == 0) {
            logger.warn(
                    "No tags or flavors have been specified, so nothing will be published to the engage publication channel");
            return createResult(mediaPackage, Action.CONTINUE);
        }

        // Parse the download target flavor
        MediaPackageElementFlavor downloadSubflavor = null;
        if (downloadTargetSubflavor != null) {
            try {
                downloadSubflavor = MediaPackageElementFlavor.parseFlavor(downloadTargetSubflavor);
            } catch (IllegalArgumentException e) {
                throw new WorkflowOperationException(e);
            }
        }

        // Parse the streaming target flavor
        MediaPackageElementFlavor streamingSubflavor = null;
        if (streamingTargetSubflavor != null) {
            try {
                streamingSubflavor = MediaPackageElementFlavor.parseFlavor(streamingTargetSubflavor);
            } catch (IllegalArgumentException e) {
                throw new WorkflowOperationException(e);
            }
        }

        // Configure the download element selector
        SimpleElementSelector downloadElementSelector = new SimpleElementSelector();
        for (String flavor : sourceDownloadFlavors) {
            downloadElementSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(flavor));
        }
        for (String tag : sourceDownloadTags) {
            downloadElementSelector.addTag(tag);
        }

        // Configure the streaming element selector
        SimpleElementSelector streamingElementSelector = new SimpleElementSelector();
        for (String flavor : sourceStreamingFlavors) {
            streamingElementSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(flavor));
        }
        for (String tag : sourceStreamingTags) {
            streamingElementSelector.addTag(tag);
        }

        // Select the appropriate elements for download and streaming
        Collection<MediaPackageElement> downloadElements = downloadElementSelector.select(mediaPackage, false);
        Collection<MediaPackageElement> streamingElements = streamingElementSelector.select(mediaPackage, false);

        try {
            Set<String> downloadElementIds = new HashSet<String>();
            Set<String> streamingElementIds = new HashSet<String>();

            // Look for elements matching the tag
            for (MediaPackageElement elem : downloadElements) {
                downloadElementIds.add(elem.getIdentifier());
            }
            for (MediaPackageElement elem : streamingElements) {
                streamingElementIds.add(elem.getIdentifier());
            }

            // Also distribute the security configuration
            // -----
            // This was removed in the meantime by a fix for MH-8515, but could now be used again.
            // -----
            Attachment[] securityAttachments = mediaPackage.getAttachments(MediaPackageElements.XACML_POLICY);
            if (securityAttachments != null && securityAttachments.length > 0) {
                for (Attachment a : securityAttachments) {
                    downloadElementIds.add(a.getIdentifier());
                    streamingElementIds.add(a.getIdentifier());
                }
            }

            List<Job> jobs = new ArrayList<Job>();
            try {
                for (String elementId : downloadElementIds) {
                    logger.info("Element distribution delay, sleeping for " + Integer.toString(distributionDelay));
                    Thread.sleep(distributionDelay);
                    Job job = downloadDistributionService.distribute(CHANNEL_ID, mediaPackage, elementId,
                            checkAvailability);
                    if (job != null)
                        jobs.add(job);
                }
                if (distributeStreaming) {
                    for (String elementId : streamingElementIds) {
                        Job job = streamingDistributionService.distribute(CHANNEL_ID, mediaPackage, elementId);
                        if (job != null)
                            jobs.add(job);
                    }
                }
            } catch (DistributionException e) {
                throw new WorkflowOperationException(e);
            }

            if (jobs.size() < 1) {
                logger.info("No mediapackage element was found for distribution to engage");
                return createResult(mediaPackage, Action.CONTINUE);
            }

            // Wait until all distribution jobs have returned
            if (!waitForStatus(jobs.toArray(new Job[jobs.size()])).isSuccess())
                throw new WorkflowOperationException("One of the distribution jobs did not complete successfully");

            logger.debug("Distribute of mediapackage {} completed", mediaPackage);

            String engageUrlString = null;
            try {
                MediaPackage mediaPackageForSearch = getMediaPackageForSearchIndex(mediaPackage, jobs,
                        downloadSubflavor, targetDownloadTags, downloadElementIds, streamingSubflavor,
                        streamingElementIds, targetStreamingTags);

                // MH-10216, check if only merging into existing mediapackage
                boolean merge = Boolean
                        .parseBoolean(workflowInstance.getCurrentOperation().getConfiguration(OPT_MERGE_ONLY));
                if (merge) {
                    // merge() returns merged mediapackage or null mediaPackage is not published
                    mediaPackageForSearch = merge(mediaPackageForSearch);
                    if (mediaPackageForSearch == null) {
                        logger.info("Skipping republish for {} since it is not currently published",
                                mediaPackage.getIdentifier().toString());
                        return createResult(mediaPackage, Action.SKIP);
                    }
                }

                if (!isPublishable(mediaPackageForSearch))
                    throw new WorkflowOperationException("Media package does not meet criteria for publication");

                logger.info("Publishing media package {} to search index", mediaPackageForSearch);

                URL engageBaseUrl = null;
                engageUrlString = StringUtils
                        .trimToNull(workflowInstance.getOrganization().getProperties().get(ENGAGE_URL_PROPERTY));
                if (engageUrlString != null) {
                    engageBaseUrl = new URL(engageUrlString);
                } else {
                    engageBaseUrl = serverUrl;
                    logger.info(
                            "Using 'server.url' as a fallback for the non-existing organization level key '{}' for the publication url",
                            ENGAGE_URL_PROPERTY);
                }

                // Create new distribution element
                URI engageUri = URIUtils.resolve(engageBaseUrl.toURI(),
                        "/engage/ui/watch.html?id=" + mediaPackage.getIdentifier().compact());
                Publication publicationElement = PublicationImpl.publication(UUID.randomUUID().toString(),
                        CHANNEL_ID, engageUri, MimeTypes.parseMimeType("text/html"));
                mediaPackage.add(publicationElement);

                // Adding media package to the search index
                Job publishJob = null;
                try {
                    publishJob = searchService.add(mediaPackageForSearch);
                    if (!waitForStatus(publishJob).isSuccess()) {
                        throw new WorkflowOperationException("Mediapackage " + mediaPackageForSearch.getIdentifier()
                                + " could not be published");
                    }
                } catch (SearchException e) {
                    throw new WorkflowOperationException("Error publishing media package", e);
                } catch (MediaPackageException e) {
                    throw new WorkflowOperationException("Error parsing media package", e);
                }

                logger.debug("Publishing of mediapackage {} completed", mediaPackage);
                return createResult(mediaPackage, Action.CONTINUE);
            } catch (MalformedURLException e) {
                logger.error("{} is malformed: {}", ENGAGE_URL_PROPERTY, engageUrlString);
                throw new WorkflowOperationException(e);
            } catch (Throwable t) {
                if (t instanceof WorkflowOperationException)
                    throw (WorkflowOperationException) t;
                else
                    throw new WorkflowOperationException(t);
            }
        } catch (Exception e) {
            if (e instanceof WorkflowOperationException) {
                throw (WorkflowOperationException) e;
            } else {
                throw new WorkflowOperationException(e);
            }
        }
    }

    /**
     * Returns a mediapackage that only contains elements that are marked for distribution.
     *
     * @param current
     *          the current mediapackage
     * @param jobs
     *          the distribution jobs
     * @param downloadSubflavor
     *          flavor to be applied to elements distributed to download
     * @param downloadTargetTags
     *          tags to be applied to elements distributed to downloads
     * @param downloadElementIds
     *          identifiers for elements that have been distributed to downloads
     * @param streamingSubflavor
     *          flavor to be applied to elements distributed to streaming
     * @param streamingElementIds
     *          identifiers for elements that have been distributed to streaming
     * @param streamingTargetTags
     *          tags to be applied to elements distributed to streaming
     * @return the new mediapackage
     */
    protected MediaPackage getMediaPackageForSearchIndex(MediaPackage current, List<Job> jobs,
            MediaPackageElementFlavor downloadSubflavor, String[] downloadTargetTags,
            Set<String> downloadElementIds, MediaPackageElementFlavor streamingSubflavor,
            Set<String> streamingElementIds, String[] streamingTargetTags)
            throws MediaPackageException, NotFoundException, ServiceRegistryException, WorkflowOperationException {
        MediaPackage mp = (MediaPackage) current.clone();

        // All the jobs have passed, let's update the mediapackage with references to the distributed elements
        List<String> elementsToPublish = new ArrayList<String>();
        Map<String, String> distributedElementIds = new HashMap<String, String>();

        for (Job entry : jobs) {
            Job job = serviceRegistry.getJob(entry.getId());
            String sourceElementId = job.getArguments().get(2);
            MediaPackageElement sourceElement = mp.getElementById(sourceElementId);

            // If there is no payload, then the item has not been distributed.
            if (job.getPayload() == null)
                continue;

            List<MediaPackageElement> distributedElements = null;
            try {
                distributedElements = (List<MediaPackageElement>) MediaPackageElementParser
                        .getArrayFromXml(job.getPayload());
            } catch (MediaPackageException e) {
                throw new WorkflowOperationException(e);
            }

            // If the job finished successfully, but returned no new element, the channel simply doesn't support this
            // kind of element. So we just keep on looping.
            if (distributedElements == null || distributedElements.size() < 1)
                continue;

            // Make sure the mediapackage is prompted to create a new identifier for this element
            for (int i = 0; i < distributedElements.size(); i++) {
                if (distributedElements.get(i) instanceof MediaPackageElement)
                    ((MediaPackageElement) distributedElements.get(i)).setIdentifier(null);
                else
                    distributedElements.remove(i); // make sure no other Elements are in the list for future operations
            }

            // Adjust the flavor and tags for downloadable elements
            if (downloadElementIds.contains(sourceElementId)) {
                if (downloadSubflavor != null) {
                    MediaPackageElementFlavor flavor = sourceElement.getFlavor();
                    if (flavor != null) {
                        MediaPackageElementFlavor newFlavor = new MediaPackageElementFlavor(flavor.getType(),
                                downloadSubflavor.getSubtype());
                        for (int i = 0; i < distributedElements.size(); i++)
                            ((MediaPackageElement) distributedElements.get(i)).setFlavor(newFlavor);
                    }
                }
                for (String tag : downloadTargetTags) {
                    for (int i = 0; i < distributedElements.size(); i++)
                        ((MediaPackageElement) distributedElements.get(i)).addTag(tag);
                }
            }

            // Adjust the flavor and tags for streaming elements
            else if (streamingElementIds.contains(sourceElementId)) {
                if (streamingSubflavor != null && streamingElementIds.contains(sourceElementId)) {
                    MediaPackageElementFlavor flavor = sourceElement.getFlavor();
                    if (flavor != null) {
                        MediaPackageElementFlavor newFlavor = new MediaPackageElementFlavor(flavor.getType(),
                                streamingSubflavor.getSubtype());
                        for (int i = 0; i < distributedElements.size(); i++)
                            ((MediaPackageElement) distributedElements.get(i)).setFlavor(newFlavor);
                    }
                }
                for (String tag : streamingTargetTags) {
                    for (int i = 0; i < distributedElements.size(); i++)
                        ((MediaPackageElement) distributedElements.get(i)).addTag(tag);
                }
            }

            // Copy references from the source elements to the distributed elements
            MediaPackageReference ref = sourceElement.getReference();
            if (ref != null && mp.getElementByReference(ref) != null) {
                MediaPackageReference newReference = (MediaPackageReference) ref.clone();
                for (int i = 0; i < distributedElements.size(); i++)
                    ((MediaPackageElement) distributedElements.get(i)).setReference(newReference);
            }

            // Add the new element to the mediapackage
            for (int i = 0; i < distributedElements.size(); i++) {
                mp.add((MediaPackageElement) distributedElements.get(i));
                elementsToPublish.add(((MediaPackageElement) distributedElements.get(i)).getIdentifier());
                distributedElementIds.put(sourceElementId,
                        ((MediaPackageElement) distributedElements.get(i)).getIdentifier());
            }

        }

        // Mark everything that is set for removal
        List<MediaPackageElement> removals = new ArrayList<MediaPackageElement>();
        for (MediaPackageElement element : mp.getElements()) {
            if (!elementsToPublish.contains(element.getIdentifier())) {
                removals.add(element);
            }
        }

        // Translate references to the distributed artifacts
        for (MediaPackageElement element : mp.getElements()) {

            if (removals.contains(element))
                continue;

            // Is the element referencing anything?
            MediaPackageReference reference = element.getReference();
            if (reference == null)
                continue;

            // See if the element has been distributed
            String distributedElementId = distributedElementIds.get(reference.getIdentifier());
            if (distributedElementId == null)
                continue;

            MediaPackageReference translatedReference = new MediaPackageReferenceImpl(
                    mp.getElementById(distributedElementId));
            if (reference.getProperties() != null) {
                translatedReference.getProperties().putAll(reference.getProperties());
            }

            // Set the new reference
            element.setReference(translatedReference);

        }

        // Remove everything we don't want to add to publish
        for (MediaPackageElement element : removals) {
            mp.remove(element);
        }
        return mp;
    }

    /** Media package must meet these criteria in order to be published. */
    private boolean isPublishable(MediaPackage mp) {
        boolean hasTitle = !isBlank(mp.getTitle());
        if (!hasTitle)
            logger.warn("Media package does not meet criteria for publication: There is no title");

        boolean hasTracks = mp.hasTracks();
        if (!hasTracks)
            logger.warn("Media package does not meet criteria for publication: There are no tracks");

        return hasTitle && hasTracks;
    }

    /**
     * MH-10216, method copied from the original RepublishWorkflowOperationHandler
     * Merges mediapackage with published mediapackage.
     *
     * @param mediaPackageForSearch
     * @return merged mediapackage or null if a published medipackage was not found
     * @throws WorkflowOperationException
     */
    protected MediaPackage merge(MediaPackage mediaPackageForSearch) throws WorkflowOperationException {
        MediaPackage mergedMediaPackage = null;
        SearchQuery query = new SearchQuery().withId(mediaPackageForSearch.toString());
        query.includeEpisodes(true);
        query.includeSeries(false);
        SearchResult result = searchService.getByQuery(query);
        if (result.size() == 0) {
            logger.info("The search service doesn't know mediapackage {}, cannot be republished.",
                    mediaPackageForSearch);
            return mergedMediaPackage; // i.e. null
        } else if (result.size() > 1) {
            logger.warn("More than one mediapackage with id {} returned from search service",
                    mediaPackageForSearch);
            throw new WorkflowOperationException(
                    "More than one mediapackage with id " + mediaPackageForSearch + " found");
        } else {
            // else, merge the new with the existing (new elements will overwrite existing elements)
            mergedMediaPackage = mergePackages(mediaPackageForSearch, result.getItems()[0].getMediaPackage());
        }

        return mergedMediaPackage;
    }

    /**
     * MH-10216, Copied from the original RepublishWorkflowOperationHandler
     *
     * Merges the updated mediapackage with the one that is currently published in a way where the updated elements
     * replace existing ones in the published mediapackage based on their flavor.
     * <p>
     * If <code>publishedMp</code> is <code>null</code>, this method returns the updated mediapackage without any
     * modifications.
     *
     * @param updatedMp
     *          the updated media package
     * @param publishedMp
     *          the mediapackage that is currently published
     * @return the merged mediapackage
     */
    protected MediaPackage mergePackages(MediaPackage updatedMp, MediaPackage publishedMp) {
        if (publishedMp == null)
            return updatedMp;

        MediaPackage mergedMediaPackage = (MediaPackage) updatedMp.clone();
        for (MediaPackageElement element : publishedMp.elements()) {
            String type = element.getElementType().toString().toLowerCase();
            if (updatedMp.getElementsByFlavor(element.getFlavor()).length == 0) {
                logger.info("Merging {} '{}' into the updated mediapackage", type, element.getIdentifier());
                mergedMediaPackage.add((MediaPackageElement) element.clone());
            } else {
                logger.info(String.format("Overwriting existing %s '%s' with '%s' in the updated mediapackage",
                        type, element.getIdentifier(),
                        updatedMp.getElementsByFlavor(element.getFlavor())[0].getIdentifier()));

            }
        }

        return mergedMediaPackage;
    }

}