SeriesWorkflowOperationHandler.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.workflow;
import org.opencastproject.job.api.JobContext;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.EName;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.mediapackage.selector.AbstractMediaPackageElementSelector;
import org.opencastproject.mediapackage.selector.CatalogSelector;
import org.opencastproject.metadata.dublincore.DublinCore;
import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
import org.opencastproject.metadata.dublincore.DublinCoreUtil;
import org.opencastproject.metadata.dublincore.DublinCores;
import org.opencastproject.metadata.dublincore.SeriesCatalogUIAdapter;
import org.opencastproject.security.api.AccessControlList;
import org.opencastproject.security.api.AclScope;
import org.opencastproject.security.api.AuthorizationService;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.series.api.SeriesException;
import org.opencastproject.series.api.SeriesService;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.util.Checksum;
import org.opencastproject.util.ChecksumType;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.XmlNamespaceBinding;
import org.opencastproject.util.XmlNamespaceContext;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
import org.opencastproject.workflow.api.WorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowOperationResult;
import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
import org.opencastproject.workspace.api.Workspace;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* The workflow definition for handling "series" operations
*/
@Component(
immediate = true,
service = WorkflowOperationHandler.class,
property = {
"service.description=Apply Series to Recording Workflow Operation Handler",
"workflow.operation=series"
}
)
public class SeriesWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(SeriesWorkflowOperationHandler.class);
/** Name of the configuration option that provides the optional series identifier */
public static final String SERIES_PROPERTY = "series";
/** Name of the configuration option that provides the flavors of the series catalogs to attach */
public static final String ATTACH_PROPERTY = "attach";
/** Name of the configuration option that provides whether the ACL should be applied or not */
public static final String APPLY_ACL_PROPERTY = "apply-acl";
/** Name of the configuration key that specifies the list of series metadata to be copied to the episode */
public static final String COPY_METADATA_PROPERTY = "copy-metadata";
/** Name of the configuration key that specifies the default namespace for the metadata to be copied to the episode */
public static final String DEFAULT_NS_PROPERTY = "default-namespace";
/** The authorization service */
private AuthorizationService authorizationService;
/** The series service */
private SeriesService seriesService;
/** The security service */
private SecurityService securityService;
/** The list series catalog UI adapters */
private final List<SeriesCatalogUIAdapter> seriesCatalogUIAdapters = new ArrayList<>();
/**
* Callback for the OSGi declarative services configuration.
*
* @param authorizationService
* the authorization service
*/
@Reference
protected void setAuthorizationService(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
/**
* Callback for the OSGi declarative services configuration.
*
* @param seriesService
* the series service
*/
@Reference
public void setSeriesService(SeriesService seriesService) {
this.seriesService = seriesService;
}
/**
* Callback for the OSGi declarative services configuration.
*
* @param workspace
* the workspace
*/
@Reference
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* Callback for the OSGi declarative services configuration.
*
* @param securityService
* the securityService
*/
@Reference
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/** OSGi callback to add {@link SeriesCatalogUIAdapter} instance. */
@Reference(
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC,
unbind = "removeCatalogUIAdapter"
)
public void addCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
seriesCatalogUIAdapters.add(catalogUIAdapter);
}
/** OSGi callback to remove {@link SeriesCatalogUIAdapter} instance. */
public void removeCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
seriesCatalogUIAdapters.remove(catalogUIAdapter);
}
@Reference
@Override
public void setServiceRegistry(ServiceRegistry serviceRegistry) {
super.setServiceRegistry(serviceRegistry);
}
/**
* {@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 series workflow operation");
MediaPackage mediaPackage = workflowInstance.getMediaPackage();
Optional<String> optSeries = getOptConfig(workflowInstance.getCurrentOperation(), SERIES_PROPERTY);
Optional<String> optAttachFlavors = getOptConfig(workflowInstance.getCurrentOperation(), ATTACH_PROPERTY);
Boolean applyAcl = getOptConfig(workflowInstance.getCurrentOperation(), APPLY_ACL_PROPERTY)
.map(value -> BooleanUtils.toBoolean(value))
.orElse(false);
Optional<String> optCopyMetadata = getOptConfig(workflowInstance.getCurrentOperation(), COPY_METADATA_PROPERTY);
String defaultNamespace = getOptConfig(workflowInstance.getCurrentOperation(), DEFAULT_NS_PROPERTY)
.orElse(DublinCore.TERMS_NS_URI);
logger.debug("Using default namespace: '{}'", defaultNamespace);
if (optSeries.isPresent() && !optSeries.get().equals(mediaPackage.getSeries())) {
logger.info("Changing series id from '{}' to '{}'", StringUtils.trimToEmpty(mediaPackage.getSeries()),
optSeries.get());
mediaPackage.setSeries(optSeries.get());
}
String seriesId = mediaPackage.getSeries();
if (seriesId == null) {
logger.info("No series set, skip operation");
return createResult(mediaPackage, Action.SKIP);
}
DublinCoreCatalog series;
try {
series = seriesService.getSeries(seriesId);
} catch (NotFoundException e) {
logger.info("No series with the identifier '{}' found, skip operation", seriesId);
return createResult(mediaPackage, Action.SKIP);
} catch (UnauthorizedException e) {
logger.warn("Not authorized to get series with identifier '{}' found, skip operation", seriesId);
return createResult(mediaPackage, Action.SKIP);
} catch (SeriesException e) {
logger.error("Unable to get series with identifier '{}', skip operation:", seriesId, e);
throw new WorkflowOperationException(e);
}
mediaPackage.setSeriesTitle(series.getFirst(DublinCore.PROPERTY_TITLE));
// Process extra metadata
HashSet<EName> extraMetadata = new HashSet<>();
if (optCopyMetadata.isPresent()) {
for (String strEName : optCopyMetadata.get().split(",+\\s*")) {
try {
if (!strEName.isEmpty()) {
extraMetadata.add(EName.fromString(strEName, defaultNamespace));
}
} catch (IllegalArgumentException iae) {
logger.warn("Ignoring incorrect dublincore metadata property: '{}'", strEName);
}
}
}
// Update the episode catalog
for (Catalog episodeCatalog : mediaPackage.getCatalogs(MediaPackageElements.EPISODE)) {
DublinCoreCatalog episodeDublinCore = DublinCoreUtil.loadDublinCore(workspace, episodeCatalog);
// Make sure the MP catalog has bindings defined
episodeDublinCore.addBindings(
XmlNamespaceContext.mk(XmlNamespaceBinding.mk(DublinCore.TERMS_NS_PREFIX, DublinCore.TERMS_NS_URI)));
episodeDublinCore.addBindings(XmlNamespaceContext
.mk(XmlNamespaceBinding.mk(DublinCore.ELEMENTS_1_1_NS_PREFIX, DublinCore.ELEMENTS_1_1_NS_URI)));
episodeDublinCore.addBindings(XmlNamespaceContext
.mk(XmlNamespaceBinding.mk(DublinCores.OC_PROPERTY_NS_PREFIX, DublinCores.OC_PROPERTY_NS_URI)));
episodeDublinCore.set(DublinCore.PROPERTY_IS_PART_OF, seriesId);
for (EName property : extraMetadata) {
if (!episodeDublinCore.hasValue(property) && series.hasValue(property)) {
episodeDublinCore.set(property, series.get(property));
}
}
try (InputStream in = IOUtils.toInputStream(episodeDublinCore.toXmlString(), "UTF-8")) {
String filename = FilenameUtils.getName(episodeCatalog.getURI().toString());
URI uri = workspace.put(mediaPackage.getIdentifier().toString(), episodeCatalog.getIdentifier(), filename, in);
episodeCatalog.setURI(uri);
// setting the URI to a new source so the checksum will most like be invalid
episodeCatalog.setChecksum(null);
} catch (Exception e) {
logger.error("Unable to update episode catalog isPartOf field", e);
throw new WorkflowOperationException(e);
}
}
// Attach series catalogs
if (optAttachFlavors.isPresent()) {
// Remove existing series catalogs
AbstractMediaPackageElementSelector<Catalog> catalogSelector = new CatalogSelector();
String[] seriesFlavors = StringUtils.split(optAttachFlavors.get(), ",");
for (String flavor : seriesFlavors) {
if ("*".equals(flavor)) {
catalogSelector.addFlavor("*/*");
} else {
catalogSelector.addFlavor(flavor);
}
}
for (Catalog c : catalogSelector.select(mediaPackage, false)) {
if (MediaPackageElements.SERIES.equals(c.getFlavor()) || "series".equals(c.getFlavor().getSubtype())) {
mediaPackage.remove(c);
}
}
List<SeriesCatalogUIAdapter> adapters = getSeriesCatalogUIAdapters();
for (String flavorString : seriesFlavors) {
MediaPackageElementFlavor flavor;
if ("*".equals(flavorString)) {
flavor = MediaPackageElementFlavor.parseFlavor("*/*");
} else {
flavor = MediaPackageElementFlavor.parseFlavor(flavorString);
}
for (SeriesCatalogUIAdapter a : adapters) {
MediaPackageElementFlavor adapterFlavor = MediaPackageElementFlavor.parseFlavor(a.getFlavor().toString());
if (flavor.matches(adapterFlavor)) {
if (MediaPackageElements.SERIES.eq(a.getFlavor().toString())) {
addDublinCoreCatalog(series, MediaPackageElements.SERIES, mediaPackage);
} else {
try {
Optional<byte[]> seriesElementData = seriesService.getSeriesElementData(seriesId,
adapterFlavor.getType());
if (seriesElementData.isPresent()) {
DublinCoreCatalog catalog = DublinCores.read(new ByteArrayInputStream(seriesElementData.get()));
addDublinCoreCatalog(catalog, adapterFlavor, mediaPackage);
} else {
logger.warn("No extended series catalog found for flavor '{}' and series '{}', skip adding catalog",
adapterFlavor.getType(), seriesId);
}
} catch (SeriesException e) {
logger.error("Unable to load extended series metadata for flavor {}", adapterFlavor.getType());
throw new WorkflowOperationException(e);
}
}
}
}
}
}
if (applyAcl) {
try {
AccessControlList acl = seriesService.getSeriesAccessControl(seriesId);
if (acl != null) {
authorizationService.setAcl(mediaPackage, AclScope.Series, acl);
}
} catch (Exception e) {
logger.error("Unable to update series ACL", e);
throw new WorkflowOperationException(e);
}
}
return createResult(mediaPackage, Action.CONTINUE);
}
/**
* @param organization
* The organization to filter the results with.
* @return A {@link List} of {@link SeriesCatalogUIAdapter} that provide the metadata to the front end.
*/
private List<SeriesCatalogUIAdapter> getSeriesCatalogUIAdapters() {
String organization = securityService.getOrganization().getId();
List<SeriesCatalogUIAdapter> filteredAdapters = new ArrayList<>();
for (SeriesCatalogUIAdapter adapter : seriesCatalogUIAdapters) {
if (adapter.handlesOrganization(organization)) {
filteredAdapters.add(adapter);
}
}
return filteredAdapters;
}
private MediaPackage addDublinCoreCatalog(DublinCoreCatalog catalog, MediaPackageElementFlavor flavor,
MediaPackage mediaPackage) throws WorkflowOperationException {
try (InputStream in = IOUtils.toInputStream(catalog.toXmlString(), "UTF-8")) {
String elementId = UUID.randomUUID().toString();
URI catalogUrl = workspace.put(mediaPackage.getIdentifier().toString(), elementId, "dublincore.xml", in);
logger.info("Adding catalog with flavor {} to mediapackage {}", flavor, mediaPackage);
MediaPackageElement mpe = mediaPackage.add(catalogUrl, MediaPackageElement.Type.Catalog, flavor);
mpe.setIdentifier(elementId);
mpe.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, workspace.get(catalogUrl)));
return mediaPackage;
} catch (IOException | NotFoundException e) {
throw new WorkflowOperationException(e);
}
}
}