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.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
79
80
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
95
96 public CaptionServiceImpl() {
97 super(JOB_TYPE);
98 }
99
100
101 private static final Logger logger = LoggerFactory.getLogger(CaptionServiceImpl.class);
102
103
104 private enum Operation {
105 Convert, ConvertWithLanguage
106 };
107
108
109 public static final String COLLECTION = "captions";
110
111
112 public static final float DEFAULT_CAPTION_JOB_LOAD = 0.1f;
113
114
115 public static final String CAPTION_JOB_LOAD_KEY = "job.load.caption";
116
117
118 private float captionJobLoad = DEFAULT_CAPTION_JOB_LOAD;
119
120
121 protected Workspace workspace;
122
123
124 protected ServiceRegistry serviceRegistry;
125
126
127 protected SecurityService securityService = null;
128
129
130 protected UserDirectoryService userDirectoryService = null;
131
132
133 protected OrganizationDirectoryService organizationDirectoryService = null;
134
135
136 protected ComponentContext componentContext = null;
137
138
139
140
141
142
143
144 @Override
145 @Activate
146 public void activate(ComponentContext componentContext) {
147 super.activate(componentContext);
148 this.componentContext = componentContext;
149 }
150
151
152
153
154
155
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
179
180
181
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
206
207
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
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
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
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
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
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
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
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
354
355
356
357
358
359
360
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
382
383
384
385
386
387
388
389
390
391
392
393
394
395 private List<Caption> importCaptions(File input, String inputFormat, String language)
396 throws UnsupportedCaptionFormatException, CaptionConverterException {
397
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
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
443 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
444 try {
445 converter.exportCaption(outputStream, captions, language);
446 } catch (IOException e) {
447
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
463
464
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
502
503 @Reference
504 protected void setWorkspace(Workspace workspace) {
505 this.workspace = workspace;
506 }
507
508
509
510
511 @Reference
512 protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
513 this.serviceRegistry = serviceRegistry;
514 }
515
516
517
518
519
520
521
522 @Reference
523 public void setSecurityService(SecurityService securityService) {
524 this.securityService = securityService;
525 }
526
527
528
529
530
531
532
533 @Reference
534 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
535 this.userDirectoryService = userDirectoryService;
536 }
537
538
539
540
541
542
543
544 @Reference
545 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
546 this.organizationDirectoryService = organizationDirectory;
547 }
548
549
550
551
552
553
554 @Override
555 protected SecurityService getSecurityService() {
556 return securityService;
557 }
558
559
560
561
562
563
564 @Override
565 protected OrganizationDirectoryService getOrganizationDirectoryService() {
566 return organizationDirectoryService;
567 }
568
569
570
571
572
573
574 @Override
575 protected UserDirectoryService getUserDirectoryService() {
576 return userDirectoryService;
577 }
578
579
580
581
582
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 }