View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.workflow.handler.assetmanager;
23  
24  import org.opencastproject.assetmanager.api.AssetManager;
25  import org.opencastproject.assetmanager.api.Snapshot;
26  import org.opencastproject.assetmanager.api.query.AQueryBuilder;
27  import org.opencastproject.assetmanager.api.query.ARecord;
28  import org.opencastproject.assetmanager.api.query.AResult;
29  import org.opencastproject.job.api.JobContext;
30  import org.opencastproject.mediapackage.MediaPackage;
31  import org.opencastproject.mediapackage.MediaPackageElement;
32  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
33  import org.opencastproject.mediapackage.selector.SimpleElementSelector;
34  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
35  import org.opencastproject.workflow.api.WorkflowInstance;
36  import org.opencastproject.workflow.api.WorkflowOperationException;
37  import org.opencastproject.workflow.api.WorkflowOperationHandler;
38  import org.opencastproject.workflow.api.WorkflowOperationInstance;
39  import org.opencastproject.workflow.api.WorkflowOperationResult;
40  
41  import org.apache.commons.lang3.StringUtils;
42  import org.osgi.service.component.annotations.Component;
43  import org.osgi.service.component.annotations.Reference;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.Collection;
50  import java.util.List;
51  import java.util.Optional;
52  
53  /**
54   * Replaces the media package in the current workflow with a previous version from the asset manager. There are two ways
55   * to choose the version: by version number or a combination of source-flavors and no-tags (choose the latest version
56   * where elements with source-flavor do not have the tags specified in no-tags.
57   *
58   * This operation should be the first one in a workflow executed from the archive because it REPLACES the media package
59   * used by the current workflow.
60   */
61  @Component(immediate = true, service = WorkflowOperationHandler.class, property = {
62          "service.description=Selects a mp version from the archive", "workflow.operation=select-version" })
63  public class SelectVersionWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
64  
65    private static final Logger logger = LoggerFactory.getLogger(SelectVersionWorkflowOperationHandler.class);
66  
67    public static final String OPT_VERSION = "version";
68    public static final String OPT_NO_TAGS = "no-tags";
69    public static final String OPT_SOURCE_FLAVORS = "source-flavors";
70  
71    private AssetManager assetManager;
72  
73    @Override
74    public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
75            throws WorkflowOperationException {
76  
77      final WorkflowOperationInstance currentOperation = workflowInstance.getCurrentOperation();
78      if (currentOperation == null) {
79        throw new WorkflowOperationException("Cannot get current workflow operation");
80      }
81      // Get current media package
82      MediaPackage mp = workflowInstance.getMediaPackage();
83      MediaPackage resultMp = null;
84  
85      // Make sure operation configuration is valid.
86      String version = StringUtils.trimToNull(currentOperation.getConfiguration(OPT_VERSION));
87      String noTagsOpt = StringUtils.trimToNull(currentOperation.getConfiguration(OPT_NO_TAGS));
88      String sourceFlavorsOpt = StringUtils.trimToNull(currentOperation.getConfiguration(OPT_SOURCE_FLAVORS));
89      if (version != null && (noTagsOpt != null || sourceFlavorsOpt != null)) {
90        throw new WorkflowOperationException(
91                String.format("Configuration error: '%s' cannot be used with '%s' and '%s'.",
92                OPT_VERSION, OPT_NO_TAGS, OPT_SOURCE_FLAVORS));
93      }
94  
95      // Specific version informed? If yes, use it.
96      if (version != null) {
97        try {
98          // Validate the number
99          Integer.parseInt(version);
100         resultMp = findVersion(mp.getIdentifier().toString(), version);
101         if (resultMp == null) {
102           throw new WorkflowOperationException(
103                   String.format("Could not find version %d of mp %s in the archive", mp.getIdentifier(), version));
104         }
105       } catch (NumberFormatException e) {
106         throw new WorkflowOperationException("Invalid version passed: " + version);
107       }
108     } else {
109       if (noTagsOpt == null || sourceFlavorsOpt == null) {
110         throw new WorkflowOperationException(String.format("Configuration error: both '%s' and '%s' must be passed.",
111                 OPT_NO_TAGS, OPT_SOURCE_FLAVORS));
112       }
113       Collection<String> noTags = Arrays.asList(noTagsOpt.split(","));
114 
115       SimpleElementSelector elementSelector = new SimpleElementSelector();
116       for (MediaPackageElementFlavor flavor : parseFlavors(sourceFlavorsOpt)) {
117         elementSelector.addFlavor(flavor);
118       }
119 
120       resultMp = findVersionWithNoTags(mp.getIdentifier().toString(), elementSelector, noTags);
121       if (resultMp == null) {
122         throw new WorkflowOperationException(String.format(
123                 "Could not find in the archive a version of mp %s that does not have the tags %s in element flavors %s",
124                 mp.getIdentifier(), noTagsOpt, sourceFlavorsOpt));
125       }
126     }
127     return createResult(resultMp, WorkflowOperationResult.Action.CONTINUE);
128   }
129 
130   private MediaPackage findVersion(String mpId, String version) throws WorkflowOperationException {
131     // Get the specific version from the asset manager
132     AQueryBuilder q = assetManager.createQuery();
133 
134     AResult r = q.select(q.snapshot())
135             .where(q.mediaPackageId(mpId).and(q.version().eq(assetManager.toVersion(version).get()))).run();
136 
137     if (r.getSize() == 0) {
138       // Version not found
139       throw new WorkflowOperationException(
140               String.format("Media package %s, version %s not found in the archive.", mpId, version));
141     }
142 
143     for (ARecord rec : r.getRecords()) {
144       // There should be only one
145       Optional<Snapshot> optSnap = rec.getSnapshot();
146       if (optSnap.isEmpty()) {
147         continue;
148       }
149       logger.info("Replacing current media package with version: {}", version);
150       return optSnap.get().getMediaPackage();
151     }
152     return null;
153   }
154 
155   private MediaPackage findVersionWithNoTags(String mpId, SimpleElementSelector elementSelector,
156           Collection<String> tags) throws WorkflowOperationException {
157     // Get all the snapshots from the asset manager
158     AQueryBuilder q = assetManager.createQuery();
159 
160     AResult r = q.select(q.snapshot()).where(q.mediaPackageId(mpId)).orderBy(q.version().desc()).run();
161     if (r.getSize() == 0) {
162       // This is strange because it should run from the archive
163       throw new WorkflowOperationException("Media package not found in the archive: " + mpId);
164     }
165 
166     nextVersion: for (ARecord rec : r.getRecords()) {
167       Optional<Snapshot> optSnap = rec.getSnapshot();
168       if (optSnap.isEmpty()) {
169         continue;
170       }
171       Snapshot snapshot = optSnap.get();
172       MediaPackage mp = snapshot.getMediaPackage();
173       for (MediaPackageElement el : elementSelector.select(mp, false)) {
174         for (String t : el.getTags()) {
175           if (tags.contains(t)) {
176             continue nextVersion;
177           }
178         }
179       }
180       logger.info("Replacing current media package with version: {}", snapshot.getVersion());
181       return mp;
182     }
183     return null;
184   }
185 
186   private List<MediaPackageElementFlavor> parseFlavors(String flavorStr) {
187     List<MediaPackageElementFlavor> flavors = new ArrayList<MediaPackageElementFlavor>();
188     if (flavorStr != null) {
189       for (String flavor : asList(flavorStr)) {
190         flavors.add(MediaPackageElementFlavor.parseFlavor(flavor));
191       }
192     }
193     return flavors;
194   }
195 
196   @Reference
197   public void setAssetManager(AssetManager service) {
198     this.assetManager = service;
199   }
200 }