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     if (StringUtils.isBlank(inputFormat))
165       throw new IllegalArgumentException("Input format is null");
166     if (StringUtils.isBlank(outputFormat))
167       throw new IllegalArgumentException("Output format is null");
168 
169     try {
170       return serviceRegistry.createJob(JOB_TYPE, Operation.Convert.toString(),
171               Arrays.asList(MediaPackageElementParser.getAsXml(input), inputFormat, outputFormat), captionJobLoad);
172     } catch (ServiceRegistryException e) {
173       throw new CaptionConverterException("Unable to create a job", e);
174     }
175   }
176 
177   /**
178    * {@inheritDoc}
179    *
180    * @see org.opencastproject.caption.api.CaptionService#convert(org.opencastproject.mediapackage.MediaPackageElement,
181    *      java.lang.String, java.lang.String, java.lang.String)
182    */
183   @Override
184   public Job convert(MediaPackageElement input, String inputFormat, String outputFormat, String language)
185           throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException {
186 
187     if (input == null)
188       throw new IllegalArgumentException("Input catalog can't be null");
189     if (StringUtils.isBlank(inputFormat))
190       throw new IllegalArgumentException("Input format is null");
191     if (StringUtils.isBlank(outputFormat))
192       throw new IllegalArgumentException("Output format is null");
193     if (StringUtils.isBlank(language))
194       throw new IllegalArgumentException("Language format is null");
195 
196     try {
197       return serviceRegistry.createJob(JOB_TYPE, Operation.ConvertWithLanguage.toString(),
198               Arrays.asList(MediaPackageElementParser.getAsXml(input), inputFormat, outputFormat, language), captionJobLoad);
199     } catch (ServiceRegistryException e) {
200       throw new CaptionConverterException("Unable to create a job", e);
201     }
202   }
203 
204   /**
205    * Converts the captions and returns them in a new catalog.
206    *
207    * @return the converted catalog
208    */
209   protected MediaPackageElement convert(Job job, MediaPackageElement input, String inputFormat, String outputFormat,
210           String language)
211           throws UnsupportedCaptionFormatException, CaptionConverterException, MediaPackageException {
212     try {
213 
214       // check parameters
215       if (input == null)
216         throw new IllegalArgumentException("Input element can't be null");
217       if (StringUtils.isBlank(inputFormat))
218         throw new IllegalArgumentException("Input format is null");
219       if (StringUtils.isBlank(outputFormat))
220         throw new IllegalArgumentException("Output format is null");
221 
222       // get input file
223       File captionsFile;
224       try {
225         captionsFile = workspace.get(input.getURI());
226       } catch (NotFoundException e) {
227         throw new CaptionConverterException("Requested media package element " + input + " could not be found.");
228       } catch (IOException e) {
229         throw new CaptionConverterException("Requested media package element " + input + "could not be accessed.");
230       }
231 
232       logger.debug("Atempting to convert from {} to {}...", inputFormat, outputFormat);
233 
234       List<Caption> collection = null;
235       try {
236         collection = importCaptions(captionsFile, inputFormat, language);
237         logger.debug("Parsing to collection succeeded.");
238       } catch (UnsupportedCaptionFormatException e) {
239         throw new UnsupportedCaptionFormatException(inputFormat);
240       } catch (CaptionConverterException e) {
241         throw e;
242       }
243 
244       URI exported;
245       try {
246         exported = exportCaptions(collection,
247                 job.getId() + "." + FilenameUtils.getExtension(captionsFile.getAbsolutePath()), outputFormat, language);
248         logger.debug("Exporting captions succeeding.");
249       } catch (UnsupportedCaptionFormatException e) {
250         throw new UnsupportedCaptionFormatException(outputFormat);
251       } catch (IOException e) {
252         throw new CaptionConverterException("Could not export caption collection.", e);
253       }
254 
255       // create catalog and set properties
256       CaptionConverter converter = getCaptionConverter(outputFormat);
257       MediaPackageElementBuilder elementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
258       MediaPackageElement mpe = elementBuilder.elementFromURI(exported, converter.getElementType(),
259               new MediaPackageElementFlavor(
260                       "captions", outputFormat + (language == null ? "" : "+" + language)));
261       if (mpe.getMimeType() == null) {
262         String[] mimetype = FileTypeMap.getDefaultFileTypeMap().getContentType(exported.getPath()).split("/");
263         mpe.setMimeType(mimeType(mimetype[0], mimetype[1]));
264       }
265      // Don't need to add language tag if it doesn't exist or used for different purpose
266       if (language != null && !isNumeric(language)) {
267         mpe.addTag("lang:" + language);
268       }
269 
270       return mpe;
271 
272     } catch (Exception e) {
273       logger.warn("Error converting captions in " + input, e);
274       if (e instanceof CaptionConverterException) {
275         throw (CaptionConverterException) e;
276       } else if (e instanceof UnsupportedCaptionFormatException) {
277         throw (UnsupportedCaptionFormatException) e;
278       } else {
279         throw new CaptionConverterException(e);
280       }
281     }
282   }
283 
284   /**
285    *
286    * {@inheritDoc}
287    *
288    */
289   @Override
290   public String[] getLanguageList(MediaPackageElement input, String format) throws UnsupportedCaptionFormatException,
291           CaptionConverterException {
292 
293     if (format == null) {
294       throw new UnsupportedCaptionFormatException("<null>");
295     }
296     CaptionConverter converter = getCaptionConverter(format);
297     if (converter == null) {
298       throw new UnsupportedCaptionFormatException(format);
299     }
300 
301     File captions;
302     try {
303       captions = workspace.get(input.getURI());
304     } catch (NotFoundException e) {
305       throw new CaptionConverterException("Requested media package element " + input + " could not be found.");
306     } catch (IOException e) {
307       throw new CaptionConverterException("Requested media package element " + input + "could not be accessed.");
308     }
309 
310     FileInputStream stream = null;
311     String[] languageList;
312     try {
313       stream = new FileInputStream(captions);
314       languageList = converter.getLanguageList(stream);
315     } catch (FileNotFoundException e) {
316       throw new CaptionConverterException("Requested file " + captions + "could not be found.");
317     } finally {
318       IoSupport.closeQuietly(stream);
319     }
320 
321     return languageList == null ? new String[0] : languageList;
322   }
323 
324   /**
325    * Returns all registered CaptionFormats.
326    */
327   protected HashMap<String, CaptionConverter> getAvailableCaptionConverters() {
328     HashMap<String, CaptionConverter> captionConverters = new HashMap<String, CaptionConverter>();
329     ServiceReference[] refs = null;
330     try {
331       refs = componentContext.getBundleContext().getServiceReferences(CaptionConverter.class.getName(), null);
332     } catch (InvalidSyntaxException e) {
333       // should not happen since it is called with null argument
334     }
335 
336     if (refs != null) {
337       for (ServiceReference ref : refs) {
338         CaptionConverter converter = (CaptionConverter) componentContext.getBundleContext().getService(ref);
339         String format = (String) ref.getProperty("caption.format");
340         if (captionConverters.containsKey(format)) {
341           logger.warn("Caption converter with format {} has already been registered. Ignoring second definition.",
342                   format);
343         } else {
344           captionConverters.put((String) ref.getProperty("caption.format"), converter);
345         }
346       }
347     }
348 
349     return captionConverters;
350   }
351 
352   /**
353    * Returns specific {@link CaptionConverter}. Registry is searched based on formatName, so in order for
354    * {@link CaptionConverter} to be found, it has to have <code>caption.format</code> property set with
355    * {@link CaptionConverter} format. If none is found, null is returned, if more than one is found then the first
356    * reference is returned.
357    *
358    * @param formatName
359    *          name of the caption format
360    * @return {@link CaptionConverter} or null if none is found
361    */
362   protected CaptionConverter getCaptionConverter(String formatName) {
363     ServiceReference[] ref = null;
364     try {
365       ref = componentContext.getBundleContext().getServiceReferences(CaptionConverter.class.getName(),
366               "(caption.format=" + formatName + ")");
367     } catch (InvalidSyntaxException e) {
368       throw new RuntimeException(e);
369     }
370     if (ref == null) {
371       logger.warn("No caption format available for {}.", formatName);
372       return null;
373     }
374     if (ref.length > 1)
375       logger.warn("Multiple references for caption format {}! Returning first service reference.", formatName);
376     CaptionConverter converter = (CaptionConverter) componentContext.getBundleContext().getService(ref[0]);
377     return converter;
378   }
379 
380   /**
381    * Imports captions using registered converter engine and specified language.
382    *
383    * @param input
384    *          file containing captions
385    * @param inputFormat
386    *          format of imported captions
387    * @param language
388    *          (optional) captions' language
389    * @return {@link List} of parsed captions
390    * @throws UnsupportedCaptionFormatException
391    *           if there is no registered engine for given format
392    * @throws IllegalCaptionFormatException
393    *           if parser encounters exception
394    */
395   private List<Caption> importCaptions(File input, String inputFormat, String language)
396           throws UnsupportedCaptionFormatException, CaptionConverterException {
397     // get input format
398     CaptionConverter converter = getCaptionConverter(inputFormat);
399     if (converter == null) {
400       logger.error("No available caption format found for {}.", inputFormat);
401       throw new UnsupportedCaptionFormatException(inputFormat);
402     }
403 
404     FileInputStream fileStream = null;
405     try {
406       fileStream = new FileInputStream(input);
407       List<Caption> collection = converter.importCaption(fileStream, language);
408       return collection;
409     } catch (FileNotFoundException e) {
410       throw new CaptionConverterException("Could not locate file " + input);
411     } finally {
412       IOUtils.closeQuietly(fileStream);
413     }
414   }
415 
416   /**
417    * Exports captions {@link List} to specified format. Extension is added to exported file name. Throws
418    * {@link UnsupportedCaptionFormatException} if format is not supported.
419    *
420    * @param captions
421    *          {@link {@link List} to be exported
422    * @param outputName
423    *          name under which exported captions will be stored
424    * @param outputFormat
425    *          format of exported collection
426    * @param language
427    *          (optional) captions' language
428    * @throws UnsupportedCaptionFormatException
429    *           if there is no registered engine for given format
430    * @return location of converted captions
431    * @throws IOException
432    *           if exception occurs while writing to output stream
433    */
434   private URI exportCaptions(List<Caption> captions, String outputName, String outputFormat, String language)
435           throws UnsupportedCaptionFormatException, IOException {
436     CaptionConverter converter = getCaptionConverter(outputFormat);
437     if (converter == null) {
438       logger.error("No available caption format found for {}.", outputFormat);
439       throw new UnsupportedCaptionFormatException(outputFormat);
440     }
441 
442     // TODO instead of first writing it all in memory, write it directly to disk
443     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
444     try {
445       converter.exportCaption(outputStream, captions, language);
446     } catch (IOException e) {
447       // since we're writing to memory, this should not happen
448     }
449     ByteArrayInputStream in = new ByteArrayInputStream(outputStream.toByteArray());
450     return workspace.putInCollection(COLLECTION, outputName + "." + converter.getExtension(), in);
451   }
452 
453   private boolean isNumeric(String str) {
454     try {
455       Integer.parseInt(str);
456     } catch (NumberFormatException e) {
457       return false;
458     }
459     return true;
460   }
461   /**
462    * {@inheritDoc}
463    *
464    * @see org.opencastproject.job.api.AbstractJobProducer#process(Job)
465    */
466   @Override
467   protected String process(Job job) throws Exception {
468     Operation op = null;
469     String operation = job.getOperation();
470     List<String> arguments = job.getArguments();
471     try {
472       op = Operation.valueOf(operation);
473 
474       MediaPackageElement catalog = MediaPackageElementParser.getFromXml(arguments.get(0));
475       String inputFormat = arguments.get(1);
476       String outputFormat = arguments.get(2);
477 
478       MediaPackageElement resultingCatalog = null;
479 
480       switch (op) {
481         case Convert:
482           resultingCatalog = convert(job, catalog, inputFormat, outputFormat, null);
483           return MediaPackageElementParser.getAsXml(resultingCatalog);
484         case ConvertWithLanguage:
485           String language = arguments.get(3);
486           resultingCatalog = convert(job, catalog, inputFormat, outputFormat, language);
487           return MediaPackageElementParser.getAsXml(resultingCatalog);
488         default:
489           throw new IllegalStateException("Don't know how to handle operation '" + operation + "'");
490       }
491     } catch (IllegalArgumentException e) {
492       throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'", e);
493     } catch (IndexOutOfBoundsException e) {
494       throw new ServiceRegistryException("This argument list for operation '" + op + "' does not meet expectations", e);
495     } catch (Exception e) {
496       throw new ServiceRegistryException("Error handling operation '" + op + "'", e);
497     }
498   }
499 
500   /**
501    * Setter for workspace via declarative activation
502    */
503   @Reference
504   protected void setWorkspace(Workspace workspace) {
505     this.workspace = workspace;
506   }
507 
508   /**
509    * Setter for remote service manager via declarative activation
510    */
511   @Reference
512   protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
513     this.serviceRegistry = serviceRegistry;
514   }
515 
516   /**
517    * Callback for setting the security service.
518    *
519    * @param securityService
520    *          the securityService to set
521    */
522   @Reference
523   public void setSecurityService(SecurityService securityService) {
524     this.securityService = securityService;
525   }
526 
527   /**
528    * Callback for setting the user directory service.
529    *
530    * @param userDirectoryService
531    *          the userDirectoryService to set
532    */
533   @Reference
534   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
535     this.userDirectoryService = userDirectoryService;
536   }
537 
538   /**
539    * Sets a reference to the organization directory service.
540    *
541    * @param organizationDirectory
542    *          the organization directory
543    */
544   @Reference
545   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
546     this.organizationDirectoryService = organizationDirectory;
547   }
548 
549   /**
550    * {@inheritDoc}
551    *
552    * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService()
553    */
554   @Override
555   protected SecurityService getSecurityService() {
556     return securityService;
557   }
558 
559   /**
560    * {@inheritDoc}
561    *
562    * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService()
563    */
564   @Override
565   protected OrganizationDirectoryService getOrganizationDirectoryService() {
566     return organizationDirectoryService;
567   }
568 
569   /**
570    * {@inheritDoc}
571    *
572    * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService()
573    */
574   @Override
575   protected UserDirectoryService getUserDirectoryService() {
576     return userDirectoryService;
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   @Override
590   public void updated(@SuppressWarnings("rawtypes") Dictionary properties) throws ConfigurationException {
591     captionJobLoad = LoadUtil.getConfiguredLoadValue(properties, CAPTION_JOB_LOAD_KEY, DEFAULT_CAPTION_JOB_LOAD, serviceRegistry);
592   }
593 
594 }