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.api;
23  
24  import static java.lang.String.format;
25  
26  import org.opencastproject.job.api.Job;
27  import org.opencastproject.job.api.JobBarrier;
28  import org.opencastproject.job.api.JobContext;
29  import org.opencastproject.mediapackage.MediaPackage;
30  import org.opencastproject.mediapackage.MediaPackageElement;
31  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
32  import org.opencastproject.serviceregistry.api.ServiceRegistry;
33  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
34  
35  import org.apache.commons.io.FilenameUtils;
36  import org.apache.commons.lang3.StringUtils;
37  import org.osgi.framework.Constants;
38  import org.osgi.service.component.ComponentContext;
39  import org.osgi.service.component.annotations.Activate;
40  import org.osgi.service.component.annotations.Reference;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import java.util.ArrayList;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Optional;
48  
49  /**
50   * Abstract base implementation for an operation handler, which implements a simple start operation that returns a
51   * {@link WorkflowOperationResult} with the current mediapackage and {@link Action#CONTINUE}.
52   */
53  public abstract class AbstractWorkflowOperationHandler implements WorkflowOperationHandler {
54  
55    /** The logging facility */
56    private static final Logger logger = LoggerFactory.getLogger(AbstractWorkflowOperationHandler.class);
57  
58    /** The ID of this operation handler */
59    protected String id = null;
60  
61    /** The description of what this handler actually does */
62    protected String description = null;
63  
64    /** Optional service registry */
65    protected ServiceRegistry serviceRegistry = null;
66  
67    /** The JobBarrier polling interval */
68    private long jobBarrierPollingInterval = JobBarrier.DEFAULT_POLLING_INTERVAL;
69  
70    /** Config for Tag Parsing operation */
71    protected enum Configuration {
72      none, one, atLeastOne, many
73    };
74  
75    public static final String TARGET_FLAVORS = "target-flavors";
76    public static final String TARGET_FLAVOR = "target-flavor";
77    public static final String TARGET_TAGS = "target-tags";
78    public static final String TARGET_TAG = "target-tag";
79    public static final String SOURCE_FLAVORS = "source-flavors";
80    public static final String SOURCE_FLAVOR = "source-flavor";
81    public static final String SOURCE_TAG = "source-tag";
82    public static final String SOURCE_TAGS = "source-tags";
83  
84    /**
85     * Activates this component with its properties once all of the collaborating services have been set
86     *
87     * @param cc
88     *          The component's context, containing the properties used for configuration
89     */
90    @Activate
91    protected void activate(ComponentContext cc) {
92      this.id = (String) cc.getProperties().get(WorkflowService.WORKFLOW_OPERATION_PROPERTY);
93      this.description = (String) cc.getProperties().get(Constants.SERVICE_DESCRIPTION);
94    }
95  
96    /**
97     * {@inheritDoc}
98     *
99     * @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(
100    *      org.opencastproject.workflow.api.WorkflowInstance, JobContext)
101    */
102   @Override
103   public abstract WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context)
104           throws WorkflowOperationException;
105 
106   /**
107    * {@inheritDoc}
108    *
109    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#skip(
110    *      org.opencastproject.workflow.api.WorkflowInstance, JobContext)
111    */
112   @Override
113   public WorkflowOperationResult skip(WorkflowInstance workflowInstance, JobContext context)
114           throws WorkflowOperationException {
115     return createResult(Action.SKIP);
116   }
117 
118   /**
119    * {@inheritDoc}
120    *
121    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#destroy(
122    *      org.opencastproject.workflow.api.WorkflowInstance, JobContext)
123    */
124   @Override
125   public void destroy(WorkflowInstance workflowInstance, JobContext context) throws WorkflowOperationException {
126   }
127 
128   /**
129    * Converts a comma separated string into a set of values. Useful for converting operation configuration strings into
130    * multi-valued sets.
131    *
132    * @param elements
133    *          The comma space separated string
134    * @return the set of values
135    */
136   protected List<String> asList(String elements) {
137     elements = StringUtils.trimToEmpty(elements);
138     List<String> list = new ArrayList<>();
139     for (String s : StringUtils.split(elements, ",")) {
140       if (StringUtils.trimToNull(s) != null) {
141         list.add(s.trim());
142       }
143     }
144     return list;
145   }
146 
147   /**
148    * Generates a filename using the base name of a source element and the extension of a derived element.
149    *
150    * @param source
151    *          the source media package element
152    * @param derived
153    *          the derived media package element
154    * @return the filename
155    */
156   protected String getFileNameFromElements(MediaPackageElement source, MediaPackageElement derived) {
157     String fileName = FilenameUtils.getBaseName(source.getURI().getPath());
158     String fileExtension = FilenameUtils.getExtension(derived.getURI().getPath());
159     return fileName + "." + fileExtension;
160   }
161 
162   /**
163    * {@inheritDoc}
164    *
165    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#getId()
166    */
167   @Override
168   public String getId() {
169     return id;
170   }
171 
172   /**
173    * {@inheritDoc}
174    *
175    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#getDescription()
176    */
177   @Override
178   public String getDescription() {
179     return description;
180   }
181 
182   /**
183    * Creates a result for the execution of this workflow operation handler.
184    *
185    * @param action
186    *          the action to take
187    * @return the result
188    */
189   protected WorkflowOperationResult createResult(Action action) {
190     return createResult(null, null, action, 0);
191   }
192 
193   /**
194    * Creates a result for the execution of this workflow operation handler.
195    *
196    * @param mediaPackage
197    *          the modified mediapackage
198    * @param action
199    *          the action to take
200    * @return the result
201    */
202   protected WorkflowOperationResult createResult(MediaPackage mediaPackage, Action action) {
203     return createResult(mediaPackage, null, action, 0);
204   }
205 
206   /**
207    * Creates a result for the execution of this workflow operation handler.
208    * <p>
209    * Since there is no way for the workflow service to determine the queuing time (e. g. waiting on services), it needs
210    * to be provided by the handler.
211    *
212    * @param mediaPackage
213    *          the modified mediapackage
214    * @param action
215    *          the action to take
216    * @param timeInQueue
217    *          the amount of time this handle spent waiting for services
218    * @return the result
219    */
220   protected WorkflowOperationResult createResult(MediaPackage mediaPackage, Action action, long timeInQueue) {
221     return createResult(mediaPackage, null, action, timeInQueue);
222   }
223 
224   /**
225    * Creates a result for the execution of this workflow operation handler.
226    * <p>
227    * Since there is no way for the workflow service to determine the queuing time (e. g. waiting on services), it needs
228    * to be provided by the handler.
229    *
230    * @param mediaPackage
231    *          the modified mediapackage
232    * @param properties
233    *          the properties to add to the workflow instance
234    * @param action
235    *          the action to take
236    * @param timeInQueue
237    *          the amount of time this handle spent waiting for services
238    * @return the result
239    */
240   protected WorkflowOperationResult createResult(MediaPackage mediaPackage, Map<String, String> properties,
241           Action action, long timeInQueue) {
242     return new WorkflowOperationResultImpl(mediaPackage, properties, action, timeInQueue);
243   }
244 
245   /**
246    * Sets the service registry. This method is here as a convenience for developers that need the registry to do job
247    * waiting.
248    *
249    * @param serviceRegistry
250    *          the service registry
251    */
252   @Reference
253   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
254     this.serviceRegistry = serviceRegistry;
255   }
256 
257   /**
258    * Waits until all of the jobs have reached either one of these statuses:
259    * <ul>
260    * <li>{@link Job.Status#FINISHED}</li>
261    * <li>{@link Job.Status#FAILED}</li>
262    * <li>{@link Job.Status#DELETED}</li>
263    * </ul>
264    * After that, the method returns with the actual outcomes of the jobs.
265    *
266    * @param jobs
267    *          the jobs
268    * @return the jobs and their outcomes
269    * @throws IllegalStateException
270    *           if the service registry has not been set
271    * @throws IllegalArgumentException
272    *           if the jobs collecion is either <code>null</code> or empty
273    */
274   protected JobBarrier.Result waitForStatus(Job... jobs) throws IllegalStateException, IllegalArgumentException {
275     return waitForStatus(0, jobs);
276   }
277 
278   /**
279    * Waits until all of the jobs have reached either one of these statuses:
280    * <ul>
281    * <li>{@link Job.Status#FINISHED}</li>
282    * <li>{@link Job.Status#FAILED}</li>
283    * <li>{@link Job.Status#DELETED}</li>
284    * </ul>
285    * After that, the method returns with the actual outcomes of the jobs.
286    *
287    * @param timeout
288    *          the maximum amount of time in milliseconds to wait
289    * @param jobs
290    *          the jobs
291    * @return the jobs and their outcomes
292    * @throws IllegalStateException
293    *           if the service registry has not been set
294    * @throws IllegalArgumentException
295    *           if the jobs collection is either <code>null</code> or empty
296    */
297   protected JobBarrier.Result waitForStatus(long timeout, Job... jobs) throws IllegalStateException,
298           IllegalArgumentException {
299     if (serviceRegistry == null) {
300       throw new IllegalStateException("Can't wait for job status without providing a service registry first");
301     }
302     JobBarrier barrier = new JobBarrier(null, serviceRegistry, jobBarrierPollingInterval, jobs);
303     return barrier.waitForJobs(timeout);
304   }
305 
306   /**
307    * Get a configuration option.
308    *
309    * @deprecated use {@link #getConfig(WorkflowInstance, String)} or
310    *             {@link #getOptConfig(org.opencastproject.workflow.api.WorkflowInstance, String)}
311    */
312   protected Optional<String> getCfg(WorkflowInstance wi, String key) {
313     return Optional.ofNullable(wi.getCurrentOperation().getConfiguration(key));
314   }
315 
316   /**
317    * Get a mandatory configuration key. Values are returned trimmed.
318    *
319    * @throws WorkflowOperationException
320    *         if the configuration key is either missing or empty
321    */
322   protected String getConfig(WorkflowInstance wi, String key) throws WorkflowOperationException {
323     return getConfig(wi.getCurrentOperation(), key);
324   }
325 
326   /**
327    * Get a configuration key. Values are returned trimmed.
328    *
329    * @param w
330    *        WorkflowInstance with current operation
331    * @param key
332    *        Configuration key to check for
333    * @param defaultValue
334    *        Value to return if key does not exists
335    */
336   protected String getConfig(WorkflowInstance w, String key, String defaultValue) {
337     Optional<String> cfgOpt = getOptConfig(w.getCurrentOperation(), key);
338     if (cfgOpt.isPresent()) {
339       return cfgOpt.get();
340     }
341     return defaultValue;
342   }
343 
344   /**
345    * Get a mandatory configuration key. Values are returned trimmed.
346    *
347    * @throws WorkflowOperationException
348    *         if the configuration key is either missing or empty
349    */
350   protected String getConfig(WorkflowOperationInstance woi, String key) throws WorkflowOperationException {
351     Optional<String> cfgOpt = getOptConfig(woi, key);
352     if (cfgOpt.isPresent()) {
353       return cfgOpt.get();
354     }
355     throw new WorkflowOperationException(format("Configuration key '%s' is either missing or empty", key));
356   }
357 
358   /**
359    * Get an optional configuration key. Values are returned trimmed.
360    */
361   protected Optional<String> getOptConfig(WorkflowInstance wi, String key) {
362     return getOptConfig(wi.getCurrentOperation(), key);
363   }
364 
365   /**
366    * Get an optional configuration key. Values are returned trimmed.
367    */
368   protected Optional<String> getOptConfig(WorkflowOperationInstance woi, String key) {
369     return Optional.ofNullable(woi.getConfiguration(key))
370         .map(String::trim)
371         .filter(s -> !s.isEmpty());
372   }
373 
374   /**
375    * Uses one {@code configuration} for both source tags and flavors, in case the WOH does not care whether the source
376    * entities are identified by tags or flavors.
377    *
378    * @see AbstractWorkflowOperationHandler#getTagsAndFlavors(WorkflowInstance, Configuration, Configuration,
379    *      Configuration, Configuration)
380    */
381   protected ConfiguredTagsAndFlavors getTagsAndFlavors(WorkflowInstance workflow, Configuration srcTagsAndFlavors,
382       Configuration targetTags, Configuration targetFlavors) throws WorkflowOperationException {
383     ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(
384         workflow,
385         Configuration.many,
386         Configuration.many,
387         targetTags,
388         targetFlavors);
389 
390     switch(srcTagsAndFlavors) {
391       case none:
392         if (!tagsAndFlavors.getSrcFlavors().isEmpty() || !tagsAndFlavors.getSrcTags().isEmpty()) {
393           throw new WorkflowOperationException("No source tags or flavors may be set.");
394         }
395         break;
396       case one:
397         if (tagsAndFlavors.getSrcFlavors().size() + tagsAndFlavors.getSrcTags().size() != 1) {
398           throw new WorkflowOperationException("Exactly one source tag or flavor must be set.");
399         }
400         break;
401       case atLeastOne:
402         if (tagsAndFlavors.getSrcFlavors().size() + tagsAndFlavors.getSrcTags().size() < 1) {
403           throw new WorkflowOperationException("At least one source tag or flavor must be set.");
404         }
405         break;
406       case many:
407         break;
408       default:
409         throw new WorkflowOperationException("Couldn't process srcTagsAndFlavors configuration option!");
410     }
411 
412     return tagsAndFlavors;
413   }
414 
415   /**
416    * Returns a ConfiguredTagsAndFlavors instance, which includes all specified source/target tags and flavors if they
417    * are valid.
418    * Lists can be empty, if no values were specified! This is to enable WOHs to individually check if a given tag/flavor
419    * was set.
420    * This also means that you should use Configuration.many as parameter, if a tag/flavor is optional.
421    * None: Must not be specified
422    * One: Exactly one must be specified
423    * AtLeastOne: At least one must be specified
424    * Many: An arbitrary amount may be specified
425    * @param srcTags none, one, atLeastOne or many
426    * @param srcFlavors none, one, atLeastOne or many
427    * @param targetFlavors none, one, atLeastOne or many
428    * @param targetTags none, one, atLeastOne or many
429    * @return ConfiguredTagsAndFlavors object including lists for the configured tags/flavors
430    */
431   protected ConfiguredTagsAndFlavors getTagsAndFlavors(WorkflowInstance workflow, Configuration srcTags,
432       Configuration srcFlavors, Configuration targetTags, Configuration targetFlavors)
433           throws WorkflowOperationException {
434     WorkflowOperationInstance operation = workflow.getCurrentOperation();
435     ConfiguredTagsAndFlavors tagsAndFlavors = new ConfiguredTagsAndFlavors();
436     MediaPackageElementFlavor flavor;
437 
438     List<String> srcTagList = new ArrayList<>();
439     switch(srcTags) {
440       case none:
441         break;
442       case one:
443         String srcTag = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAG));
444         if (srcTag == null) {
445           throw new WorkflowOperationException("Configuration key '" + SOURCE_TAG + "' must be set");
446         }
447         srcTagList.add(srcTag);
448         break;
449       case atLeastOne:
450         srcTagList = getTags(operation, SOURCE_TAGS, SOURCE_TAG);
451         if (srcTagList.isEmpty()) {
452           throw new WorkflowOperationException("Configuration key '" + SOURCE_TAGS + "' or '" + SOURCE_TAG
453               + "' must be set");
454         }
455         break;
456       case many:
457         srcTagList = getTags(operation, SOURCE_TAGS, SOURCE_TAG);
458         break;
459       default:
460         throw new WorkflowOperationException("Couldn't process srcTags configuration option!");
461     }
462     tagsAndFlavors.setSrcTags(srcTagList);
463 
464     List<MediaPackageElementFlavor> srcFlavorList = new ArrayList<>();
465     switch(srcFlavors) {
466       case none:
467         break;
468       case one:
469         String singleSourceFlavor = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR));
470         if (singleSourceFlavor == null) {
471           throw new WorkflowOperationException("Configuration key '" + SOURCE_FLAVOR + "' must be set");
472         }
473         try {
474           flavor = MediaPackageElementFlavor.parseFlavor(singleSourceFlavor);
475         } catch (IllegalArgumentException e) {
476           throw new WorkflowOperationException(singleSourceFlavor + " is not a valid flavor!");
477         }
478         srcFlavorList.add(flavor);
479         break;
480       case atLeastOne:
481         srcFlavorList = getFlavors(operation, SOURCE_FLAVORS, SOURCE_FLAVOR);
482         if (srcFlavorList.isEmpty()) {
483           throw new WorkflowOperationException("Configuration key '" + SOURCE_FLAVORS + "' or '" + SOURCE_FLAVOR
484               + "' must be set");
485         }
486         break;
487       case many:
488         srcFlavorList = getFlavors(operation, SOURCE_FLAVORS, SOURCE_FLAVOR);
489         break;
490       default:
491         throw new WorkflowOperationException("Couldn't process srcFlavors configuration option!");
492     }
493     tagsAndFlavors.setSrcFlavors(srcFlavorList);
494 
495     ConfiguredTagsAndFlavors.TargetTags targetTagMap = new ConfiguredTagsAndFlavors.TargetTags();
496     List<String> targetTagList = new ArrayList<>();
497     switch(targetTags) {
498       case none:
499         break;
500       case one:
501         String targetTag = StringUtils.trimToNull(operation.getConfiguration(TARGET_TAG));
502         if (targetTag == null) {
503           throw new WorkflowOperationException("Configuration key '" + TARGET_TAG + "' must be set");
504         }
505         targetTagMap = parseTargetTagsByType(List.of(targetTag));
506         break;
507       case atLeastOne:
508         targetTagList = getTags(operation, TARGET_TAGS, TARGET_TAG);
509         if (targetTagList.isEmpty()) {
510           throw new WorkflowOperationException("Configuration key '" + TARGET_TAGS + "' or '" + TARGET_TAG
511               + "' must be set");
512         }
513         targetTagMap = parseTargetTagsByType(targetTagList);
514         break;
515       case many:
516         targetTagList = getTags(operation, TARGET_TAGS, TARGET_TAG);
517         targetTagMap = parseTargetTagsByType(targetTagList);
518         break;
519       default:
520         throw new WorkflowOperationException("Couldn't process target-tag configuration option!");
521     }
522     tagsAndFlavors.setTargetTags(targetTagMap);
523 
524     List<MediaPackageElementFlavor> targetFlavorList = new ArrayList<>();
525     switch(targetFlavors) {
526       case none:
527         break;
528       case one:
529         String singleTargetFlavor = StringUtils.trimToNull(operation.getConfiguration(TARGET_FLAVOR));
530         if (singleTargetFlavor == null) {
531           throw new WorkflowOperationException("Configuration key '" + TARGET_FLAVOR + "' must be set");
532         }
533         try {
534           flavor = MediaPackageElementFlavor.parseFlavor(singleTargetFlavor);
535         } catch (IllegalArgumentException e) {
536           throw new WorkflowOperationException(singleTargetFlavor + " is not a valid flavor!");
537         }
538         targetFlavorList.add(flavor);
539         break;
540       case atLeastOne:
541         targetFlavorList = getFlavors(operation, TARGET_FLAVORS, TARGET_FLAVOR);
542         if (targetTagList.isEmpty()) {
543           throw new WorkflowOperationException("Configuration key '" + TARGET_FLAVORS + "' or '" + TARGET_FLAVOR
544               + "' must be set");
545         }
546         break;
547       case many:
548         targetFlavorList = getFlavors(operation, TARGET_FLAVORS, TARGET_FLAVOR);
549         break;
550       default:
551         throw new WorkflowOperationException("Couldn't process targetFlavors configuration option!");
552     }
553     tagsAndFlavors.setTargetFlavors(targetFlavorList);
554     return tagsAndFlavors;
555   }
556 
557   private ConfiguredTagsAndFlavors.TargetTags parseTargetTagsByType(List<String> tags) {
558     final String plus = "+";
559     final String minus = "-";
560     List<String> overrideTags = new ArrayList();
561     List<String> addTags = new ArrayList();
562     List<String> removeTags = new ArrayList();
563 
564     for (String targetTag : tags) {
565       if (!StringUtils.startsWithAny(targetTag, plus, minus)) {
566         if (addTags.size() > 0
567             || removeTags.size() > 0) {
568           logger.warn("You may not mix override tags and tag changes. "
569               + "The list of override tags so far is {}. "
570               + "The tag {} is not prefixed with '{}' or '{}'.", overrideTags, targetTag, plus, minus);
571         }
572         overrideTags.add(targetTag);
573       } else if (StringUtils.startsWith(targetTag, plus)) {
574         addTags.add(StringUtils.substring(targetTag, 1));
575       } else if (StringUtils.startsWith(targetTag, minus)) {
576         removeTags.add(StringUtils.substring(targetTag, 1));
577       }
578     }
579 
580     return new ConfiguredTagsAndFlavors.TargetTags(overrideTags, addTags, removeTags);
581   }
582 
583   /**
584    * Helper function that applies target tags to the given element, based on the type(s) of the tag(s)
585    * @param targetTags The target tags to apply to the element
586    * @param element The element the target tags are applied to
587    * @return The element with the applied target tags
588    */
589   protected <T extends MediaPackageElement> T applyTargetTagsToElement(
590       ConfiguredTagsAndFlavors.TargetTags targetTags,
591       T element
592   ) {
593     // set tags on target element
594     List<String> overrideTags = targetTags.getOverrideTags();
595     List<String> addTags = targetTags.getAddTags();
596     List<String> removeTags = targetTags.getRemoveTags();
597     if (overrideTags.size() > 0) {
598       element.clearTags();
599       for (String tag : overrideTags) {
600         element.addTag(tag);
601       }
602     } else {
603       for (String tag : removeTags) {
604         element.removeTag(tag);
605       }
606       for (String tag : addTags) {
607         element.addTag(tag);
608       }
609     }
610 
611     return element;
612   }
613 
614   private List<String> getTags(WorkflowOperationInstance operation, String multipleTagsKey, String singleTagKey) {
615     List<String> tagList = asList(StringUtils.trimToNull(operation.getConfiguration(multipleTagsKey)));
616     String singleTag = StringUtils.trimToNull(operation.getConfiguration(singleTagKey));
617     if (tagList.isEmpty() && singleTag != null) {
618       tagList.add(singleTag);
619     }
620     return tagList;
621   }
622 
623   private List<MediaPackageElementFlavor> getFlavors(WorkflowOperationInstance operation, String multipleFlavorsKey,
624       String singleFlavorKey) throws WorkflowOperationException {
625     List<MediaPackageElementFlavor> flavorList = new ArrayList<>();
626     List<String> singleFlavorString = asList(StringUtils.trimToNull(operation.getConfiguration(multipleFlavorsKey)));
627     String singleSourceFlavor = StringUtils.trimToNull(operation.getConfiguration(singleFlavorKey));
628     if (singleFlavorString.isEmpty() && singleSourceFlavor != null) {
629       singleFlavorString.add(singleSourceFlavor);
630     }
631     for (String elem : singleFlavorString) {
632       try {
633         MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(elem);
634         flavorList.add(flavor);
635       } catch (IllegalArgumentException e) {
636         throw new WorkflowOperationException(elem + " is not a valid flavor!");
637       }
638     }
639     return flavorList;
640   }
641 
642   /**
643    * Set the @link org.opencastproject.job.api.JobBarrier polling interval.
644    * <p>
645    * While waiting for other jobs to finish, the barrier will poll the status of these jobs until they are finished. To
646    * reduce load on the system, the polling is done only every x milliseconds. This interval defines the sleep time
647    * between these polls.
648    * <p>
649    * If most cases you want to leave this at its default value. It will make sense, though, to adjust this time if you
650    * know that your job will be exceptionally short. An example of this might be the unit tests where other jobs are
651    * usually mocked. But this setting is not limited to tests and may be a sensible options for other jobs as well.
652    *
653    * @param interval the time in miliseconds between two polling operations
654    *
655    * @see org.opencastproject.job.api.JobBarrier#DEFAULT_POLLING_INTERVAL
656    */
657   public void setJobBarrierPollingInterval(long interval) {
658     this.jobBarrierPollingInterval = interval;
659   }
660 
661   /**
662    * {@inheritDoc}
663    *
664    * @see java.lang.Object#hashCode()
665    */
666   @Override
667   public int hashCode() {
668     return id != null ? id.hashCode() : super.hashCode();
669   }
670 
671   /**
672    * {@inheritDoc}
673    *
674    * @see java.lang.Object#equals(java.lang.Object)
675    */
676   @Override
677   public boolean equals(Object obj) {
678     if (obj instanceof WorkflowOperationHandler) {
679       if (id != null) {
680         return id.equals(((WorkflowOperationHandler) obj).getId());
681       } else {
682         return this == obj;
683       }
684     }
685     return false;
686   }
687 
688   /**
689    * {@inheritDoc}
690    *
691    * @see java.lang.Object#toString()
692    */
693   @Override
694   public String toString() {
695     return getId();
696   }
697 }