1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
51
52
53 public abstract class AbstractWorkflowOperationHandler implements WorkflowOperationHandler {
54
55
56 private static final Logger logger = LoggerFactory.getLogger(AbstractWorkflowOperationHandler.class);
57
58
59 protected String id = null;
60
61
62 protected String description = null;
63
64
65 protected ServiceRegistry serviceRegistry = null;
66
67
68 private long jobBarrierPollingInterval = JobBarrier.DEFAULT_POLLING_INTERVAL;
69
70
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
86
87
88
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
98
99
100
101
102 @Override
103 public abstract WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context)
104 throws WorkflowOperationException;
105
106
107
108
109
110
111
112 @Override
113 public WorkflowOperationResult skip(WorkflowInstance workflowInstance, JobContext context)
114 throws WorkflowOperationException {
115 return createResult(Action.SKIP);
116 }
117
118
119
120
121
122
123
124 @Override
125 public void destroy(WorkflowInstance workflowInstance, JobContext context) throws WorkflowOperationException {
126 }
127
128
129
130
131
132
133
134
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
149
150
151
152
153
154
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
164
165
166
167 @Override
168 public String getId() {
169 return id;
170 }
171
172
173
174
175
176
177 @Override
178 public String getDescription() {
179 return description;
180 }
181
182
183
184
185
186
187
188
189 protected WorkflowOperationResult createResult(Action action) {
190 return createResult(null, null, action, 0);
191 }
192
193
194
195
196
197
198
199
200
201
202 protected WorkflowOperationResult createResult(MediaPackage mediaPackage, Action action) {
203 return createResult(mediaPackage, null, action, 0);
204 }
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220 protected WorkflowOperationResult createResult(MediaPackage mediaPackage, Action action, long timeInQueue) {
221 return createResult(mediaPackage, null, action, timeInQueue);
222 }
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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
247
248
249
250
251
252 @Reference
253 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
254 this.serviceRegistry = serviceRegistry;
255 }
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274 protected JobBarrier.Result waitForStatus(Job... jobs) throws IllegalStateException, IllegalArgumentException {
275 return waitForStatus(0, jobs);
276 }
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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
308
309
310
311
312 protected Optional<String> getCfg(WorkflowInstance wi, String key) {
313 return Optional.ofNullable(wi.getCurrentOperation().getConfiguration(key));
314 }
315
316
317
318
319
320
321
322 protected String getConfig(WorkflowInstance wi, String key) throws WorkflowOperationException {
323 return getConfig(wi.getCurrentOperation(), key);
324 }
325
326
327
328
329
330
331
332
333
334
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
346
347
348
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
360
361 protected Optional<String> getOptConfig(WorkflowInstance wi, String key) {
362 return getOptConfig(wi.getCurrentOperation(), key);
363 }
364
365
366
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
376
377
378
379
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
417
418
419
420
421
422
423
424
425
426
427
428
429
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
585
586
587
588
589 protected <T extends MediaPackageElement> T applyTargetTagsToElement(
590 ConfiguredTagsAndFlavors.TargetTags targetTags,
591 T element
592 ) {
593
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
644
645
646
647
648
649
650
651
652
653
654
655
656
657 public void setJobBarrierPollingInterval(long interval) {
658 this.jobBarrierPollingInterval = interval;
659 }
660
661
662
663
664
665
666 @Override
667 public int hashCode() {
668 return id != null ? id.hashCode() : super.hashCode();
669 }
670
671
672
673
674
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
690
691
692
693 @Override
694 public String toString() {
695 return getId();
696 }
697 }