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.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   * Implements a service that runs CLI commands with MediaPackage elements as arguments
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    /** The logging facility */
91    private static final Logger logger = LoggerFactory.getLogger(ExecuteServiceImpl.class);
92  
93    /** Reference to the receipt service */
94    private ServiceRegistry serviceRegistry = null;
95  
96    /** The security service */
97    protected SecurityService securityService = null;
98  
99    /** The user directory service */
100   protected UserDirectoryService userDirectoryService = null;
101 
102   /** The organization directory service */
103   protected OrganizationDirectoryService organizationDirectoryService = null;
104 
105   /** The workspace service */
106   protected Workspace workspace;
107 
108   /**
109    * List of allowed commands that can be run with an executor. By convention, an empty set doesn't mean any command can
110    * be run. An '*' in the service configuration means any command can be executed
111    */
112   protected final Set<String> allowedCommands = new HashSet<String>();
113 
114   /** Bundle property specifying which commands can be run with this executor */
115   public static final String COMMANDS_ALLOWED_PROPERTY = "commands.allowed";
116 
117   /** To allow command-line parameter substitutions configured globally i.e. in config.properties */
118   private BundleContext bundleContext;
119 
120   /** To allow command-line parameter substitutions configured at the service level */
121   @SuppressWarnings("rawtypes")
122   private Dictionary properties = null;
123 
124   /** The approximate load placed on the system by running an execute operation */
125   public static final float DEFAULT_EXECUTE_JOB_LOAD = 0.1f;
126 
127   /** The key to look for in the service configuration file to override the {@link DEFAULT_EXECUTE_JOB_LOAD} */
128   public static final String EXECUTE_JOB_LOAD_KEY = "job.load.execute";
129 
130   private float executeJobLoad = 1.0f;
131 
132   /**
133    * Creates a new instance of the execute service.
134    */
135   public ExecuteServiceImpl() {
136     super(JOB_TYPE);
137   }
138 
139   /**
140    * Activates this component with its properties once all of the collaborating services have been set
141    *
142    * @param cc
143    *          The component's context, containing the properties used for configuration
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    * {@inheritDoc}
166    *
167    * @see org.opencastproject.execute.api.ExecuteService#execute(java.lang.String, java.lang.String,
168    *      org.opencastproject.mediapackage.MediaPackageElement, java.lang.String,
169    *      org.opencastproject.mediapackage.MediaPackageElement.Type, float)
170    * @throws IllegalArgumentException
171    *           if the input arguments are incorrect
172    * @throws ExecuteException
173    *           if an internal error occurs
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    * {@inheritDoc}
213    *
214    * @see org.opencastproject.execute.api.ExecuteService#execute(java.lang.String, java.lang.String,
215    *      org.opencastproject.mediapackage.MediaPackage, java.lang.String,
216    *      org.opencastproject.mediapackage.MediaPackageElement.Type, float)
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    * {@inheritDoc}
250    *
251    * @throws ExecuteException
252    *
253    * @see org.opencastproject.job.api.AbstractJobProducer#process(org.opencastproject.job.api.Job)
254    */
255   @Override
256   protected String process(Job job) throws ExecuteException {
257     List<String> arguments = new ArrayList<String>(job.getArguments());
258 
259     // Check this operation is allowed
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    * Does the actual processing, given a mediapackage (Execute Once WOH)
312    *
313    * @param arguments
314    *          The list containing the program and its arguments
315    * @param mp
316    *          MediaPackage used in the operation
317    * @param outFileName
318    *          The name of the resulting file
319    * @param expectedType
320    *          The expected element type
321    * @return A {@code String} containing the command output
322    * @throws ExecuteException
323    *           if some internal error occurred
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         // FIXME : Find a better way to place the output File
336         File firstElement = workspace.get(mp.getElements()[0].getURI());
337         outFile = new File(firstElement.getParentFile(), outFileName);
338       }
339 
340       // Get the substitution pattern.
341       // The following pattern matches, any construct with the form
342       // #{name}
343       // , where 'name' is the value of a certain property. It is stored in the backreference group 1.
344       // Optionally, expressions can take a parameter, like
345       // #{name(parameter)}
346       // , where 'parameter' is the name of a certain parameter.
347       // If specified, 'parameter' is stored in the group 2. Otherwise it's null.
348       // Both name and parameter match any character sequence that does not contain {, }, ( or ) .
349       Pattern pat = Pattern.compile("#\\{([^\\{\\}\\(\\)]+)(?:\\(([^\\{\\}\\(\\)]+)\\))?\\}");
350 
351       // Substitute the appearances of the patterns with the actual absolute paths
352       Matcher matcher = pat.matcher(params);
353       StringBuffer sb = new StringBuffer();
354       while (matcher.find()) {
355         // group(1) = property. group(2) = (optional) parameter
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    * Does the actual processing, given a mediapackage element (Execute Many WOH)
410    *
411    * @param arguments
412    *          The list containing the program and its arguments
413    * @param outFileName
414    *          The name of the resulting file
415    * @param expectedType
416    *          The expected element type
417    * @return A {@code String} containing the command output
418    * @throws ExecuteException
419    *           if some internal error occurred
420    */
421   protected String doProcess(List<String> arguments, MediaPackageElement element, String outFileName, Type expectedType)
422           throws ExecuteException {
423 
424     // arguments(1) contains a list of space-separated arguments for the command
425     String params = arguments.remove(1);
426     arguments.addAll(splitParameters(params));
427 
428     File outFile = null;
429 
430     try {
431       // Get the track file from the workspace
432       File trackFile = workspace.get(element.getURI());
433 
434       // Put the destination file in the same folder as the source file
435       if (outFileName != null)
436         outFile = new File(trackFile.getParentFile(), outFileName);
437 
438       // Substitute the appearances of the patterns with the actual absolute paths
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         // Read the command output
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       // Only log the first argument, the executable, as other arguments may contain sensitive values
522       // e.g. MySQL password/user, paths, etc. that should not be shown to caller
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    * Returns a list of strings broken on whitespace characters except where those whitespace characters are escaped or
538    * quoted.
539    *
540    * @return list of individual arguments
541    */
542   private List<String> splitParameters(String input) {
543 
544     // This delimiter matches any non-escaped quote
545     final String quoteDelim = "(?<!\\\\)\"";
546 
547     // This delimiter matches any number of non-escaped spaces
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           // This ignores empty tokens if quotes are at the beginning or the end of the string
560           if (!token2.isEmpty())
561             parsedInput.add(token2);
562         quoted = true;
563       }
564 
565     return parsedInput;
566   }
567 
568   /**
569    * Sets the receipt service
570    *
571    * @param serviceRegistry
572    *          the service registry
573    */
574   @Reference
575   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
576     this.serviceRegistry = serviceRegistry;
577   }
578 
579   /**
580    * {@inheritDoc}
581    *
582    * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry()
583    */
584   @Override
585   protected ServiceRegistry getServiceRegistry() {
586     return serviceRegistry;
587   }
588 
589   /**
590    * {@inheritDoc}
591    *
592    * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService()
593    */
594   @Override
595   protected SecurityService getSecurityService() {
596     return securityService;
597   }
598 
599   /**
600    * Callback for setting the security service.
601    *
602    * @param securityService
603    *          the securityService to set
604    */
605   @Reference
606   public void setSecurityService(SecurityService securityService) {
607     this.securityService = securityService;
608   }
609 
610   /**
611    * Callback for setting the user directory service.
612    *
613    * @param userDirectoryService
614    *          the userDirectoryService to set
615    */
616   @Reference
617   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
618     this.userDirectoryService = userDirectoryService;
619   }
620 
621   /**
622    * {@inheritDoc}
623    *
624    * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService()
625    */
626   @Override
627   protected UserDirectoryService getUserDirectoryService() {
628     return userDirectoryService;
629   }
630 
631   /**
632    * {@inheritDoc}
633    *
634    * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService()
635    */
636   @Override
637   protected OrganizationDirectoryService getOrganizationDirectoryService() {
638     return organizationDirectoryService;
639   }
640 
641   /**
642    * Sets a reference to the organization directory service.
643    *
644    * @param organizationDirectory
645    *          the organization directory
646    */
647   @Reference
648   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
649     this.organizationDirectoryService = organizationDirectory;
650   }
651 
652   /**
653    * Sets a reference to the workspace service.
654    *
655    * @param workspace
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 }