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.execute.impl;
23
24 import org.opencastproject.execute.api.ExecuteException;
25 import org.opencastproject.execute.api.ExecuteService;
26 import org.opencastproject.job.api.AbstractJobProducer;
27 import org.opencastproject.job.api.Job;
28 import org.opencastproject.mediapackage.MediaPackage;
29 import org.opencastproject.mediapackage.MediaPackageElement;
30 import org.opencastproject.mediapackage.MediaPackageElement.Type;
31 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
32 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
33 import org.opencastproject.mediapackage.MediaPackageElementParser;
34 import org.opencastproject.mediapackage.MediaPackageException;
35 import org.opencastproject.mediapackage.MediaPackageParser;
36 import org.opencastproject.mediapackage.UnsupportedElementException;
37 import org.opencastproject.security.api.OrganizationDirectoryService;
38 import org.opencastproject.security.api.SecurityService;
39 import org.opencastproject.security.api.UserDirectoryService;
40 import org.opencastproject.serviceregistry.api.ServiceRegistry;
41 import org.opencastproject.serviceregistry.api.ServiceRegistryException;
42 import org.opencastproject.util.ConfigurationException;
43 import org.opencastproject.util.IoSupport;
44 import org.opencastproject.util.LoadUtil;
45 import org.opencastproject.util.NotFoundException;
46 import org.opencastproject.workspace.api.Workspace;
47
48 import org.apache.commons.lang3.StringUtils;
49 import org.osgi.framework.BundleContext;
50 import org.osgi.service.cm.ManagedService;
51 import org.osgi.service.component.ComponentContext;
52 import org.osgi.service.component.annotations.Activate;
53 import org.osgi.service.component.annotations.Component;
54 import org.osgi.service.component.annotations.Reference;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import java.io.BufferedReader;
59 import java.io.File;
60 import java.io.FileInputStream;
61 import java.io.IOException;
62 import java.io.InputStreamReader;
63 import java.net.URI;
64 import java.util.ArrayList;
65 import java.util.Arrays;
66 import java.util.Dictionary;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Set;
70 import java.util.regex.Matcher;
71 import java.util.regex.Pattern;
72
73
74
75
76 @Component(
77 immediate = true,
78 service = { ExecuteService.class,ManagedService.class },
79 property = {
80 "service.description=Execute Service",
81 "service.pid=org.opencastproject.execute.impl.ExecuteServiceImpl"
82 }
83 )
84 public class ExecuteServiceImpl extends AbstractJobProducer implements ExecuteService, ManagedService {
85
86 public enum Operation {
87 Execute_Element, Execute_Mediapackage
88 }
89
90
91 private static final Logger logger = LoggerFactory.getLogger(ExecuteServiceImpl.class);
92
93
94 private ServiceRegistry serviceRegistry = null;
95
96
97 protected SecurityService securityService = null;
98
99
100 protected UserDirectoryService userDirectoryService = null;
101
102
103 protected OrganizationDirectoryService organizationDirectoryService = null;
104
105
106 protected Workspace workspace;
107
108
109
110
111
112 protected final Set<String> allowedCommands = new HashSet<String>();
113
114
115 public static final String COMMANDS_ALLOWED_PROPERTY = "commands.allowed";
116
117
118 private BundleContext bundleContext;
119
120
121 @SuppressWarnings("rawtypes")
122 private Dictionary properties = null;
123
124
125 public static final float DEFAULT_EXECUTE_JOB_LOAD = 0.1f;
126
127
128 public static final String EXECUTE_JOB_LOAD_KEY = "job.load.execute";
129
130 private float executeJobLoad = 1.0f;
131
132
133
134
135 public ExecuteServiceImpl() {
136 super(JOB_TYPE);
137 }
138
139
140
141
142
143
144
145 @Override
146 @Activate
147 public void activate(ComponentContext cc) {
148 super.activate(cc);
149
150 properties = cc.getProperties();
151
152 if (properties != null) {
153 String commandString = (String) properties.get(COMMANDS_ALLOWED_PROPERTY);
154 if (StringUtils.isNotBlank(commandString)) {
155 logger.info("Execute Service permitted commands: {}", commandString);
156 for (String command : commandString.split("\\s+")) {
157 allowedCommands.add(command);
158 }
159 }
160 }
161
162 this.bundleContext = cc.getBundleContext();
163 }
164
165
166
167
168
169
170
171
172
173
174
175
176 @Override
177 public Job execute(String exec, String params, MediaPackageElement inElement, String outFileName, Type expectedType,
178 float load) throws ExecuteException, IllegalArgumentException {
179
180 logger.debug("Creating Execute Job for command: {}", exec);
181
182 if (StringUtils.isBlank(exec)) {
183 throw new IllegalArgumentException("The command to execute cannot be null");
184 }
185
186 if (StringUtils.isBlank(params)) {
187 throw new IllegalArgumentException("The command arguments cannot be null");
188 }
189
190 if (inElement == null) {
191 throw new IllegalArgumentException("The input MediaPackage element cannot be null");
192 }
193
194 outFileName = StringUtils.trimToNull(outFileName);
195 if ((outFileName == null) && (expectedType != null) || (outFileName != null) && (expectedType == null)) {
196 throw new IllegalArgumentException("Expected element type and output filename cannot be null");
197 }
198
199 try {
200 List<String> paramList = new ArrayList<String>(5);
201 paramList.add(exec);
202 paramList.add(params);
203 paramList.add(MediaPackageElementParser.getAsXml(inElement));
204 paramList.add(outFileName);
205 paramList.add((expectedType == null) ? null : expectedType.toString());
206
207 return serviceRegistry.createJob(JOB_TYPE, Operation.Execute_Element.toString(), paramList, load);
208
209 } catch (ServiceRegistryException e) {
210 throw new ExecuteException(String.format("Unable to create a job of type '%s'", JOB_TYPE), e);
211 } catch (MediaPackageException e) {
212 throw new ExecuteException("Error serializing an element", e);
213 }
214 }
215
216
217
218
219
220
221
222
223 @Override
224 public Job execute(String exec, String params, MediaPackage mp, String outFileName, Type expectedType, float load)
225 throws ExecuteException {
226 if (StringUtils.isBlank(exec)) {
227 throw new IllegalArgumentException("The command to execute cannot be null");
228 }
229
230 if (StringUtils.isBlank(params)) {
231 throw new IllegalArgumentException("The command arguments cannot be null");
232 }
233
234 if (mp == null) {
235 throw new IllegalArgumentException("The input MediaPackage cannot be null");
236 }
237
238 outFileName = StringUtils.trimToNull(outFileName);
239 if ((outFileName == null) && (expectedType != null) || (outFileName != null) && (expectedType == null)) {
240 throw new IllegalArgumentException("Expected element type and output filename cannot be null");
241 }
242
243 try {
244 List<String> paramList = new ArrayList<String>(5);
245 paramList.add(exec);
246 paramList.add(params);
247 paramList.add(MediaPackageParser.getAsXml(mp));
248 paramList.add(outFileName);
249 paramList.add((expectedType == null) ? null : expectedType.toString());
250
251 return serviceRegistry.createJob(JOB_TYPE, Operation.Execute_Mediapackage.toString(), paramList, load);
252 } catch (ServiceRegistryException e) {
253 throw new ExecuteException(String.format("Unable to create a job of type '%s'", JOB_TYPE), e);
254 }
255 }
256
257
258
259
260
261
262
263
264 @Override
265 protected String process(Job job) throws ExecuteException {
266 List<String> arguments = new ArrayList<String>(job.getArguments());
267
268
269 if (!allowedCommands.contains("*") && !allowedCommands.contains(arguments.get(0))) {
270 throw new ExecuteException("Command '" + arguments.get(0) + "' is not allowed");
271 }
272
273 String outFileName = null;
274 String strAux = null;
275 MediaPackage mp = null;
276 Type expectedType = null;
277 MediaPackageElement element = null;
278 Operation op = null;
279
280 try {
281 op = Operation.valueOf(job.getOperation());
282
283 int nargs = arguments.size();
284
285 if (nargs != 3 && nargs != 5) {
286 throw new IndexOutOfBoundsException(
287 "Incorrect number of parameters for operation execute_" + op + ": " + arguments.size());
288 }
289 if (nargs == 5) {
290 strAux = arguments.remove(4);
291 expectedType = (strAux == null) ? null : Type.valueOf(strAux);
292 outFileName = StringUtils.trimToNull(arguments.remove(3));
293 if ((StringUtils.isNotBlank(outFileName) && (expectedType == null))
294 || (StringUtils.isBlank(outFileName) && (expectedType != null))) {
295 throw new ExecuteException("The output type and filename must be both specified");
296 }
297 outFileName = (outFileName == null) ? null : job.getId() + "_" + outFileName;
298 }
299
300 switch (op) {
301 case Execute_Mediapackage:
302 mp = MediaPackageParser.getFromXml(arguments.remove(2));
303 return doProcess(arguments, mp, outFileName, expectedType);
304 case Execute_Element:
305 element = MediaPackageElementParser.getFromXml(arguments.remove(2));
306 return doProcess(arguments, element, outFileName, expectedType);
307 default:
308 throw new IllegalStateException("Don't know how to handle operation '" + job.getOperation() + "'");
309 }
310
311 } catch (MediaPackageException e) {
312 throw new ExecuteException("Error unmarshalling the input mediapackage/element", e);
313 } catch (IllegalArgumentException e) {
314 throw new ExecuteException("This service can't handle operations of type '" + op + "'", e);
315 } catch (IndexOutOfBoundsException e) {
316 throw new ExecuteException("The argument list for operation '" + op + "' does not meet expectations", e);
317 }
318 }
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335 protected String doProcess(List<String> arguments, MediaPackage mp, String outFileName, Type expectedType)
336 throws ExecuteException {
337
338 String params = arguments.remove(1);
339
340 File outFile = null;
341 MediaPackageElement[] elements = null;
342
343 try {
344 if (outFileName != null) {
345
346 File firstElement = workspace.get(mp.getElements()[0].getURI());
347 outFile = new File(firstElement.getParentFile(), outFileName);
348 }
349
350
351
352
353
354
355
356
357
358
359 Pattern pat = Pattern.compile("#\\{([^\\{\\}\\(\\)]+)(?:\\(([^\\{\\}\\(\\)]+)\\))?\\}");
360
361
362 Matcher matcher = pat.matcher(params);
363 StringBuffer sb = new StringBuffer();
364 while (matcher.find()) {
365
366 if (matcher.group(1).equals("id")) {
367 matcher.appendReplacement(sb, mp.getIdentifier().toString());
368 } else if (matcher.group(1).equals("flavor")) {
369 elements = mp.getElementsByFlavor(MediaPackageElementFlavor.parseFlavor(matcher.group(2)));
370 if (elements.length == 0) {
371 throw new ExecuteException("No elements in the MediaPackage match the flavor '" + matcher.group(2) + "'.");
372 }
373
374 if (elements.length > 1) {
375 logger.warn("Found more than one element with flavor '{}'. Using {} by default...", matcher.group(2),
376 elements[0].getIdentifier());
377 }
378
379 File elementFile = workspace.get(elements[0].getURI());
380 matcher.appendReplacement(sb, elementFile.getAbsolutePath());
381 } else if (matcher.group(1).equals("tags")) {
382 elements = mp.getElementsByTags(Arrays.asList(StringUtils.split(matcher.group(2), ",")));
383
384 if (elements.length == 0) {
385 throw new ExecuteException("No elements in the MediaPackage match the tags '" + matcher.group(2) + "'.");
386 }
387
388 if (elements.length > 1) {
389 logger.warn("Found more than one element with matching tags '{}'. Using {} by default...", matcher.group(2),
390 elements[0].getIdentifier());
391 }
392
393 File elementFile = workspace.get(elements[0].getURI());
394 matcher.appendReplacement(sb, elementFile.getAbsolutePath());
395 } else if (matcher.group(1).equals("out")) {
396 matcher.appendReplacement(sb, outFile.getAbsolutePath());
397 } else if (matcher.group(1).equals("org_id")) {
398 matcher.appendReplacement(sb, securityService.getOrganization().getId());
399 } else if (properties.get(matcher.group(1)) != null) {
400 matcher.appendReplacement(sb, (String) properties.get(matcher.group(1)));
401 } else if (bundleContext.getProperty(matcher.group(1)) != null) {
402 matcher.appendReplacement(sb, bundleContext.getProperty(matcher.group(1)));
403 }
404 }
405 matcher.appendTail(sb);
406 params = sb.toString();
407 } catch (IllegalArgumentException e) {
408 throw new ExecuteException("Tag 'flavor' must specify a valid MediaPackage element flavor.", e);
409 } catch (NotFoundException e) {
410 throw new ExecuteException(
411 "The element '" + elements[0].getURI().toString() + "' does not exist in the workspace.", e);
412 } catch (IOException e) {
413 throw new ExecuteException("Error retrieving MediaPackage element from workspace: '"
414 + elements[0].getURI().toString() + "'.", e);
415 }
416
417 arguments.addAll(splitParameters(params));
418
419 return runCommand(arguments, outFile, expectedType);
420 }
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435 protected String doProcess(List<String> arguments, MediaPackageElement element, String outFileName, Type expectedType)
436 throws ExecuteException {
437
438
439 String params = arguments.remove(1);
440 arguments.addAll(splitParameters(params));
441
442 File outFile = null;
443
444 try {
445
446 File trackFile = workspace.get(element.getURI());
447
448
449 if (outFileName != null) {
450 outFile = new File(trackFile.getParentFile(), outFileName);
451 }
452
453
454 for (int i = 1; i < arguments.size(); i++) {
455 if (arguments.get(i).contains(INPUT_FILE_PATTERN)) {
456 arguments.set(i, arguments.get(i).replace(INPUT_FILE_PATTERN, trackFile.getAbsolutePath()));
457 continue;
458 }
459
460 if (arguments.get(i).contains(OUTPUT_FILE_PATTERN)) {
461 if (outFile != null) {
462 arguments.set(i, arguments.get(i).replace(OUTPUT_FILE_PATTERN, outFile.getAbsolutePath()));
463 continue;
464 } else {
465 logger.error("{} pattern found, but no valid output filename was specified", OUTPUT_FILE_PATTERN);
466 throw new ExecuteException(
467 OUTPUT_FILE_PATTERN + " pattern found, but no valid output filename was specified");
468 }
469 }
470
471 if (arguments.get(i).contains(MP_ID_PATTERN)) {
472 arguments.set(i, arguments.get(i).replace(MP_ID_PATTERN,
473 element.getMediaPackage().getIdentifier().toString()));
474 }
475
476 if (arguments.get(i).contains(ORG_ID_PATTERN)) {
477 arguments.set(i, arguments.get(i).replace(ORG_ID_PATTERN, securityService.getOrganization().getId()));
478 }
479 }
480
481 return runCommand(arguments, outFile, expectedType);
482 } catch (IOException e) {
483 logger.error("Error retrieving file from workspace: {}", element.getURI());
484 throw new ExecuteException("Error retrieving file from workspace: " + element.getURI(), e);
485 } catch (NotFoundException e) {
486 logger.error("Element '{}' cannot be found in the workspace.", element.getURI());
487 throw new ExecuteException("Element " + element.getURI() + " cannot be found in the workspace");
488 }
489 }
490
491 private String runCommand(List<String> command, File outFile, Type expectedType) throws ExecuteException {
492
493 Process p = null;
494 int result = 0;
495
496 try {
497 logger.info("Running command {}", command.get(0));
498 logger.debug("Starting subprocess {} with arguments {}", command.get(0),
499 StringUtils.join(command.subList(1, command.size()), ", "));
500
501 ProcessBuilder pb = new ProcessBuilder(command);
502 pb.redirectErrorStream(true);
503
504 p = pb.start();
505 BufferedReader stdout = new BufferedReader(new InputStreamReader(p.getInputStream()));
506 String line;
507 while ((line = stdout.readLine()) != null) {
508 logger.debug(line);
509 }
510 result = p.waitFor();
511
512 logger.debug("Command {} finished with result {}", command.get(0), result);
513
514 if (result == 0) {
515
516 if (outFile != null) {
517 if (outFile.isFile()) {
518 URI newURI = workspace.putInCollection(ExecuteService.COLLECTION, outFile.getName(),
519 new FileInputStream(outFile));
520 if (outFile.delete()) {
521 logger.debug("Deleted the local copy of the encoded file at {}", outFile.getAbsolutePath());
522 } else {
523 logger.warn("Unable to delete the encoding output at {}", outFile.getAbsolutePath());
524 }
525 return MediaPackageElementParser.getAsXml(MediaPackageElementBuilderFactory.newInstance()
526 .newElementBuilder().elementFromURI(newURI, expectedType, null));
527 } else {
528 throw new ExecuteException("Expected output file does not exist: " + outFile.getAbsolutePath());
529 }
530 }
531 return "";
532 } else {
533 throw new ExecuteException(String.format("Process %s returned error code %d", command.get(0), result));
534 }
535 } catch (InterruptedException e) {
536 throw new ExecuteException("The executor thread has been unexpectedly interrupted", e);
537 } catch (IOException e) {
538
539
540 logger.error("Could not start subprocess {}", command.get(0));
541 throw new ExecuteException("Could not start subprocess: " + command.get(0), e);
542 } catch (UnsupportedElementException e) {
543 throw new ExecuteException("Couldn't create a new MediaPackage element of type " + expectedType.toString(), e);
544 } catch (ConfigurationException e) {
545 throw new ExecuteException("Couldn't instantiate a new MediaPackage element builder", e);
546 } catch (MediaPackageException e) {
547 throw new ExecuteException("Couldn't serialize a new Mediapackage element of type " + expectedType.toString(), e);
548 } finally {
549 IoSupport.closeQuietly(p);
550 }
551 }
552
553
554
555
556
557
558
559 private List<String> splitParameters(String input) {
560
561
562 final String quoteDelim = "(?<!\\\\)\"";
563
564
565 final String spaceDelim = "((?<!\\\\)\\s)+";
566
567 ArrayList<String> parsedInput = new ArrayList<String>();
568 boolean quoted = false;
569
570 for (String token1 : input.split(quoteDelim)) {
571 if (quoted) {
572 parsedInput.add(token1);
573 quoted = false;
574 } else {
575 for (String token2 : token1.split(spaceDelim)) {
576
577 if (!token2.isEmpty()) {
578 parsedInput.add(token2);
579 }
580 }
581 quoted = true;
582 }
583 }
584
585 return parsedInput;
586 }
587
588
589
590
591
592
593
594 @Reference
595 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
596 this.serviceRegistry = serviceRegistry;
597 }
598
599
600
601
602
603
604 @Override
605 protected ServiceRegistry getServiceRegistry() {
606 return serviceRegistry;
607 }
608
609
610
611
612
613
614 @Override
615 protected SecurityService getSecurityService() {
616 return securityService;
617 }
618
619
620
621
622
623
624
625 @Reference
626 public void setSecurityService(SecurityService securityService) {
627 this.securityService = securityService;
628 }
629
630
631
632
633
634
635
636 @Reference
637 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
638 this.userDirectoryService = userDirectoryService;
639 }
640
641
642
643
644
645
646 @Override
647 protected UserDirectoryService getUserDirectoryService() {
648 return userDirectoryService;
649 }
650
651
652
653
654
655
656 @Override
657 protected OrganizationDirectoryService getOrganizationDirectoryService() {
658 return organizationDirectoryService;
659 }
660
661
662
663
664
665
666
667 @Reference
668 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
669 this.organizationDirectoryService = organizationDirectory;
670 }
671
672
673
674
675
676
677 @Reference
678 public void setWorkspace(Workspace workspace) {
679 this.workspace = workspace;
680 }
681
682 @Override
683 public void updated(@SuppressWarnings("rawtypes") Dictionary properties)
684 throws org.osgi.service.cm.ConfigurationException {
685 executeJobLoad = LoadUtil.getConfiguredLoadValue(properties, EXECUTE_JOB_LOAD_KEY, DEFAULT_EXECUTE_JOB_LOAD,
686 serviceRegistry);
687 }
688
689 }