/**
* 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 com.entwinemedia.fn.fns.Strings.trimToNone;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.opencastproject.util.EqualsUtil.ne;
import static org.opencastproject.util.data.Collections.smap;
import static org.opencastproject.util.data.Monadics.mlist;
import static org.opencastproject.util.data.Tuple.tuple;
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.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageReference;
import org.opencastproject.mediapackage.MediaPackageSupport;
import org.opencastproject.mediapackage.Publication;
import org.opencastproject.oaipmh.persistence.OaiPmhDatabase;
import org.opencastproject.oaipmh.persistence.OaiPmhDatabaseException;
import org.opencastproject.oaipmh.persistence.QueryBuilder;
import org.opencastproject.oaipmh.persistence.SearchResult;
import org.opencastproject.publication.api.OaiPmhPublicationService;
import org.opencastproject.util.JobUtil;
import org.opencastproject.util.data.Collections;
import org.opencastproject.util.data.Function;
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 com.entwinemedia.fn.data.Opt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
/** Workflow operation for handling "republish" operations to OAI-PMH repositories. */
public final class RepublishOaiPmhWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(RepublishOaiPmhWorkflowOperationHandler.class);
private OaiPmhDatabase oaiPmhDb = null;
private DownloadDistributionService distSvc = null;
/** The configuration options */
private static final String OPT_SOURCE_FLAVORS = "source-flavors";
private static final String OPT_SOURCE_TAGS = "source-tags";
private static final String OPT_MERGE = "merge";
private static final String OPT_REPOSITORY = "repository";
/** The configuration options for this handler */
private static final SortedMap<String, String> CONFIG_OPTIONS = smap(
tuple(OPT_SOURCE_FLAVORS, "Republish any media package elements with one of these flavors"),
tuple(OPT_SOURCE_TAGS, "Republish only media package elements that are tagged with one of these tags"),
tuple(OPT_MERGE, "Merge with existing published data"),
tuple(OPT_REPOSITORY, "The OAI-PMH repository to update"));
@Override
public SortedMap<String, String> getConfigurationOptions() {
return CONFIG_OPTIONS;
}
@Override
public WorkflowOperationResult start(WorkflowInstance wi, JobContext context) throws WorkflowOperationException {
final MediaPackage mp = wi.getMediaPackage();
// The flavors of the elements that are to be published
final Set<MediaPackageElementFlavor> flavors = new HashSet<>();
// Check which flavors have been configured
final List<String> configuredFlavors = getOptConfig(wi, OPT_SOURCE_FLAVORS).bind(trimToNone).map(asList.toFn())
.getOr(Collections.<String> nil());
for (String flavor : configuredFlavors) {
flavors.add(MediaPackageElementFlavor.parseFlavor(flavor));
}
// Get the configured tags
final List<String> tags = asList(getOptConfig(wi, OPT_SOURCE_TAGS).getOr(""));
// Merge or replace?
boolean merge = Boolean.parseBoolean(getConfig(wi, OPT_MERGE));
// repository
final String repository = getConfig(wi, OPT_REPOSITORY);
//
final MediaPackage filteredMp;
final MediaPackage publishedMp;
final SearchResult result = oaiPmhDb.search(QueryBuilder.queryRepo(repository).mediaPackageId(mp).build());
if (result.size() == 1) {
// apply tags and flavors to the current media package
try {
filteredMp = filterMediaPackage(mp, flavors, tags);
} catch (MediaPackageException e) {
throw new WorkflowOperationException("Error filtering media package", e);
}
} else if (result.size() == 0) {
logger.info(
format("Skipping update of media package %s since it is not currently published to %s", mp, repository));
return createResult(mp, Action.SKIP);
} else {
final String msg = format("More than one media package with id %s found", mp);
logger.warn(msg);
throw new WorkflowOperationException(msg);
}
// re-distribute elements to download
final List<MediaPackageElement> distributedElements = mlist(filteredMp.getElements())
.filter(MediaPackageSupport.Filters.isNotPublication).map(new Function.X<MediaPackageElement, Job>() {
@Override
public Job xapply(MediaPackageElement mpe) throws Exception {
try {
// todo this -> OaiPmhPublicationService.PUBLICATION_CHANNEL_PREFIX + repository
// is pretty ugly. In fact republication should be subject to the OaiPmhPublicationService
// and _not_ the workflow operation handler.
return distSvc.distribute(OaiPmhPublicationService.PUBLICATION_CHANNEL_PREFIX + repository,
filteredMp, mpe.getIdentifier());
} catch (Exception e) {
throw new WorkflowOperationException(e);
}
}
}).map(JobUtil.payloadAsMediaPackageElement(serviceRegistry)).value();
// update elements (URLs)
for (MediaPackageElement e : filteredMp.getElements()) {
if (MediaPackageElement.Type.Publication.equals(e.getElementType()))
continue;
filteredMp.remove(e);
}
for (MediaPackageElement e : distributedElements) {
filteredMp.add(e);
}
if (merge) {
publishedMp = merge(filteredMp, result.getItems().get(0).getMediaPackage());
} else {
publishedMp = filteredMp;
}
// Does the media package have a title and track?
if (!isPublishable(publishedMp)) {
throw new WorkflowOperationException("Media package does not meet criteria for publication");
}
// Publish the media package to the search index
try {
logger.info(format("Updating metadata of media package %s in %s", publishedMp, repository));
oaiPmhDb.store(publishedMp, repository);
logger.info("Completed update operation on {}", mp.getIdentifier());
return createResult(mp, Action.CONTINUE);
} catch (OaiPmhDatabaseException e) {
throw new WorkflowOperationException(format("Media package %s could not be updated", publishedMp));
}
}
/**
* Creates a clone of the mediapackage and removes those elements that do not match the flavor and tags filter
* criteria.
*
* @param mediaPackage
* the media package
* @param flavors
* the flavors
* @param tags
* the tags
* @return the filtered media package
*/
private MediaPackage filterMediaPackage(MediaPackage mediaPackage, Set<MediaPackageElementFlavor> flavors,
List<String> tags) throws MediaPackageException {
MediaPackage filteredMediaPackage = (MediaPackage) mediaPackage.clone();
// The list of elements to keep
List<MediaPackageElement> keep = new ArrayList<>();
// Filter by flavor
if (flavors.size() > 0) {
logger.debug("Filtering elements based on flavors");
for (MediaPackageElementFlavor flavor : flavors) {
keep.addAll(Arrays.asList(mediaPackage.getElementsByFlavor(flavor)));
}
}
// Keep those elements that have been identified in the tags
if (tags.size() > 0) {
logger.debug("Filtering elements based on tags");
if (keep.size() > 0) {
keep.retainAll(Arrays.asList(mediaPackage.getElementsByTags(tags)));
} else {
keep.addAll(Arrays.asList(mediaPackage.getElementsByTags(tags)));
}
}
// Keep publications
for (Publication p : filteredMediaPackage.getPublications())
keep.add(p);
// Fix references and flavors
for (MediaPackageElement element : filteredMediaPackage.getElements()) {
if (!keep.contains(element)) {
logger.info("Removing {} '{}' from media package '{}'",
new String[] { element.getElementType().toString().toLowerCase(), element.getIdentifier(),
filteredMediaPackage.getIdentifier().toString() });
filteredMediaPackage.remove(element);
continue;
}
// Is the element referencing anything?
MediaPackageReference reference = element.getReference();
if (reference != null) {
Map<String, String> referenceProperties = reference.getProperties();
MediaPackageElement referencedElement = filteredMediaPackage.getElementByReference(reference);
// if we are distributing the referenced element, everything is fine. Otherwise...
if (referencedElement != null && !keep.contains(referencedElement)) {
// Follow the references until we find a flavor
MediaPackageElement parent = null;
while ((parent = mediaPackage.getElementByReference(reference)) != null) {
if (parent.getFlavor() != null && element.getFlavor() == null) {
element.setFlavor(parent.getFlavor());
}
if (parent.getReference() == null)
break;
reference = parent.getReference();
}
// Done. Let's cut the path but keep references to the mediapackage itself
if (reference != null && reference.getType().equals(MediaPackageReference.TYPE_MEDIAPACKAGE))
element.setReference(reference);
else if (reference != null && (referenceProperties == null || referenceProperties.size() == 0))
element.clearReference();
else {
// Ok, there is more to that reference than just pointing at an element. Let's keep the original,
// you never know.
referencedElement.setURI(null);
referencedElement.setChecksum(null);
}
}
}
}
return filteredMediaPackage;
}
/**
* 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
*/
public static MediaPackage merge(MediaPackage updatedMp, MediaPackage publishedMp) {
if (publishedMp == null)
return updatedMp;
final MediaPackage mergedMp = MediaPackageSupport.copy(publishedMp);
// Merge the elements
for (final MediaPackageElement updatedElement : updatedMp.elements()) {
for (final MediaPackageElementFlavor flavor : Opt.nul(updatedElement.getFlavor())) {
for (final MediaPackageElement outdated : mergedMp.getElementsByFlavor(flavor)) {
mergedMp.remove(outdated);
}
logger.info(format("Update %s of type %s", updatedElement.getIdentifier(), updatedElement.getElementType()));
mergedMp.add(updatedElement);
}
}
// Remove publications
for (final Publication p : mergedMp.getPublications())
mergedMp.remove(p);
// Add updated publications
for (final Publication updatedPublication : updatedMp.getPublications())
mergedMp.add(updatedPublication);
// Merge media package fields
if (updatedMp.getDate() != null && ne(updatedMp.getDate(), mergedMp.getDate()))
mergedMp.setDate(updatedMp.getDate());
if (updatedMp.getDuration() != null && ne(updatedMp.getDuration(), mergedMp.getDuration()))
mergedMp.setDuration(updatedMp.getDuration());
if (isNotBlank(updatedMp.getLicense()) && ne(updatedMp.getLicense(), mergedMp.getLicense()))
mergedMp.setLicense(updatedMp.getLicense());
if (isNotBlank(updatedMp.getSeries()) && ne(updatedMp.getSeries(), mergedMp.getSeries()))
mergedMp.setSeries(updatedMp.getSeries());
if (isNotBlank(updatedMp.getSeriesTitle()) && ne(updatedMp.getSeriesTitle(), mergedMp.getSeriesTitle()))
mergedMp.setSeriesTitle(updatedMp.getSeriesTitle());
if (isNotBlank(updatedMp.getTitle()) && ne(updatedMp.getTitle(), mergedMp.getTitle()))
mergedMp.setTitle(updatedMp.getTitle());
if (updatedMp.getSubjects().length > 0 && ne(updatedMp.getSubjects(), mergedMp.getSubjects())) {
for (String subject : mergedMp.getSubjects()) {
mergedMp.removeSubject(subject);
}
for (String subject : updatedMp.getSubjects()) {
mergedMp.addSubject(subject);
}
}
if (updatedMp.getContributors().length > 0 && ne(updatedMp.getContributors(), mergedMp.getContributors())) {
for (String contributor : mergedMp.getContributors()) {
mergedMp.removeContributor(contributor);
}
for (String contributor : updatedMp.getContributors()) {
mergedMp.addContributor(contributor);
}
}
if (updatedMp.getCreators().length > 0 && ne(updatedMp.getCreators(), mergedMp.getCreators())) {
for (String creator : mergedMp.getCreators()) {
mergedMp.removeCreator(creator);
}
for (String creator : updatedMp.getCreators()) {
mergedMp.addCreator(creator);
}
}
return mergedMp;
}
/**
* Media package must have a title and contain tracks in order to be published.
*
* @param mp
* the media package
* @return <code>true</code> if the media package can be published
*/
private boolean isPublishable(MediaPackage mp) {
return !isBlank(mp.getTitle()) && mp.hasTracks();
}
/** OSGi DI. */
public void setOaiPmhDatabase(OaiPmhDatabase oaiPmhDb) {
this.oaiPmhDb = oaiPmhDb;
}
/** OSGi DI. */
public void setDistributionService(DownloadDistributionService distSvc) {
this.distSvc = distSvc;
}
}