SelectVersionWorkflowOperationHandler.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.assetmanager;

import org.opencastproject.assetmanager.api.AssetManager;
import org.opencastproject.assetmanager.api.Snapshot;
import org.opencastproject.assetmanager.api.query.AQueryBuilder;
import org.opencastproject.assetmanager.api.query.ARecord;
import org.opencastproject.assetmanager.api.query.AResult;
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.selector.SimpleElementSelector;
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.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowOperationResult;

import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

/**
 * Replaces the media package in the current workflow with a previous version from the asset manager. There are two ways
 * to choose the version: by version number or a combination of source-flavors and no-tags (choose the latest version
 * where elements with source-flavor do not have the tags specified in no-tags.
 *
 * This operation should be the first one in a workflow executed from the archive because it REPLACES the media package
 * used by the current workflow.
 */
@Component(immediate = true, service = WorkflowOperationHandler.class, property = {
        "service.description=Selects a mp version from the archive", "workflow.operation=select-version" })
public class SelectVersionWorkflowOperationHandler extends AbstractWorkflowOperationHandler {

  private static final Logger logger = LoggerFactory.getLogger(SelectVersionWorkflowOperationHandler.class);

  public static final String OPT_VERSION = "version";
  public static final String OPT_NO_TAGS = "no-tags";
  public static final String OPT_SOURCE_FLAVORS = "source-flavors";

  private AssetManager assetManager;

  @Override
  public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
          throws WorkflowOperationException {

    final WorkflowOperationInstance currentOperation = workflowInstance.getCurrentOperation();
    if (currentOperation == null) {
      throw new WorkflowOperationException("Cannot get current workflow operation");
    }
    // Get current media package
    MediaPackage mp = workflowInstance.getMediaPackage();
    MediaPackage resultMp = null;

    // Make sure operation configuration is valid.
    String version = StringUtils.trimToNull(currentOperation.getConfiguration(OPT_VERSION));
    String noTagsOpt = StringUtils.trimToNull(currentOperation.getConfiguration(OPT_NO_TAGS));
    String sourceFlavorsOpt = StringUtils.trimToNull(currentOperation.getConfiguration(OPT_SOURCE_FLAVORS));
    if (version != null && (noTagsOpt != null || sourceFlavorsOpt != null)) {
      throw new WorkflowOperationException(
              String.format("Configuration error: '%s' cannot be used with '%s' and '%s'.",
              OPT_VERSION, OPT_NO_TAGS, OPT_SOURCE_FLAVORS));
    }

    // Specific version informed? If yes, use it.
    if (version != null) {
      try {
        // Validate the number
        Integer.parseInt(version);
        resultMp = findVersion(mp.getIdentifier().toString(), version);
        if (resultMp == null) {
          throw new WorkflowOperationException(
                  String.format("Could not find version %d of mp %s in the archive", mp.getIdentifier(), version));
        }
      } catch (NumberFormatException e) {
        throw new WorkflowOperationException("Invalid version passed: " + version);
      }
    } else {
      if (noTagsOpt == null || sourceFlavorsOpt == null) {
        throw new WorkflowOperationException(String.format("Configuration error: both '%s' and '%s' must be passed.",
                OPT_NO_TAGS, OPT_SOURCE_FLAVORS));
      }
      Collection<String> noTags = Arrays.asList(noTagsOpt.split(","));

      SimpleElementSelector elementSelector = new SimpleElementSelector();
      for (MediaPackageElementFlavor flavor : parseFlavors(sourceFlavorsOpt)) {
        elementSelector.addFlavor(flavor);
      }

      resultMp = findVersionWithNoTags(mp.getIdentifier().toString(), elementSelector, noTags);
      if (resultMp == null) {
        throw new WorkflowOperationException(String.format(
                "Could not find in the archive a version of mp %s that does not have the tags %s in element flavors %s",
                mp.getIdentifier(), noTagsOpt, sourceFlavorsOpt));
      }
    }
    return createResult(resultMp, WorkflowOperationResult.Action.CONTINUE);
  }

  private MediaPackage findVersion(String mpId, String version) throws WorkflowOperationException {
    // Get the specific version from the asset manager
    AQueryBuilder q = assetManager.createQuery();

    AResult r = q.select(q.snapshot())
            .where(q.mediaPackageId(mpId).and(q.version().eq(assetManager.toVersion(version).get()))).run();

    if (r.getSize() == 0) {
      // Version not found
      throw new WorkflowOperationException(
              String.format("Media package %s, version %s not found in the archive.", mpId, version));
    }

    for (ARecord rec : r.getRecords()) {
      // There should be only one
      Optional<Snapshot> optSnap = rec.getSnapshot();
      if (optSnap.isEmpty()) {
        continue;
      }
      logger.info("Replacing current media package with version: {}", version);
      return optSnap.get().getMediaPackage();
    }
    return null;
  }

  private MediaPackage findVersionWithNoTags(String mpId, SimpleElementSelector elementSelector,
          Collection<String> tags) throws WorkflowOperationException {
    // Get all the snapshots from the asset manager
    AQueryBuilder q = assetManager.createQuery();

    AResult r = q.select(q.snapshot()).where(q.mediaPackageId(mpId)).orderBy(q.version().desc()).run();
    if (r.getSize() == 0) {
      // This is strange because it should run from the archive
      throw new WorkflowOperationException("Media package not found in the archive: " + mpId);
    }

    nextVersion: for (ARecord rec : r.getRecords()) {
      Optional<Snapshot> optSnap = rec.getSnapshot();
      if (optSnap.isEmpty()) {
        continue;
      }
      Snapshot snapshot = optSnap.get();
      MediaPackage mp = snapshot.getMediaPackage();
      for (MediaPackageElement el : elementSelector.select(mp, false)) {
        for (String t : el.getTags()) {
          if (tags.contains(t)) {
            continue nextVersion;
          }
        }
      }
      logger.info("Replacing current media package with version: {}", snapshot.getVersion());
      return mp;
    }
    return null;
  }

  private List<MediaPackageElementFlavor> parseFlavors(String flavorStr) {
    List<MediaPackageElementFlavor> flavors = new ArrayList<MediaPackageElementFlavor>();
    if (flavorStr != null) {
      for (String flavor : asList(flavorStr)) {
        flavors.add(MediaPackageElementFlavor.parseFlavor(flavor));
      }
    }
    return flavors;
  }

  @Reference
  public void setAssetManager(AssetManager service) {
    this.assetManager = service;
  }
}