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.caption.impl;
23  
24  import static org.opencastproject.util.MimeType.mimeType;
25  
26  import org.opencastproject.caption.api.Caption;
27  import org.opencastproject.caption.api.CaptionConverter;
28  import org.opencastproject.caption.api.CaptionConverterException;
29  import org.opencastproject.caption.api.CaptionService;
30  import org.opencastproject.caption.api.UnsupportedCaptionFormatException;
31  import org.opencastproject.job.api.AbstractJobProducer;
32  import org.opencastproject.job.api.Job;
33  import org.opencastproject.mediapackage.MediaPackageElement;
34  import org.opencastproject.mediapackage.MediaPackageElementBuilder;
35  import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
36  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
37  import org.opencastproject.mediapackage.MediaPackageElementParser;
38  import org.opencastproject.mediapackage.MediaPackageException;
39  import org.opencastproject.security.api.OrganizationDirectoryService;
40  import org.opencastproject.security.api.SecurityService;
41  import org.opencastproject.security.api.UserDirectoryService;
42  import org.opencastproject.serviceregistry.api.ServiceRegistry;
43  import org.opencastproject.serviceregistry.api.ServiceRegistryException;
44  import org.opencastproject.util.IoSupport;
45  import org.opencastproject.util.LoadUtil;
46  import org.opencastproject.util.NotFoundException;
47  import org.opencastproject.workspace.api.Workspace;
48  
49  import org.apache.commons.io.FilenameUtils;
50  import org.apache.commons.io.IOUtils;
51  import org.apache.commons.lang3.StringUtils;
52  import org.osgi.framework.InvalidSyntaxException;
53  import org.osgi.framework.ServiceReference;
54  import org.osgi.service.cm.ConfigurationException;
55  import org.osgi.service.cm.ManagedService;
56  import org.osgi.service.component.ComponentContext;
57  import org.osgi.service.component.annotations.Activate;
58  import org.osgi.service.component.annotations.Component;
59  import org.osgi.service.component.annotations.Reference;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import java.io.ByteArrayInputStream;
64  import java.io.ByteArrayOutputStream;
65  import java.io.File;
66  import java.io.FileInputStream;
67  import java.io.FileNotFoundException;
68  import java.io.IOException;
69  import java.net.URI;
70  import java.util.Arrays;
71  import java.util.Dictionary;
72  import java.util.HashMap;
73  import java.util.List;
74  
75  import javax.activation.FileTypeMap;
76  
77  /**
78   * Implementation of {@link CaptionService}. Uses {@link ComponentContext} to get all registered
79   * {@link CaptionConverter}s. Converters are searched based on <code>caption.format</code> property. If there is no
80   * match for specified input or output format {@link UnsupportedCaptionFormatException} is thrown.
81   *
82   */
83  @Component(
84      immediate = true,
85      service = { CaptionService.class,ManagedService.class },
86      property = {
87          "service.description=Caption Converter Service",
88          "service.pid=org.opencastproject.caption.impl.CaptionServiceImpl"
89      }
90  )
91  public class CaptionServiceImpl extends AbstractJobProducer implements CaptionService, ManagedService {
92  
93    /**
94     * Creates a new caption service.
95     */
96    public CaptionServiceImpl() {
97      super(JOB_TYPE);
98    }
99  
100   /** Logging utility */
101   private static final Logger logger = LoggerFactory.getLogger(CaptionServiceImpl.class);
102 
103   /** List of available operations on jobs */
104   private enum Operation {
105     Convert, ConvertWithLanguage
106   };
107 
108   /** The collection name */
109   public static final String COLLECTION = "captions";
110 
111   /** The load introduced on the system by creating a caption job */
112   public static final float DEFAULT_CAPTION_JOB_LOAD = 0.1f;
113 
114   /** The key to look for in the service configuration file to override the {@link DEFAULT_CAPTION_JOB_LOAD} */
115   public static final String CAPTION_JOB_LOAD_KEY = "job.load.caption";
116 
117   /** The load introduced on the system by creating a caption job */
118   private float captionJobLoad = DEFAULT_CAPTION_JOB_LOAD;
119 
120   /** Reference to workspace */
121   protected Workspace workspace;
122 
123   /** Reference to remote service manager */
124   protected ServiceRegistry serviceRegistry;
125 
126   /** The security service */
127   protected SecurityService securityService = null;
128 
129   /** The user directory service */
130   protected UserDirectoryService userDirectoryService = null;
131 
132   /** The organization directory service */
133   protected OrganizationDirectoryService organizationDirectoryService = null;
134 
135   /** Component context needed for retrieving Converter Engines */
136   protected ComponentContext componentContext = null;
137 
138   /**
139    * Activate this service implementation via the OSGI service component runtime.
140    *
141    * @param componentContext
142    *          the component context
143    */
144   @Override
145   @Activate
146   public void activate(ComponentContext componentContext) {
147     super.activate(componentContext);
148     this.componentContext = componentContext;
149   }
150 
151   /**
152    * {@inheritDoc}
153    *
154    * @see org.opencastproject.caption.api.CaptionService#convert(org.opencastproject.mediapackage.MediaPackageElement,
155    *      java.lang.String, java.lang.String)
156    */
157   @Override
158   public Job convert(MediaPackageElement input, String inputFormat, String outputFormat)
159           throws UnsupportedCaptionFormatException,
160           CaptionConverterException, MediaPackageException {
161 
162     if (input == null) {
163       throw new IllegalArgumentException("Input catalog can't be null");
164     }
165     if (StringUtils.isBlank(inputFormat)) {
166       throw new IllegalArgumentException("Input format is null");
167     }
168     if (StringUtils.isBlank(outputFormat)) {
169       throw new IllegalArgumentException("Output format is null");
170     }
171 
172     try {
173       return serviceRegistry.createJob(JOB_TYPE, Operation.Convert.toString(),
174               Arrays.asList(MediaPackageElementParser.getAsXml(input), inputFormat, outputFormat), captionJobLoad);
175     } catch (ServiceRegistryException e) {
176       throw new CaptionConverterException("Unable to create a job", e);
177     }
178   }
179 
180   /**
181    * {@inheritDoc}
182    *
183    * @see org.opencastproject.caption.api.CaptionService#convert(org.opencastproject.mediapackage.MediaPackageElement,
184    *      java.lang.String, java.lang.String, java.lang.String)
185    */
186   @Override
187   public Job convert(MediaPackageElement input, String inputFormat, String outputFormat, String language)
188           throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException {
189 
190     if (input == null) {
191       throw new IllegalArgumentException("Input catalog can't be null");
192     }
193     if (StringUtils.isBlank(inputFormat)) {
194       throw new IllegalArgumentException("Input format is null");
195     }
196     if (StringUtils.isBlank(outputFormat)) {
197       throw new IllegalArgumentException("Output format is null");
198     }
199     if (StringUtils.isBlank(language)) {
200       throw new IllegalArgumentException("Language format is null");
201     }
202 
203     try {
204       return serviceRegistry.createJob(JOB_TYPE, Operation.ConvertWithLanguage.toString(),
205           Arrays.asList(MediaPackageElementParser.getAsXml(input), inputFormat, outputFormat, language),
206           captionJobLoad);
207     } catch (ServiceRegistryException e) {
208       throw new CaptionConverterException("Unable to create a job", e);
209     }
210   }
211 
212   /**
213    * Converts the captions and returns them in a new catalog.
214    *
215    * @return the converted catalog
216    */
217   protected MediaPackageElement convert(Job job, MediaPackageElement input, String inputFormat, String outputFormat,
218           String language)
219           throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException {
220     try {
221 
222       // check parameters
223       if (input == null) {
224         throw new IllegalArgumentException("Input element can't be null");
225       }
226       if (StringUtils.isBlank(inputFormat)) {
227         throw new IllegalArgumentException("Input format is null");
228       }
229       if (StringUtils.isBlank(outputFormat)) {
230         throw new IllegalArgumentException("Output format is null");
231       }
232 
233       // get input file
234       File captionsFile;
235       try {
236         captionsFile = workspace.get(input.getURI());
237       } catch (NotFoundException e) {
238         throw new CaptionConverterException("Requested media package element " + input + " could not be found.");
239       } catch (IOException e) {
240         throw new CaptionConverterException("Requested media package element " + input + "could not be accessed.");
241       }
242 
243       logger.debug("Atempting to convert from {} to {}...", inputFormat, outputFormat);
244 
245       List<Caption> collection = null;
246       try {
247         collection = importCaptions(captionsFile, inputFormat, language);
248         logger.debug("Parsing to collection succeeded.");
249       } catch (UnsupportedCaptionFormatException e) {
250         throw new UnsupportedCaptionFormatException(inputFormat);
251       } catch (CaptionConverterException e) {
252         throw e;
253       }
254 
255       URI exported;
256       try {
257         exported = exportCaptions(collection,
258                 job.getId() + "." + FilenameUtils.getExtension(captionsFile.getAbsolutePath()), outputFormat, language);
259         logger.debug("Exporting captions succeeding.");
260       } catch (UnsupportedCaptionFormatException e) {
261         throw new UnsupportedCaptionFormatException(outputFormat);
262       } catch (IOException e) {
263         throw new CaptionConverterException("Could not export caption collection.", e);
264       }
265 
266       // create catalog and set properties
267       CaptionConverter converter = getCaptionConverter(outputFormat);
268       MediaPackageElementBuilder elementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
269       MediaPackageElement mpe = elementBuilder.elementFromURI(exported, converter.getElementType(),
270               new MediaPackageElementFlavor(
271                       "captions", outputFormat + (language == null ? "" : "+" + language)));
272       if (mpe.getMimeType() == null) {
273         String[] mimetype = FileTypeMap.getDefaultFileTypeMap().getContentType(exported.getPath()).split("/");
274         mpe.setMimeType(mimeType(mimetype[0], mimetype[1]));
275       }
276      // Don't need to add language tag if it doesn't exist or used for different purpose
277       if (language != null && !isNumeric(language)) {
278         mpe.addTag("lang:" + language);
279       }
280 
281       return mpe;
282 
283     } catch (Exception e) {
284       logger.warn("Error converting captions in " + input, e);
285       if (e instanceof CaptionConverterException) {
286         throw (CaptionConverterException) e;
287       } else if (e instanceof UnsupportedCaptionFormatException) {
288         throw (UnsupportedCaptionFormatException) e;
289       } else {
290         throw new CaptionConverterException(e);
291       }
292     }
293   }
294 
295   /**
296    *
297    * {@inheritDoc}
298    *
299    */
300   @Override
301   public String[] getLanguageList(MediaPackageElement input, String format) throws UnsupportedCaptionFormatException,
302           CaptionConverterException {
303 
304     if (format == null) {
305       throw new UnsupportedCaptionFormatException("<null>");
306     }
307     CaptionConverter converter = getCaptionConverter(format);
308     if (converter == null) {
309       throw new UnsupportedCaptionFormatException(format);
310     }
311 
312     File captions;
313     try {
314       captions = workspace.get(input.getURI());
315     } catch (NotFoundException e) {
316       throw new CaptionConverterException("Requested media package element " + input + " could not be found.");
317     } catch (IOException e) {
318       throw new CaptionConverterException("Requested media package element " + input + "could not be accessed.");
319     }
320 
321     FileInputStream stream = null;
322     String[] languageList;
323     try {
324       stream = new FileInputStream(captions);
325       languageList = converter.getLanguageList(stream);
326     } catch (FileNotFoundException e) {
327       throw new CaptionConverterException("Requested file " + captions + "could not be found.");
328     } finally {
329       IoSupport.closeQuietly(stream);
330     }
331 
332     return languageList == null ? new String[0] : languageList;
333   }
334 
335   /**
336    * Returns all registered CaptionFormats.
337    */
338   protected HashMap<String, CaptionConverter> getAvailableCaptionConverters() {
339     HashMap<String, CaptionConverter> captionConverters = new HashMap<String, CaptionConverter>();
340     ServiceReference[] refs = null;
341     try {
342       refs = componentContext.getBundleContext().getServiceReferences(CaptionConverter.class.getName(), null);
343     } catch (InvalidSyntaxException e) {
344       // should not happen since it is called with null argument
345     }
346 
347     if (refs != null) {
348       for (ServiceReference ref : refs) {
349         CaptionConverter converter = (CaptionConverter) componentContext.getBundleContext().getService(ref);
350         String format = (String) ref.getProperty("caption.format");
351         if (captionConverters.containsKey(format)) {
352           logger.warn("Caption converter with format {} has already been registered. Ignoring second definition.",
353                   format);
354         } else {
355           captionConverters.put((String) ref.getProperty("caption.format"), converter);
356         }
357       }
358     }
359 
360     return captionConverters;
361   }
362 
363   /**
364    * Returns specific {@link CaptionConverter}. Registry is searched based on formatName, so in order for
365    * {@link CaptionConverter} to be found, it has to have <code>caption.format</code> property set with
366    * {@link CaptionConverter} format. If none is found, null is returned, if more than one is found then the first
367    * reference is returned.
368    *
369    * @param formatName
370    *          name of the caption format
371    * @return {@link CaptionConverter} or null if none is found
372    */
373   protected CaptionConverter getCaptionConverter(String formatName) {
374     ServiceReference[] ref = null;
375     try {
376       ref = componentContext.getBundleContext().getServiceReferences(CaptionConverter.class.getName(),
377               "(caption.format=" + formatName + ")");
378     } catch (InvalidSyntaxException e) {
379       throw new RuntimeException(e);
380     }
381     if (ref == null) {
382       logger.warn("No caption format available for {}.", formatName);
383       return null;
384     }
385     if (ref.length > 1) {
386       logger.warn("Multiple references for caption format {}! Returning first service reference.", formatName);
387     }
388     CaptionConverter converter = (CaptionConverter) componentContext.getBundleContext().getService(ref[0]);
389     return converter;
390   }
391 
392   /**
393    * Imports captions using registered converter engine and specified language.
394    *
395    * @param input
396    *          file containing captions
397    * @param inputFormat
398    *          format of imported captions
399    * @param language
400    *          (optional) captions' language
401    * @return {@link List} of parsed captions
402    * @throws UnsupportedCaptionFormatException
403    *           if there is no registered engine for given format
404    * @throws IllegalCaptionFormatException
405    *           if parser encounters exception
406    */
407   private List<Caption> importCaptions(File input, String inputFormat, String language)
408           throws UnsupportedCaptionFormatException, CaptionConverterException {
409     // get input format
410     CaptionConverter converter = getCaptionConverter(inputFormat);
411     if (converter == null) {
412       logger.error("No available caption format found for {}.", inputFormat);
413       throw new UnsupportedCaptionFormatException(inputFormat);
414     }
415 
416     FileInputStream fileStream = null;
417     try {
418       fileStream = new FileInputStream(input);
419       List<Caption> collection = converter.importCaption(fileStream, language);
420       return collection;
421     } catch (FileNotFoundException e) {
422       throw new CaptionConverterException("Could not locate file " + input);
423     } finally {
424       IOUtils.closeQuietly(fileStream);
425     }
426   }
427 
428   /**
429    * Exports captions {@link List} to specified format. Extension is added to exported file name. Throws
430    * {@link UnsupportedCaptionFormatException} if format is not supported.
431    *
432    * @param captions
433    *          {@link {@link List} to be exported
434    * @param outputName
435    *          name under which exported captions will be stored
436    * @param outputFormat
437    *          format of exported collection
438    * @param language
439    *          (optional) captions' language
440    * @throws UnsupportedCaptionFormatException
441    *           if there is no registered engine for given format
442    * @return location of converted captions
443    * @throws IOException
444    *           if exception occurs while writing to output stream
445    */
446   private URI exportCaptions(List<Caption> captions, String outputName, String outputFormat, String language)
447           throws UnsupportedCaptionFormatException, IOException {
448     CaptionConverter converter = getCaptionConverter(outputFormat);
449     if (converter == null) {
450       logger.error("No available caption format found for {}.", outputFormat);
451       throw new UnsupportedCaptionFormatException(outputFormat);
452     }
453 
454     // TODO instead of first writing it all in memory, write it directly to disk
455     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
456     try {
457       converter.exportCaption(outputStream, captions, language);
458     } catch (IOException e) {
459       // since we're writing to memory, this should not happen
460     }
461     ByteArrayInputStream in = new ByteArrayInputStream(outputStream.toByteArray());
462     return workspace.putInCollection(COLLECTION, outputName + "." + converter.getExtension(), in);
463   }
464 
465   private boolean isNumeric(String str) {
466     try {
467       Integer.parseInt(str);
468     } catch (NumberFormatException e) {
469       return false;
470     }
471     return true;
472   }
473   /**
474    * {@inheritDoc}
475    *
476    * @see org.opencastproject.job.api.AbstractJobProducer#process(Job)
477    */
478   @Override
479   protected String process(Job job) throws Exception {
480     Operation op = null;
481     String operation = job.getOperation();
482     List<String> arguments = job.getArguments();
483     try {
484       op = Operation.valueOf(operation);
485 
486       MediaPackageElement catalog = MediaPackageElementParser.getFromXml(arguments.get(0));
487       String inputFormat = arguments.get(1);
488       String outputFormat = arguments.get(2);
489 
490       MediaPackageElement resultingCatalog = null;
491 
492       switch (op) {
493         case Convert:
494           resultingCatalog = convert(job, catalog, inputFormat, outputFormat, null);
495           return MediaPackageElementParser.getAsXml(resultingCatalog);
496         case ConvertWithLanguage:
497           String language = arguments.get(3);
498           resultingCatalog = convert(job, catalog, inputFormat, outputFormat, language);
499           return MediaPackageElementParser.getAsXml(resultingCatalog);
500         default:
501           throw new IllegalStateException("Don't know how to handle operation '" + operation + "'");
502       }
503     } catch (IllegalArgumentException e) {
504       throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'", e);
505     } catch (IndexOutOfBoundsException e) {
506       throw new ServiceRegistryException("This argument list for operation '" + op + "' does not meet expectations", e);
507     } catch (Exception e) {
508       throw new ServiceRegistryException("Error handling operation '" + op + "'", e);
509     }
510   }
511 
512   /**
513    * Setter for workspace via declarative activation
514    */
515   @Reference
516   protected void setWorkspace(Workspace workspace) {
517     this.workspace = workspace;
518   }
519 
520   /**
521    * Setter for remote service manager via declarative activation
522    */
523   @Reference
524   protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
525     this.serviceRegistry = serviceRegistry;
526   }
527 
528   /**
529    * Callback for setting the security service.
530    *
531    * @param securityService
532    *          the securityService to set
533    */
534   @Reference
535   public void setSecurityService(SecurityService securityService) {
536     this.securityService = securityService;
537   }
538 
539   /**
540    * Callback for setting the user directory service.
541    *
542    * @param userDirectoryService
543    *          the userDirectoryService to set
544    */
545   @Reference
546   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
547     this.userDirectoryService = userDirectoryService;
548   }
549 
550   /**
551    * Sets a reference to the organization directory service.
552    *
553    * @param organizationDirectory
554    *          the organization directory
555    */
556   @Reference
557   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
558     this.organizationDirectoryService = organizationDirectory;
559   }
560 
561   /**
562    * {@inheritDoc}
563    *
564    * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService()
565    */
566   @Override
567   protected SecurityService getSecurityService() {
568     return securityService;
569   }
570 
571   /**
572    * {@inheritDoc}
573    *
574    * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService()
575    */
576   @Override
577   protected OrganizationDirectoryService getOrganizationDirectoryService() {
578     return organizationDirectoryService;
579   }
580 
581   /**
582    * {@inheritDoc}
583    *
584    * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService()
585    */
586   @Override
587   protected UserDirectoryService getUserDirectoryService() {
588     return userDirectoryService;
589   }
590 
591   /**
592    * {@inheritDoc}
593    *
594    * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry()
595    */
596   @Override
597   protected ServiceRegistry getServiceRegistry() {
598     return serviceRegistry;
599   }
600 
601   @Override
602   public void updated(@SuppressWarnings("rawtypes") Dictionary properties) throws ConfigurationException {
603     captionJobLoad = LoadUtil.getConfiguredLoadValue(properties, CAPTION_JOB_LOAD_KEY, DEFAULT_CAPTION_JOB_LOAD,
604         serviceRegistry);
605   }
606 
607 }