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