/** * 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.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.util.Checksum; import org.opencastproject.util.ChecksumType; import org.opencastproject.util.NotFoundException; 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.Fn2; import com.entwinemedia.fn.Stream; import com.entwinemedia.fn.data.Opt; 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.apache.commons.lang3.exception.ExceptionUtils; 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.List; import java.util.SortedMap; import java.util.TreeMap; import java.util.UUID; /** * The workflow definition for handling "series" operations */ public class SeriesWorkflowOperationHandler extends AbstractWorkflowOperationHandler { /** The logging facility */ private static final Logger logger = LoggerFactory.getLogger(SeriesWorkflowOperationHandler.class); /** The configuration options for this handler */ private static final SortedMap<String, String> CONFIG_OPTIONS; /** 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 wheter the ACL should be applied or not */ public static final String APPLY_ACL_PROPERTY = "apply-acl"; /** The authorization service */ private AuthorizationService authorizationService; /** The series service */ private SeriesService seriesService; /** The workspace */ private Workspace workspace; /** The security service */ private SecurityService securityService; /** The list series catalog UI adapters */ private final List<SeriesCatalogUIAdapter> seriesCatalogUIAdapters = new ArrayList<SeriesCatalogUIAdapter>(); /** * Callback for the OSGi declarative services configuration. * * @param authorizationService * the authorization service */ protected void setAuthorizationService(AuthorizationService authorizationService) { this.authorizationService = authorizationService; } /** * Callback for the OSGi declarative services configuration. * * @param seriesService * the series service */ public void setSeriesService(SeriesService seriesService) { this.seriesService = seriesService; } /** * Callback for the OSGi declarative services configuration. * * @param workspace * the workspace */ public void setWorkspace(Workspace workspace) { this.workspace = workspace; } /** * Callback for the OSGi declarative services configuration. * * @param securityService * the securityService */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** OSGi callback to add {@link SeriesCatalogUIAdapter} instance. */ public void addCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) { seriesCatalogUIAdapters.add(catalogUIAdapter); } /** OSGi callback to remove {@link SeriesCatalogUIAdapter} instance. */ public void removeCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) { seriesCatalogUIAdapters.remove(catalogUIAdapter); } static { CONFIG_OPTIONS = new TreeMap<String, String>(); CONFIG_OPTIONS.put(SERIES_PROPERTY, "The optional series identifier"); CONFIG_OPTIONS.put(ATTACH_PROPERTY, "The flavors of the series catalogs to attach to the mediapackage."); CONFIG_OPTIONS.put(APPLY_ACL_PROPERTY, "Whether the ACL should be applied or not"); } /** * {@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 series workflow operation"); MediaPackage mediaPackage = workflowInstance.getMediaPackage(); Opt<String> optSeries = getOptConfig(workflowInstance.getCurrentOperation(), SERIES_PROPERTY); Opt<String> optAttachFlavors = getOptConfig(workflowInstance.getCurrentOperation(), ATTACH_PROPERTY); Boolean applyAcl = getOptConfig(workflowInstance.getCurrentOperation(), APPLY_ACL_PROPERTY).map(toBoolean) .getOr(false); if (optSeries.isSome() && !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, ExceptionUtils.getStackTrace(e)); throw new WorkflowOperationException(e); } mediaPackage.setSeriesTitle(series.getFirst(DublinCore.PROPERTY_TITLE)); // Update the episode catalog for (Catalog episodeCatalog : mediaPackage.getCatalogs(MediaPackageElements.EPISODE)) { DublinCoreCatalog episodeDublinCore = DublinCoreUtil.loadDublinCore(workspace, episodeCatalog); episodeDublinCore.set(DublinCore.PROPERTY_IS_PART_OF, seriesId); 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, IOUtils.toInputStream(episodeDublinCore.toXmlString(), "UTF-8")); 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: {}", ExceptionUtils.getStackTrace(e)); throw new WorkflowOperationException(e); } } // Attach series catalogs if (optAttachFlavors.isSome()) { // 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()); if (flavor.matches(adapterFlavor)) { if (MediaPackageElements.SERIES.eq(a.getFlavor())) { addDublinCoreCatalog(series, MediaPackageElements.SERIES, mediaPackage); } else { try { Opt<byte[]> seriesElementData = seriesService.getSeriesElementData(seriesId, adapterFlavor.getType()); if (seriesElementData.isSome()) { 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: {}", ExceptionUtils.getStackTrace(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(); return Stream.$(seriesCatalogUIAdapters).filter(seriesOrganizationFilter._2(organization)).toList(); } 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().compact(), 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); } } private static final Fn2<SeriesCatalogUIAdapter, String, Boolean> seriesOrganizationFilter = new Fn2<SeriesCatalogUIAdapter, String, Boolean>() { @Override public Boolean apply(SeriesCatalogUIAdapter catalogUIAdapter, String organization) { return catalogUIAdapter.getOrganization().equals(organization); } }; /** Convert a string into a boolean. */ private static final Fn<String, Boolean> toBoolean = new Fn<String, Boolean>() { @Override public Boolean apply(String s) { return BooleanUtils.toBoolean(s); } }; }