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.ingest.impl;
23
24 import static org.apache.commons.lang3.StringUtils.isBlank;
25 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER;
26 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TITLE;
27 import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
28 import static org.opencastproject.security.api.SecurityConstants.GLOBAL_CAPTURE_AGENT_ROLE;
29 import static org.opencastproject.util.JobUtil.waitForJob;
30
31 import org.opencastproject.authorization.xacml.XACMLParsingException;
32 import org.opencastproject.authorization.xacml.XACMLUtils;
33 import org.opencastproject.capture.CaptureParameters;
34 import org.opencastproject.ingest.api.IngestException;
35 import org.opencastproject.ingest.api.IngestService;
36 import org.opencastproject.inspection.api.MediaInspectionService;
37 import org.opencastproject.job.api.AbstractJobProducer;
38 import org.opencastproject.job.api.Job;
39 import org.opencastproject.job.api.Job.Status;
40 import org.opencastproject.mediapackage.Attachment;
41 import org.opencastproject.mediapackage.Catalog;
42 import org.opencastproject.mediapackage.EName;
43 import org.opencastproject.mediapackage.MediaPackage;
44 import org.opencastproject.mediapackage.MediaPackageBuilderFactory;
45 import org.opencastproject.mediapackage.MediaPackageElement;
46 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
47 import org.opencastproject.mediapackage.MediaPackageElementParser;
48 import org.opencastproject.mediapackage.MediaPackageElements;
49 import org.opencastproject.mediapackage.MediaPackageException;
50 import org.opencastproject.mediapackage.MediaPackageParser;
51 import org.opencastproject.mediapackage.MediaPackageSupport;
52 import org.opencastproject.mediapackage.Track;
53 import org.opencastproject.mediapackage.identifier.IdImpl;
54 import org.opencastproject.metadata.dublincore.DCMIPeriod;
55 import org.opencastproject.metadata.dublincore.DublinCore;
56 import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
57 import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
58 import org.opencastproject.metadata.dublincore.DublinCoreValue;
59 import org.opencastproject.metadata.dublincore.DublinCores;
60 import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
61 import org.opencastproject.metadata.dublincore.Precision;
62 import org.opencastproject.scheduler.api.SchedulerException;
63 import org.opencastproject.scheduler.api.SchedulerService;
64 import org.opencastproject.security.api.AccessControlEntry;
65 import org.opencastproject.security.api.AccessControlList;
66 import org.opencastproject.security.api.OrganizationDirectoryService;
67 import org.opencastproject.security.api.Permissions;
68 import org.opencastproject.security.api.SecurityService;
69 import org.opencastproject.security.api.TrustedHttpClient;
70 import org.opencastproject.security.api.UnauthorizedException;
71 import org.opencastproject.security.api.User;
72 import org.opencastproject.security.api.UserDirectoryService;
73 import org.opencastproject.security.util.SecurityUtil;
74 import org.opencastproject.series.api.SeriesException;
75 import org.opencastproject.series.api.SeriesService;
76 import org.opencastproject.serviceregistry.api.ServiceRegistry;
77 import org.opencastproject.serviceregistry.api.ServiceRegistryException;
78 import org.opencastproject.smil.api.util.SmilUtil;
79 import org.opencastproject.userdirectory.UserIdRoleProvider;
80 import org.opencastproject.util.ConfigurationException;
81 import org.opencastproject.util.IoSupport;
82 import org.opencastproject.util.LoadUtil;
83 import org.opencastproject.util.MimeTypes;
84 import org.opencastproject.util.NotFoundException;
85 import org.opencastproject.util.ProgressInputStream;
86 import org.opencastproject.util.XmlSafeParser;
87 import org.opencastproject.util.XmlUtil;
88 import org.opencastproject.util.data.functions.Misc;
89 import org.opencastproject.workflow.api.WorkflowDatabaseException;
90 import org.opencastproject.workflow.api.WorkflowDefinition;
91 import org.opencastproject.workflow.api.WorkflowException;
92 import org.opencastproject.workflow.api.WorkflowInstance;
93 import org.opencastproject.workflow.api.WorkflowService;
94 import org.opencastproject.workingfilerepository.api.WorkingFileRepository;
95
96 import com.google.common.cache.Cache;
97 import com.google.common.cache.CacheBuilder;
98
99 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
100 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
101 import org.apache.commons.io.FilenameUtils;
102 import org.apache.commons.io.IOUtils;
103 import org.apache.commons.lang3.BooleanUtils;
104 import org.apache.commons.lang3.StringUtils;
105 import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
106 import org.apache.http.Header;
107 import org.apache.http.HttpHeaders;
108 import org.apache.http.HttpResponse;
109 import org.apache.http.auth.AuthScope;
110 import org.apache.http.auth.UsernamePasswordCredentials;
111 import org.apache.http.client.CredentialsProvider;
112 import org.apache.http.client.config.AuthSchemes;
113 import org.apache.http.client.methods.HttpGet;
114 import org.apache.http.impl.client.BasicCredentialsProvider;
115 import org.apache.http.impl.client.CloseableHttpClient;
116 import org.apache.http.impl.client.HttpClientBuilder;
117 import org.osgi.service.cm.ManagedService;
118 import org.osgi.service.component.ComponentContext;
119 import org.osgi.service.component.annotations.Activate;
120 import org.osgi.service.component.annotations.Component;
121 import org.osgi.service.component.annotations.Deactivate;
122 import org.osgi.service.component.annotations.Reference;
123 import org.osgi.service.component.annotations.ReferenceCardinality;
124 import org.osgi.service.component.annotations.ReferencePolicy;
125 import org.slf4j.Logger;
126 import org.slf4j.LoggerFactory;
127 import org.xml.sax.SAXException;
128
129 import java.io.BufferedReader;
130 import java.io.IOException;
131 import java.io.InputStream;
132 import java.io.InputStreamReader;
133 import java.net.URI;
134 import java.nio.charset.StandardCharsets;
135 import java.util.ArrayList;
136 import java.util.Arrays;
137 import java.util.Base64;
138 import java.util.Date;
139 import java.util.Dictionary;
140 import java.util.HashMap;
141 import java.util.HashSet;
142 import java.util.List;
143 import java.util.Map;
144 import java.util.Map.Entry;
145 import java.util.Objects;
146 import java.util.Optional;
147 import java.util.Set;
148 import java.util.UUID;
149 import java.util.concurrent.TimeUnit;
150 import java.util.stream.Collectors;
151
152
153
154
155 @Component(
156 immediate = true,
157 service = {
158 IngestService.class,
159 ManagedService.class
160 },
161 property = {
162 "service.description=Ingest Service",
163 "service.pid=org.opencastproject.ingest.impl.IngestServiceImpl"
164 }
165 )
166 public class IngestServiceImpl extends AbstractJobProducer implements IngestService, ManagedService {
167
168
169 private static final Logger logger = LoggerFactory.getLogger(IngestServiceImpl.class);
170
171
172 private static final String PARTIAL_SMIL_NAME = "source_partial.smil";
173
174
175 protected static final String WORKFLOW_DEFINITION_DEFAULT = "org.opencastproject.workflow.default.definition";
176
177
178 protected static final String WORKFLOW_CONFIGURATION_PREFIX = "org.opencastproject.workflow.config.";
179
180
181 public static final String LEGACY_MEDIAPACKAGE_ID_KEY = "org.opencastproject.ingest.legacy.mediapackage.id";
182
183 public static final String JOB_TYPE = "org.opencastproject.ingest";
184
185
186 public static final String INGEST_ZIP = "zip";
187
188
189 public static final String INGEST_TRACK = "track";
190
191
192 public static final String INGEST_TRACK_FROM_URI = "uri-track";
193
194
195 public static final String INGEST_ATTACHMENT = "attachment";
196
197
198 public static final String INGEST_ATTACHMENT_FROM_URI = "uri-attachment";
199
200
201 public static final String INGEST_CATALOG = "catalog";
202
203
204 public static final String INGEST_CATALOG_FROM_URI = "uri-catalog";
205
206
207 public static final float DEFAULT_INGEST_FILE_JOB_LOAD = 0.2f;
208
209
210 public static final float DEFAULT_INGEST_ZIP_JOB_LOAD = 0.2f;
211
212
213 public static final String FILE_JOB_LOAD_KEY = "job.load.ingest.file";
214
215
216 public static final String ZIP_JOB_LOAD_KEY = "job.load.ingest.zip";
217
218
219 public static final String DOWNLOAD_SOURCE = "org.opencastproject.download.source";
220
221
222 public static final String DOWNLOAD_USER = "org.opencastproject.download.user";
223
224
225 public static final String DOWNLOAD_PASSWORD = "org.opencastproject.download.password";
226
227
228 public static final String DOWNLOAD_AUTH_METHOD = "org.opencastproject.download.auth.method";
229
230
231 public static final String DOWNLOAD_AUTH_FORCE_BASIC = "org.opencastproject.download.auth.force_basic";
232
233
234 public static final boolean DEFAULT_ALLOW_SERIES_MODIFICATIONS = false;
235
236
237 public static final boolean DEFAULT_ALLOW_ONLY_NEW_FLAVORS = true;
238
239
240 public static final boolean DEFAULT_SKIP = false;
241
242
243 public static final boolean DEFAULT_DOWNLOAD_AUTH_FORCE_BASIC = false;
244
245
246 public static final int FILENAME_LENGTH_MAX = 75;
247
248
249
250
251 @Deprecated
252 public static final String MODIFY_OPENCAST_SERIES_KEY = "org.opencastproject.series.overwrite";
253
254
255
256
257
258 public static final String ADD_ONLY_NEW_FLAVORS_KEY = "add.only.new.catalogs.attachments.for.existing.events";
259
260
261 public static final String SKIP_CATALOGS_KEY = "skip.catalogs.for.existing.events";
262
263
264 public static final String SKIP_ATTACHMENTS_KEY = "skip.attachments.for.existing.events";
265
266
267 private static final String SERIES_APPENDIX = "add.series.to.event.appendix";
268
269
270 private float ingestFileJobLoad = DEFAULT_INGEST_FILE_JOB_LOAD;
271
272
273 private float ingestZipJobLoad = DEFAULT_INGEST_ZIP_JOB_LOAD;
274
275
276 private static String downloadUser = DOWNLOAD_USER;
277
278
279 private static String downloadPassword = DOWNLOAD_PASSWORD;
280
281
282 private static String downloadAuthMethod = DOWNLOAD_AUTH_METHOD;
283
284
285 private static boolean downloadAuthForceBasic = DEFAULT_DOWNLOAD_AUTH_FORCE_BASIC;
286
287
288 private static String downloadSource = DOWNLOAD_SOURCE;
289
290
291 private WorkflowService workflowService;
292
293
294 private WorkingFileRepository workingFileRepository;
295
296
297 private TrustedHttpClient httpClient;
298
299
300 private SeriesService seriesService;
301
302
303 private DublinCoreCatalogService dublinCoreService;
304
305
306 private ServiceRegistry serviceRegistry;
307
308
309 protected SecurityService securityService = null;
310
311
312 protected UserDirectoryService userDirectoryService = null;
313
314
315 protected OrganizationDirectoryService organizationDirectoryService = null;
316
317
318 private SchedulerService schedulerService = null;
319
320
321 private MediaInspectionService mediaInspectionService = null;
322
323
324
325
326 protected String defaultWorkflowDefinionId;
327
328
329 private Cache<String, Long> partialTrackStartTimes = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.DAYS)
330 .build();
331
332
333
334 protected boolean isAddOnlyNew = DEFAULT_ALLOW_ONLY_NEW_FLAVORS;
335 protected boolean isAllowModifySeries = DEFAULT_ALLOW_SERIES_MODIFICATIONS;
336
337 private boolean skipCatalogs = DEFAULT_SKIP;
338 private boolean skipAttachments = DEFAULT_SKIP;
339
340
341 private String createSeriesAppendix = null;
342
343 protected boolean testMode = false;
344
345
346
347
348 public IngestServiceImpl() {
349 super(JOB_TYPE);
350 }
351
352
353
354
355
356
357
358 @Override
359 @Activate
360 public void activate(ComponentContext cc) {
361 super.activate(cc);
362 logger.info("Ingest Service started.");
363 defaultWorkflowDefinionId = StringUtils.trimToNull(cc.getBundleContext().getProperty(WORKFLOW_DEFINITION_DEFAULT));
364 if (defaultWorkflowDefinionId == null) {
365 defaultWorkflowDefinionId = "schedule-and-upload";
366 }
367 }
368
369
370
371
372 @Deactivate
373 public void deactivate() {
374
375 }
376
377
378
379
380
381
382
383 @Override
384 public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
385
386 if (properties == null) {
387 logger.info("No configuration available, using defaults");
388 return;
389 }
390
391 downloadAuthMethod = StringUtils.trimToEmpty((String)properties.get(DOWNLOAD_AUTH_METHOD));
392 if (!"Digest".equals(downloadAuthMethod) && !"Basic".equals(downloadAuthMethod)) {
393 logger.warn("Download authentication method is neither Digest nor Basic; setting to Digest");
394 downloadAuthMethod = "Digest";
395 }
396 downloadAuthForceBasic = BooleanUtils.toBoolean(Objects.toString(properties.get(DOWNLOAD_AUTH_FORCE_BASIC),
397 BooleanUtils.toStringTrueFalse(DEFAULT_DOWNLOAD_AUTH_FORCE_BASIC)));
398 downloadPassword = StringUtils.trimToEmpty((String)properties.get(DOWNLOAD_PASSWORD));
399 downloadUser = StringUtils.trimToEmpty(((String) properties.get(DOWNLOAD_USER)));
400 downloadSource = StringUtils.trimToEmpty(((String) properties.get(DOWNLOAD_SOURCE)));
401 if (!isBlank(downloadSource) && (isBlank(downloadUser) || isBlank(downloadPassword))) {
402 logger.warn("Configured ingest download source has no configured user or password; deactivating authenticated"
403 + "download");
404 downloadSource = "";
405 }
406
407 skipAttachments = BooleanUtils.toBoolean(Objects.toString(properties.get(SKIP_ATTACHMENTS_KEY),
408 BooleanUtils.toStringTrueFalse(DEFAULT_SKIP)));
409 skipCatalogs = BooleanUtils.toBoolean(Objects.toString(properties.get(SKIP_CATALOGS_KEY),
410 BooleanUtils.toStringTrueFalse(DEFAULT_SKIP)));
411 logger.debug("Skip attachments sent by agents for scheduled events: {}", skipAttachments);
412 logger.debug("Skip metadata catalogs sent by agents for scheduled events: {}", skipCatalogs);
413
414 ingestFileJobLoad = LoadUtil.getConfiguredLoadValue(properties, FILE_JOB_LOAD_KEY, DEFAULT_INGEST_FILE_JOB_LOAD,
415 serviceRegistry);
416 ingestZipJobLoad = LoadUtil.getConfiguredLoadValue(properties, ZIP_JOB_LOAD_KEY, DEFAULT_INGEST_ZIP_JOB_LOAD,
417 serviceRegistry);
418
419 isAllowModifySeries = BooleanUtils.toBoolean(Objects.toString(properties.get(MODIFY_OPENCAST_SERIES_KEY),
420 BooleanUtils.toStringTrueFalse(DEFAULT_ALLOW_SERIES_MODIFICATIONS)));
421 isAddOnlyNew = BooleanUtils.toBoolean(Objects.toString(properties.get(ADD_ONLY_NEW_FLAVORS_KEY),
422 BooleanUtils.toStringTrueFalse(DEFAULT_ALLOW_ONLY_NEW_FLAVORS)));
423 logger.info("Only allow new flavored catalogs and attachments on ingest:'{}'", isAddOnlyNew);
424 logger.info("Allowing series modification:'{}'", isAllowModifySeries);
425 createSeriesAppendix = StringUtils.trimToNull(((String) properties.get(SERIES_APPENDIX)));
426 }
427
428
429
430
431
432
433
434 @Reference
435 public void setHttpClient(TrustedHttpClient httpClient) {
436 this.httpClient = httpClient;
437 }
438
439
440
441
442
443
444
445 @Reference
446 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
447 this.serviceRegistry = serviceRegistry;
448 }
449
450
451
452
453
454
455
456 @Reference
457 public void setMediaInspectionService(MediaInspectionService mediaInspectionService) {
458 this.mediaInspectionService = mediaInspectionService;
459 }
460
461 public WorkflowInstance addZippedMediaPackage(InputStream zipStream)
462 throws IngestException, IOException, MediaPackageException {
463 try {
464 return addZippedMediaPackage(zipStream, null, null);
465 } catch (NotFoundException e) {
466 throw new IllegalStateException("A not found exception was thrown without a lookup");
467 }
468 }
469
470 @Override
471 public WorkflowInstance addZippedMediaPackage(InputStream zipStream, String wd, Map<String, String> workflowConfig)
472 throws MediaPackageException, IOException, IngestException, NotFoundException {
473 try {
474 return addZippedMediaPackage(zipStream, wd, workflowConfig, null);
475 } catch (UnauthorizedException e) {
476 throw new IllegalStateException(e);
477 }
478 }
479
480
481
482
483
484
485
486 @Override
487 public WorkflowInstance addZippedMediaPackage(InputStream zipStream, String workflowDefinitionId,
488 Map<String, String> workflowConfig, Long workflowInstanceId)
489 throws MediaPackageException, IOException, IngestException, NotFoundException, UnauthorizedException {
490
491 Job job = null;
492
493 if (StringUtils.isNotBlank(workflowDefinitionId)) {
494 try {
495 workflowService.getWorkflowDefinitionById(workflowDefinitionId);
496 } catch (WorkflowDatabaseException e) {
497 throw new IngestException(e);
498 } catch (NotFoundException nfe) {
499 logger.warn("Workflow definition {} not found, using default workflow {} instead", workflowDefinitionId,
500 defaultWorkflowDefinionId);
501 workflowDefinitionId = defaultWorkflowDefinionId;
502 }
503 }
504
505 if (workflowInstanceId != null) {
506 logger.warn("Deprecated method! Ingesting zipped mediapackage with workflow {}", workflowInstanceId);
507 } else {
508 logger.info("Ingesting zipped mediapackage");
509 }
510
511 ZipArchiveInputStream zis = null;
512 Set<String> collectionFilenames = new HashSet<>();
513 try {
514
515
516 job = serviceRegistry.createJob(JOB_TYPE, INGEST_ZIP, null, null, false, ingestZipJobLoad);
517 job.setStatus(Status.RUNNING);
518 job = serviceRegistry.updateJob(job);
519
520
521 String wfrCollectionId = Long.toString(job.getId());
522
523 zis = new ZipArchiveInputStream(zipStream);
524 ZipArchiveEntry entry;
525 MediaPackage mp = null;
526 Map<String, URI> uris = new HashMap<>();
527
528
529 int seq = 1;
530
531 String folderName = null;
532
533 boolean hasRootFolder = true;
534
535 while ((entry = zis.getNextZipEntry()) != null) {
536 try {
537 if (entry.isDirectory() || entry.getName().contains("__MACOSX")) {
538 continue;
539 }
540
541 if (entry.getName().endsWith("manifest.xml") || entry.getName().endsWith("index.xml")) {
542
543 final InputStream is = new ZipEntryInputStream(zis, entry.getSize());
544 mp = MediaPackageParser.getFromXml(IOUtils.toString(is, StandardCharsets.UTF_8));
545 } else {
546 logger.info("Storing zip entry {}/{} in working file repository collection '{}'", job.getId(),
547 entry.getName(), wfrCollectionId);
548
549
550 String fileName = FilenameUtils.getBaseName(entry.getName()) + "_" + seq++ + "."
551 + FilenameUtils.getExtension(entry.getName());
552 URI contentUri = workingFileRepository.putInCollection(wfrCollectionId, fileName,
553 new ZipEntryInputStream(zis, entry.getSize()));
554 collectionFilenames.add(fileName);
555
556 String key = entry.getName();
557 uris.put(key, contentUri);
558 logger.info("Zip entry {}/{} stored at {}", job.getId(), entry.getName(), contentUri);
559
560 int pos = entry.getName().indexOf('/');
561 if (pos == -1) {
562
563 hasRootFolder = false;
564 } else if (hasRootFolder && folderName != null && !folderName.equals(entry.getName().substring(0, pos))) {
565
566 hasRootFolder = false;
567 } else if (folderName == null) {
568
569 folderName = entry.getName().substring(0, pos);
570 }
571 }
572 } catch (IOException e) {
573 logger.warn("Unable to process zip entry {}", entry.getName(), e);
574 throw e;
575 }
576 }
577
578 if (mp == null) {
579 throw new MediaPackageException("No manifest found in this zip");
580 }
581
582
583 if (mp.getIdentifier() == null || isBlank(mp.getIdentifier().toString())) {
584 mp.setIdentifier(IdImpl.fromUUID());
585 }
586
587 String mediaPackageId = mp.getIdentifier().toString();
588
589 logger.info("Ingesting mediapackage {} is named '{}'", mediaPackageId, mp.getTitle());
590
591
592 if (mp.getTracks().length == 0) {
593 logger.warn("Mediapackage {} has no media tracks", mediaPackageId);
594 }
595
596
597 for (MediaPackageElement element : mp.elements()) {
598
599 URI uri = uris.get((hasRootFolder ? folderName + "/" : "") + element.getURI().toString());
600
601 if (uri == null) {
602 throw new MediaPackageException("Unable to map element name '" + element.getURI() + "' to workspace uri");
603 }
604 logger.info("Ingested mediapackage element {}/{} located at {}", mediaPackageId, element.getIdentifier(), uri);
605 URI dest = workingFileRepository.moveTo(wfrCollectionId, FilenameUtils.getName(uri.toString()), mediaPackageId,
606 element.getIdentifier(), FilenameUtils.getName(element.getURI().toString()));
607 element.setURI(dest);
608 }
609
610
611 logger.info("Initiating processing of ingested mediapackage {}", mediaPackageId);
612 WorkflowInstance workflowInstance = ingest(mp, workflowDefinitionId, workflowConfig, workflowInstanceId);
613 logger.info("Ingest of mediapackage {} done", mediaPackageId);
614 job.setStatus(Job.Status.FINISHED);
615 return workflowInstance;
616 } catch (ServiceRegistryException e) {
617 throw new IngestException(e);
618 } catch (MediaPackageException e) {
619 job.setStatus(Job.Status.FAILED, Job.FailureReason.DATA);
620 throw e;
621 } catch (Exception e) {
622 if (e instanceof IngestException) {
623 throw (IngestException) e;
624 }
625 throw new IngestException(e);
626 } finally {
627 IOUtils.closeQuietly(zis);
628 finallyUpdateJob(job);
629 for (String filename : collectionFilenames) {
630 workingFileRepository.deleteFromCollection(Long.toString(job.getId()), filename, true);
631 }
632 }
633 }
634
635
636
637
638
639
640 @Override
641 public MediaPackage createMediaPackage() throws MediaPackageException, ConfigurationException {
642 MediaPackage mediaPackage;
643 try {
644 mediaPackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().createNew();
645 } catch (MediaPackageException e) {
646 logger.error("INGEST:Failed to create media package " + e.getLocalizedMessage());
647 throw e;
648 }
649 mediaPackage.setDate(new Date());
650 logger.info("Created mediapackage {}", mediaPackage);
651 return mediaPackage;
652 }
653
654
655
656
657
658
659 @Override
660 public MediaPackage createMediaPackage(String mediaPackageId)
661 throws MediaPackageException, ConfigurationException {
662 MediaPackage mediaPackage;
663 try {
664 mediaPackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder()
665 .createNew(new IdImpl(mediaPackageId));
666 } catch (MediaPackageException e) {
667 logger.error("INGEST:Failed to create media package " + e.getLocalizedMessage());
668 throw e;
669 }
670 mediaPackage.setDate(new Date());
671 logger.info("Created mediapackage {}", mediaPackage);
672 return mediaPackage;
673 }
674
675
676
677
678
679
680
681
682 @Override
683 public MediaPackage addTrack(URI uri, MediaPackageElementFlavor flavor, String[] tags, MediaPackage mediaPackage)
684 throws IOException, IngestException {
685 Job job = null;
686 try {
687 job = serviceRegistry
688 .createJob(
689 JOB_TYPE, INGEST_TRACK_FROM_URI, Arrays.asList(uri.toString(),
690 flavor == null ? null : flavor.toString(), MediaPackageParser.getAsXml(mediaPackage)),
691 null, false, ingestFileJobLoad);
692 job.setStatus(Status.RUNNING);
693 job = serviceRegistry.updateJob(job);
694 String elementId = UUID.randomUUID().toString();
695 logger.info("Start adding track {} from URL {} on mediapackage {}", elementId, uri, mediaPackage);
696 URI newUrl = addContentToRepo(mediaPackage, elementId, uri);
697 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Track,
698 flavor);
699 if (tags != null && tags.length > 0) {
700 MediaPackageElement trackElement = mp.getTrack(elementId);
701 for (String tag : tags) {
702 logger.info("Adding tag: " + tag + " to Element: " + elementId);
703 trackElement.addTag(tag);
704 }
705 }
706
707 job.setStatus(Job.Status.FINISHED);
708 logger.info("Successful added track {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
709 return mp;
710 } catch (IOException e) {
711 throw e;
712 } catch (ServiceRegistryException e) {
713 throw new IngestException(e);
714 } catch (NotFoundException e) {
715 throw new IngestException("Unable to update ingest job", e);
716 } finally {
717 finallyUpdateJob(job);
718 }
719 }
720
721
722
723
724
725
726
727 @Override
728 public MediaPackage addTrack(InputStream in, String fileName, MediaPackageElementFlavor flavor,
729 MediaPackage mediaPackage) throws IOException, IngestException {
730 String[] tags = null;
731 return this.addTrack(in, fileName, flavor, tags, mediaPackage);
732 }
733
734
735
736
737
738
739
740 @Override
741 public MediaPackage addTrack(InputStream in, String fileName, MediaPackageElementFlavor flavor, String[] tags,
742 MediaPackage mediaPackage) throws IOException, IngestException {
743 Job job = null;
744 try {
745 job = serviceRegistry.createJob(JOB_TYPE, INGEST_TRACK, null, null, false, ingestFileJobLoad);
746 job.setStatus(Status.RUNNING);
747 job = serviceRegistry.updateJob(job);
748 String elementId = UUID.randomUUID().toString();
749 logger.info("Start adding track {} from input stream on mediapackage {}", elementId, mediaPackage);
750 if (fileName.length() > FILENAME_LENGTH_MAX) {
751 final String extension = "." + FilenameUtils.getExtension(fileName);
752 final int length = Math.max(0, FILENAME_LENGTH_MAX - extension.length());
753 fileName = fileName.substring(0, length) + extension;
754 }
755 URI newUrl = addContentToRepo(mediaPackage, elementId, fileName, in);
756 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Track,
757 flavor);
758 if (tags != null && tags.length > 0) {
759 MediaPackageElement trackElement = mp.getTrack(elementId);
760 for (String tag : tags) {
761 logger.debug("Adding tag `{}` to element {}", tag, elementId);
762 trackElement.addTag(tag);
763 }
764 }
765
766 job.setStatus(Job.Status.FINISHED);
767 logger.info("Successful added track {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
768 return mp;
769 } catch (IOException e) {
770 throw e;
771 } catch (ServiceRegistryException e) {
772 throw new IngestException(e);
773 } catch (NotFoundException e) {
774 throw new IngestException("Unable to update ingest job", e);
775 } finally {
776 finallyUpdateJob(job);
777 }
778 }
779
780 @Override
781 public MediaPackage addPartialTrack(URI uri, MediaPackageElementFlavor flavor, long startTime,
782 MediaPackage mediaPackage) throws IOException, IngestException {
783 Job job = null;
784 try {
785 job = serviceRegistry.createJob(
786 JOB_TYPE,
787 INGEST_TRACK_FROM_URI,
788 Arrays.asList(uri.toString(), flavor == null ? null : flavor.toString(),
789 MediaPackageParser.getAsXml(mediaPackage)), null, false);
790 job.setStatus(Status.RUNNING);
791 job = serviceRegistry.updateJob(job);
792 String elementId = UUID.randomUUID().toString();
793 logger.info("Start adding partial track {} from URL {} on mediapackage {}", elementId, uri, mediaPackage);
794 URI newUrl = addContentToRepo(mediaPackage, elementId, uri);
795 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Track,
796 flavor);
797 job.setStatus(Job.Status.FINISHED);
798
799 partialTrackStartTimes.put(elementId, startTime);
800 logger.debug("Added start time {} for track {}", startTime, elementId);
801 logger.info("Successful added partial track {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
802 return mp;
803 } catch (ServiceRegistryException e) {
804 throw new IngestException(e);
805 } catch (NotFoundException e) {
806 throw new IngestException("Unable to update ingest job", e);
807 } finally {
808 finallyUpdateJob(job);
809 }
810 }
811
812 @Override
813 public MediaPackage addPartialTrack(InputStream in, String fileName, MediaPackageElementFlavor flavor, long startTime,
814 MediaPackage mediaPackage) throws IOException, IngestException {
815 Job job = null;
816 try {
817 job = serviceRegistry.createJob(JOB_TYPE, INGEST_TRACK, null, null, false);
818 job.setStatus(Status.RUNNING);
819 job = serviceRegistry.updateJob(job);
820 String elementId = UUID.randomUUID().toString();
821 logger.info("Start adding partial track {} from input stream on mediapackage {}", elementId, mediaPackage);
822 URI newUrl = addContentToRepo(mediaPackage, elementId, fileName, in);
823 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Track,
824 flavor);
825 job.setStatus(Job.Status.FINISHED);
826
827 partialTrackStartTimes.put(elementId, startTime);
828 logger.debug("Added start time {} for track {}", startTime, elementId);
829 logger.info("Successful added partial track {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
830 return mp;
831 } catch (ServiceRegistryException e) {
832 throw new IngestException(e);
833 } catch (NotFoundException e) {
834 throw new IngestException("Unable to update ingest job", e);
835 } finally {
836 finallyUpdateJob(job);
837 }
838 }
839
840
841
842
843
844
845
846
847 @Override
848 public MediaPackage addCatalog(URI uri, MediaPackageElementFlavor flavor, String[] tags, MediaPackage mediaPackage)
849 throws IOException, IngestException {
850 Job job = null;
851 try {
852 job = serviceRegistry.createJob(JOB_TYPE, INGEST_CATALOG_FROM_URI,
853 Arrays.asList(uri.toString(), flavor.toString(), MediaPackageParser.getAsXml(mediaPackage)), null, false,
854 ingestFileJobLoad);
855 job.setStatus(Status.RUNNING);
856 job = serviceRegistry.updateJob(job);
857 String elementId = UUID.randomUUID().toString();
858 logger.info("Start adding catalog {} from URL {} on mediapackage {}", elementId, uri, mediaPackage);
859 URI newUrl = addContentToRepo(mediaPackage, elementId, uri);
860 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Catalog,
861 flavor);
862 if (tags != null && tags.length > 0) {
863 MediaPackageElement catalogElement = mp.getCatalog(elementId);
864 for (String tag : tags) {
865 logger.info("Adding tag: " + tag + " to Element: " + elementId);
866 catalogElement.addTag(tag);
867 }
868 }
869 job.setStatus(Job.Status.FINISHED);
870 logger.info("Successful added catalog {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
871 return mp;
872 } catch (ServiceRegistryException e) {
873 throw new IngestException(e);
874 } catch (NotFoundException e) {
875 throw new IngestException("Unable to update ingest job", e);
876 } finally {
877 finallyUpdateJob(job);
878 }
879 }
880
881
882
883
884
885
886
887
888
889
890
891 protected boolean updateSeries(MediaPackage mediaPackage) throws IOException, IngestException {
892 Catalog[] seriesCatalogs = mediaPackage.getCatalogs(MediaPackageElements.SERIES);
893 if (seriesCatalogs.length == 0) {
894 return false;
895 } else if (seriesCatalogs.length > 1) {
896 logger.warn("Mediapackage {} has more than one series dublincore catalogs. Using catalog {} with ID {}.",
897 mediaPackage.getIdentifier(), seriesCatalogs[0].getURI(), seriesCatalogs[0].getIdentifier());
898 }
899
900 HttpResponse response = null;
901 InputStream in = null;
902 boolean isUpdated = false;
903 boolean isNew = false;
904 String seriesId = null;
905 try {
906 HttpGet getDc = new HttpGet(seriesCatalogs[0].getURI());
907 response = httpClient.execute(getDc);
908 in = response.getEntity().getContent();
909 DublinCoreCatalog dc = dublinCoreService.load(in);
910 seriesId = dc.getFirst(DublinCore.PROPERTY_IDENTIFIER);
911 if (seriesId == null) {
912 logger.warn("Series dublin core document contains no identifier, "
913 + "rejecting ingested series catalog for mediapackage {}.", mediaPackage.getIdentifier());
914 } else {
915 try {
916 try {
917 seriesService.getSeries(seriesId);
918 if (isAllowModifySeries) {
919
920 seriesService.updateSeries(dc);
921 isUpdated = true;
922 logger.debug("Ingest is overwriting the existing series {} with the ingested series", seriesId);
923 } else {
924 logger.debug("Series {} already exists. Ignoring series catalog from ingest.", seriesId);
925 }
926 } catch (NotFoundException e) {
927 logger.info("Creating new series {} with default ACL.", seriesId);
928 seriesService.updateSeries(dc);
929 isUpdated = true;
930 isNew = true;
931 }
932
933 } catch (Exception e) {
934 throw new IngestException(e);
935 }
936 }
937 in.close();
938 } catch (IOException e) {
939 logger.error("Error updating series from DublinCoreCatalog.}", e);
940 } finally {
941 IOUtils.closeQuietly(in);
942 httpClient.close(response);
943 }
944 if (!isUpdated) {
945 return isUpdated;
946 }
947
948 for (MediaPackageElement seriesElement : mediaPackage.getElementsByFlavor(
949 MediaPackageElementFlavor.parseFlavor("*/series"))) {
950 if (MediaPackageElement.Type.Catalog == seriesElement.getElementType()
951 && !MediaPackageElements.SERIES.equals(seriesElement.getFlavor())) {
952 String catalogType = seriesElement.getFlavor().getType();
953 logger.info("Apply series {} metadata catalog from mediapackage {} to newly created series {}.",
954 catalogType, mediaPackage.getIdentifier(), seriesId);
955 byte[] data;
956 try {
957 HttpGet getExtendedMetadata = new HttpGet(seriesElement.getURI());
958 response = httpClient.execute(getExtendedMetadata);
959 in = response.getEntity().getContent();
960 data = IOUtils.readFully(in, (int) response.getEntity().getContentLength());
961 } catch (Exception e) {
962 throw new IngestException("Unable to read series " + catalogType + " metadata catalog for series "
963 + seriesId + ".", e);
964 } finally {
965 IOUtils.closeQuietly(in);
966 httpClient.close(response);
967 }
968 try {
969 seriesService.updateSeriesElement(seriesId, catalogType, data);
970 } catch (SeriesException e) {
971 throw new IngestException(
972 "Unable to update series " + catalogType + " catalog on newly created series " + seriesId + ".", e);
973 }
974 }
975 }
976 if (isNew) {
977 logger.info("Apply series ACL from mediapackage {} to newly created series {}.",
978 mediaPackage.getIdentifier(), seriesId);
979 Attachment[] seriesXacmls = mediaPackage.getAttachments(MediaPackageElements.XACML_POLICY_SERIES);
980 if (seriesXacmls.length > 0) {
981 if (seriesXacmls.length > 1) {
982 logger.warn("Mediapackage {} has more than one series xacml attachments. Using {}.",
983 mediaPackage.getIdentifier(), seriesXacmls[0].getURI());
984 }
985 AccessControlList seriesAcl = null;
986 try {
987 HttpGet getXacml = new HttpGet(seriesXacmls[0].getURI());
988 response = httpClient.execute(getXacml);
989 in = response.getEntity().getContent();
990 seriesAcl = XACMLUtils.parseXacml(in);
991 } catch (XACMLParsingException ex) {
992 throw new IngestException("Unable to parse series xacml from mediapackage "
993 + mediaPackage.getIdentifier() + ".", ex);
994 } catch (IOException e) {
995 logger.error("Error updating series {} ACL from mediapackage {}.",
996 seriesId, mediaPackage.getIdentifier(), e);
997 throw e;
998 } finally {
999 IOUtils.closeQuietly(in);
1000 httpClient.close(response);
1001 }
1002 try {
1003 seriesService.updateAccessControl(seriesId, seriesAcl);
1004 } catch (Exception e) {
1005 throw new IngestException("Unable to update series ACL on newly created series " + seriesId + ".", e);
1006 }
1007 }
1008 }
1009 return isUpdated;
1010 }
1011
1012
1013
1014
1015
1016
1017
1018 @Override
1019 public MediaPackage addCatalog(InputStream in, String fileName, MediaPackageElementFlavor flavor,
1020 MediaPackage mediaPackage) throws IOException, IngestException {
1021 return addCatalog(in, fileName, flavor, null, mediaPackage);
1022 }
1023
1024
1025
1026
1027
1028
1029
1030 @Override
1031 public MediaPackage addCatalog(InputStream in, String fileName, MediaPackageElementFlavor flavor, String[] tags,
1032 MediaPackage mediaPackage) throws IOException, IngestException, IllegalArgumentException {
1033 Job job = null;
1034 try {
1035 job = serviceRegistry.createJob(JOB_TYPE, INGEST_CATALOG, null, null, false, ingestFileJobLoad);
1036 job.setStatus(Status.RUNNING);
1037 job = serviceRegistry.updateJob(job);
1038 final String elementId = UUID.randomUUID().toString();
1039 final String mediaPackageId = mediaPackage.getIdentifier().toString();
1040 logger.info("Start adding catalog {} from input stream on mediapackage {}", elementId, mediaPackageId);
1041 final URI newUrl = addContentToRepo(mediaPackage, elementId, fileName, in);
1042
1043 final boolean isJSON;
1044 try (InputStream inputStream = workingFileRepository.get(mediaPackageId, elementId)) {
1045 try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
1046
1047 int firstChar = reader.read();
1048 isJSON = firstChar == '[' || firstChar == '{';
1049 }
1050 }
1051
1052 if (isJSON) {
1053 logger.warn("Input catalog seems to be JSON. This is a mistake and will fail in future Opencast versions."
1054 + "You will likely want to ingest this as a media package attachment instead.");
1055 } else {
1056
1057 try {
1058 XmlSafeParser.parse(workingFileRepository.get(mediaPackageId, elementId));
1059 } catch (SAXException e) {
1060 workingFileRepository.delete(mediaPackageId, elementId);
1061 throw new IllegalArgumentException("Catalog XML is invalid", e);
1062 }
1063 }
1064 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Catalog,
1065 flavor);
1066 if (tags != null && tags.length > 0) {
1067 MediaPackageElement trackElement = mp.getCatalog(elementId);
1068 for (String tag : tags) {
1069 logger.info("Adding tag {} to element {}", tag, elementId);
1070 trackElement.addTag(tag);
1071 }
1072 }
1073
1074 job.setStatus(Job.Status.FINISHED);
1075 logger.info("Successful added catalog {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
1076 return mp;
1077 } catch (ServiceRegistryException e) {
1078 throw new IngestException(e);
1079 } catch (NotFoundException e) {
1080 throw new IngestException("Unable to update ingest job", e);
1081 } finally {
1082 finallyUpdateJob(job);
1083 }
1084 }
1085
1086
1087
1088
1089
1090
1091
1092
1093 @Override
1094 public MediaPackage addAttachment(URI uri, MediaPackageElementFlavor flavor, String[] tags, MediaPackage mediaPackage)
1095 throws IOException, IngestException {
1096 Job job = null;
1097 try {
1098 job = serviceRegistry.createJob(JOB_TYPE, INGEST_ATTACHMENT_FROM_URI,
1099 Arrays.asList(uri.toString(), flavor.toString(), MediaPackageParser.getAsXml(mediaPackage)), null, false,
1100 ingestFileJobLoad);
1101 job.setStatus(Status.RUNNING);
1102 job = serviceRegistry.updateJob(job);
1103 String elementId = UUID.randomUUID().toString();
1104 logger.info("Start adding attachment {} from URL {} on mediapackage {}", elementId, uri, mediaPackage);
1105 URI newUrl = addContentToRepo(mediaPackage, elementId, uri);
1106 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Attachment,
1107 flavor);
1108 if (tags != null && tags.length > 0) {
1109 MediaPackageElement attachmentElement = mp.getAttachment(elementId);
1110 for (String tag : tags) {
1111 logger.debug("Adding tag: " + tag + " to Element: " + elementId);
1112 attachmentElement.addTag(tag);
1113 }
1114 }
1115 job.setStatus(Job.Status.FINISHED);
1116 logger.info("Successful added attachment {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
1117 return mp;
1118 } catch (ServiceRegistryException e) {
1119 throw new IngestException(e);
1120 } catch (NotFoundException e) {
1121 throw new IngestException("Unable to update ingest job", e);
1122 } finally {
1123 finallyUpdateJob(job);
1124 }
1125 }
1126
1127
1128
1129
1130
1131
1132
1133 @Override
1134 public MediaPackage addAttachment(InputStream in, String fileName, MediaPackageElementFlavor flavor, String[] tags,
1135 MediaPackage mediaPackage) throws IOException, IngestException {
1136 Job job = null;
1137 try {
1138 job = serviceRegistry.createJob(JOB_TYPE, INGEST_ATTACHMENT, null, null, false, ingestFileJobLoad);
1139 job.setStatus(Status.RUNNING);
1140 job = serviceRegistry.updateJob(job);
1141 String elementId = UUID.randomUUID().toString();
1142 logger.info("Start adding attachment {} from input stream on mediapackage {}", elementId, mediaPackage);
1143 URI newUrl = addContentToRepo(mediaPackage, elementId, fileName, in);
1144 MediaPackage mp = addContentToMediaPackage(mediaPackage, elementId, newUrl, MediaPackageElement.Type.Attachment,
1145 flavor);
1146 if (tags != null && tags.length > 0) {
1147 MediaPackageElement trackElement = mp.getAttachment(elementId);
1148 for (String tag : tags) {
1149 logger.info("Adding tag: " + tag + " to Element: " + elementId);
1150 trackElement.addTag(tag);
1151 }
1152 }
1153 job.setStatus(Job.Status.FINISHED);
1154 logger.info("Successful added attachment {} on mediapackage {} at URL {}", elementId, mediaPackage, newUrl);
1155 return mp;
1156 } catch (ServiceRegistryException e) {
1157 throw new IngestException(e);
1158 } catch (NotFoundException e) {
1159 throw new IngestException("Unable to update ingest job", e);
1160 } finally {
1161 finallyUpdateJob(job);
1162 }
1163
1164 }
1165
1166
1167
1168
1169
1170
1171
1172 @Override
1173 public MediaPackage addAttachment(InputStream in, String fileName, MediaPackageElementFlavor flavor,
1174 MediaPackage mediaPackage) throws IOException, IngestException {
1175 String[] tags = null;
1176 return addAttachment(in, fileName, flavor, tags, mediaPackage);
1177 }
1178
1179
1180
1181
1182
1183
1184
1185 @Override
1186 public WorkflowInstance ingest(MediaPackage mp) throws IngestException {
1187 try {
1188 return ingest(mp, null, null, null);
1189 } catch (NotFoundException e) {
1190 throw new IngestException(e);
1191 } catch (UnauthorizedException e) {
1192 throw new IllegalStateException(e);
1193 }
1194 }
1195
1196
1197
1198
1199
1200
1201
1202 @Override
1203 public WorkflowInstance ingest(MediaPackage mp, String wd, Map<String, String> properties)
1204 throws IngestException, NotFoundException {
1205 try {
1206 return ingest(mp, wd, properties, null);
1207 } catch (UnauthorizedException e) {
1208 throw new IllegalStateException(e);
1209 }
1210 }
1211
1212
1213
1214
1215
1216
1217
1218 @Override
1219 public WorkflowInstance ingest(MediaPackage mp, String workflowDefinitionId, Map<String, String> properties,
1220 Long workflowInstanceId) throws IngestException, NotFoundException, UnauthorizedException {
1221
1222 mp = checkForLegacyMediaPackageId(mp, properties);
1223
1224 try {
1225 mp = createSmil(mp);
1226 } catch (IOException e) {
1227 throw new IngestException("Unable to add SMIL Catalog", e);
1228 }
1229
1230 try {
1231 updateSeries(mp);
1232 } catch (IOException e) {
1233 throw new IngestException("Unable to create or update series from mediapackage " + mp.getIdentifier() + ".", e);
1234 }
1235
1236
1237 if (workflowInstanceId != null) {
1238 logger.warn(
1239 "Resuming workflow {} with ingested mediapackage {} is deprecated, skip resuming and start new workflow",
1240 workflowInstanceId, mp);
1241 }
1242
1243 if (workflowDefinitionId == null) {
1244 logger.info("Starting a new workflow with ingested mediapackage {} based on the default workflow definition '{}'",
1245 mp, defaultWorkflowDefinionId);
1246 } else {
1247 logger.info("Starting a new workflow with ingested mediapackage {} based on workflow definition '{}'", mp,
1248 workflowDefinitionId);
1249 }
1250
1251 try {
1252
1253 WorkflowDefinition workflowDef = getWorkflowDefinition(workflowDefinitionId, mp);
1254
1255
1256 properties = mergeWorkflowConfiguration(properties, mp.getIdentifier().toString());
1257
1258
1259 properties = removePrefixFromProperties(properties);
1260
1261
1262 mp = mergeScheduledMediaPackage(mp);
1263 if (mp.getSeries() == null) {
1264 mp = checkForCASeries(mp, createSeriesAppendix);
1265 }
1266
1267
1268 if (workflowDef != null) {
1269 logger.info("Starting new workflow with ingested mediapackage '{}' using the specified template '{}'",
1270 mp.getIdentifier().toString(), workflowDefinitionId);
1271 } else {
1272 logger.info("Starting new workflow with ingested mediapackage '{}' using the default template '{}'",
1273 mp.getIdentifier().toString(), defaultWorkflowDefinionId);
1274 }
1275 return workflowService.start(workflowDef, mp, properties);
1276 } catch (WorkflowException e) {
1277 throw new IngestException(e);
1278 }
1279 }
1280
1281 @Override
1282 public void schedule(MediaPackage mediaPackage, String workflowDefinitionID, Map<String, String> properties)
1283 throws IllegalStateException, IngestException, NotFoundException, UnauthorizedException, SchedulerException {
1284 MediaPackageElement[] mediaPackageElements = mediaPackage.getElementsByFlavor(MediaPackageElements.EPISODE);
1285 if (mediaPackageElements.length != 1) {
1286 logger.debug("There can be only one (and exactly one) episode dublin core catalog: https://youtu.be/_J3VeogFUOs");
1287 throw new IngestException("There can be only one (and exactly one) episode dublin core catalog");
1288 }
1289 InputStream inputStream;
1290 DublinCoreCatalog dublinCoreCatalog;
1291 try {
1292 inputStream = workingFileRepository.get(mediaPackage.getIdentifier().toString(),
1293 mediaPackageElements[0].getIdentifier());
1294 dublinCoreCatalog = dublinCoreService.load(inputStream);
1295 } catch (IOException e) {
1296 throw new IngestException(e);
1297 }
1298
1299 EName temporal = new EName(DublinCore.TERMS_NS_URI, "temporal");
1300 List<DublinCoreValue> periods = dublinCoreCatalog.get(temporal);
1301 if (periods.size() != 1) {
1302 logger.debug("There can be only one (and exactly one) period");
1303 throw new IngestException("There can be only one (and exactly one) period");
1304 }
1305 DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(periods.get(0));
1306 if (!period.hasStart() || !period.hasEnd()) {
1307 logger.debug("A scheduled recording needs to have a start and end.");
1308 throw new IngestException("A scheduled recording needs to have a start and end.");
1309 }
1310 EName createdEName = new EName(DublinCore.TERMS_NS_URI, "created");
1311 List<DublinCoreValue> created = dublinCoreCatalog.get(createdEName);
1312 if (created.size() == 0) {
1313 logger.debug("Created not set");
1314 } else if (created.size() == 1) {
1315 Date date = EncodingSchemeUtils.decodeMandatoryDate(created.get(0));
1316 if (date.getTime() != period.getStart().getTime()) {
1317 logger.debug("start and created date differ ({} vs {})", date.getTime(), period.getStart().getTime());
1318 throw new IngestException("Temporal start and created date differ");
1319 }
1320 } else {
1321 logger.debug("There can be only one created date");
1322 throw new IngestException("There can be only one created date");
1323 }
1324 String captureAgent = getCaptureAgent(dublinCoreCatalog);
1325
1326
1327 Map<String, String> agentProperties = new HashMap<>();
1328 Map<String, String> workflowProperties = new HashMap<>();
1329 for (String key : properties.keySet()) {
1330 if (key.startsWith("org.opencastproject.workflow.config.")) {
1331 workflowProperties.put(key, properties.get(key));
1332 } else {
1333 agentProperties.put(key, properties.get(key));
1334 }
1335 }
1336
1337
1338 workflowProperties = removePrefixFromProperties(workflowProperties);
1339
1340 try {
1341 schedulerService.addEvent(period.getStart(), period.getEnd(), captureAgent, new HashSet<>(), mediaPackage,
1342 workflowProperties, agentProperties, Optional.empty());
1343 } finally {
1344 for (MediaPackageElement mediaPackageElement : mediaPackage.getElements()) {
1345 try {
1346 workingFileRepository.delete(mediaPackage.getIdentifier().toString(), mediaPackageElement.getIdentifier());
1347 } catch (IOException e) {
1348 logger.warn("Failed to delete media package element", e);
1349 }
1350 }
1351 }
1352 }
1353
1354 private String getCaptureAgent(DublinCoreCatalog dublinCoreCatalog) throws IngestException {
1355
1356 EName spatial = new EName(DublinCore.TERMS_NS_URI, "spatial");
1357 List<DublinCoreValue> captureAgents = dublinCoreCatalog.get(spatial);
1358 if (captureAgents.size() != 1) {
1359 logger.debug("Exactly one capture agent needs to be set");
1360 throw new IngestException("Exactly one capture agent needs to be set");
1361 }
1362 return captureAgents.get(0).getValue();
1363 }
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374 private MediaPackage checkForLegacyMediaPackageId(MediaPackage mp, Map<String, String> properties)
1375 throws IngestException {
1376 if (properties == null || properties.isEmpty()) {
1377 return mp;
1378 }
1379
1380 try {
1381 String mediaPackageId = properties.get(LEGACY_MEDIAPACKAGE_ID_KEY);
1382 if (StringUtils.isNotBlank(mediaPackageId) && schedulerService != null) {
1383 logger.debug("Check ingested mediapackage {} for legacy mediapackage identifier {}",
1384 mp.getIdentifier().toString(), mediaPackageId);
1385 try {
1386 schedulerService.getMediaPackage(mp.getIdentifier().toString());
1387 return mp;
1388 } catch (NotFoundException e) {
1389 logger.info("No scheduler mediapackage found with ingested id {}, try legacy mediapackage id {}",
1390 mp.getIdentifier().toString(), mediaPackageId);
1391 try {
1392 schedulerService.getMediaPackage(mediaPackageId);
1393 logger.info("Legacy mediapackage id {} exists, change ingested mediapackage id {} to legacy id",
1394 mediaPackageId, mp.getIdentifier().toString());
1395 mp.setIdentifier(new IdImpl(mediaPackageId));
1396 return mp;
1397 } catch (NotFoundException e1) {
1398 logger.info("No scheduler mediapackage found with legacy mediapackage id {}, skip merging", mediaPackageId);
1399 } catch (Exception e1) {
1400 logger.error("Unable to get event mediapackage from scheduler event {}", mediaPackageId, e);
1401 throw new IngestException(e);
1402 }
1403 } catch (Exception e) {
1404 logger.error("Unable to get event mediapackage from scheduler event {}", mp.getIdentifier().toString(), e);
1405 throw new IngestException(e);
1406 }
1407 }
1408 return mp;
1409 } finally {
1410 properties.remove(LEGACY_MEDIAPACKAGE_ID_KEY);
1411 }
1412 }
1413
1414 private Map<String, String> mergeWorkflowConfiguration(Map<String, String> properties, String mediaPackageId) {
1415 if (isBlank(mediaPackageId) || schedulerService == null) {
1416 return properties;
1417 }
1418
1419 HashMap<String, String> mergedProperties = new HashMap<>();
1420
1421 try {
1422 Map<String, String> recordingProperties = schedulerService.getCaptureAgentConfiguration(mediaPackageId);
1423 logger.debug("Restoring workflow properties from scheduler event {}", mediaPackageId);
1424 mergedProperties.putAll(recordingProperties);
1425 } catch (SchedulerException e) {
1426 logger.warn("Unable to get workflow properties from scheduler event {}", mediaPackageId, e);
1427 } catch (NotFoundException e) {
1428 logger.info("No capture event found for id {}", mediaPackageId);
1429 } catch (UnauthorizedException e) {
1430 throw new IllegalStateException(e);
1431 }
1432
1433 if (properties != null) {
1434
1435 logger.debug("Merge workflow properties with the one from the scheduler event {}", mediaPackageId);
1436 mergedProperties.putAll(properties);
1437 }
1438
1439 return mergedProperties;
1440 }
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450 private MediaPackage mergeScheduledMediaPackage(MediaPackage mp) throws IngestException {
1451 if (schedulerService == null) {
1452 logger.warn("No scheduler service available to merge mediapackage!");
1453 return mp;
1454 }
1455
1456 try {
1457 MediaPackage scheduledMp = schedulerService.getMediaPackage(mp.getIdentifier().toString());
1458 logger.info("Found matching scheduled event for id '{}', merging mediapackage...", mp.getIdentifier().toString());
1459 mergeMediaPackageElements(mp, scheduledMp);
1460 mergeMediaPackageMetadata(mp, scheduledMp);
1461 return mp;
1462 } catch (NotFoundException e) {
1463 logger.debug("No scheduler mediapackage found with id {}, skip merging", mp.getIdentifier());
1464 return mp;
1465 } catch (Exception e) {
1466 throw new IngestException(String.format("Unable to get event media package from scheduler event %s",
1467 mp.getIdentifier()), e);
1468 }
1469 }
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481 private void mergeMediaPackageElements(MediaPackage mp, MediaPackage scheduledMp) {
1482
1483 if (skipCatalogs) {
1484 for (MediaPackageElement element : mp.getCatalogs()) {
1485 if (!element.getFlavor().equals(MediaPackageElements.SMIL)) {
1486 mp.remove(element);
1487 }
1488 }
1489 }
1490
1491
1492
1493 if (skipAttachments) {
1494 for (MediaPackageElement element : mp.getAttachments()) {
1495 mp.remove(element);
1496 }
1497 }
1498
1499 for (MediaPackageElement element : scheduledMp.getElements()) {
1500 if (MediaPackageElement.Type.Publication.equals(element.getElementType())) {
1501
1502
1503
1504
1505
1506 logger.debug("Ignoring {}, not adding to ingested mediapackage {}", MediaPackageElement.Type.Publication, mp);
1507 continue;
1508 } else if (mp.getElementsByFlavor(element.getFlavor()).length > 0) {
1509
1510
1511
1512
1513
1514 if (!isAddOnlyNew || MediaPackageElement.Type.Track.equals(element.getElementType())) {
1515
1516 logger.info(
1517 "Omitting Opencast (Asset Managed) element '{}', replacing with ingested element of same flavor '{}'",
1518 element,
1519 element.getFlavor());
1520 continue;
1521 }
1522
1523
1524
1525
1526 for (MediaPackageElement el : mp.getElementsByFlavor(element.getFlavor())) {
1527 logger.info("Omitting ingested element '{}' {}, keeping existing (Asset Managed) element of same flavor '{}'",
1528 el, el.getURI(), element.getFlavor());
1529 mp.remove(el);
1530 }
1531 }
1532 logger.info("Adding element {} from scheduled (Asset Managed) event '{}' into ingested mediapackage",
1533 element, mp);
1534 mp.add(element);
1535 }
1536 }
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553 private void mergeMediaPackageMetadata(MediaPackage mp, MediaPackage scheduledMp) {
1554
1555 boolean noOverwrite = (isAddOnlyNew && !skipCatalogs) || skipCatalogs;
1556 if ((mp.getDate() == null) || noOverwrite) {
1557 mp.setDate(scheduledMp.getDate());
1558 }
1559 if (isBlank(mp.getLicense()) || noOverwrite) {
1560 mp.setLicense(scheduledMp.getLicense());
1561 }
1562 if (isBlank(mp.getSeries()) || noOverwrite) {
1563 mp.setSeries(scheduledMp.getSeries());
1564 }
1565 if (isBlank(mp.getSeriesTitle()) || noOverwrite) {
1566 mp.setSeriesTitle(scheduledMp.getSeriesTitle());
1567 }
1568 if (isBlank(mp.getTitle()) || noOverwrite) {
1569 mp.setTitle(scheduledMp.getTitle());
1570 }
1571
1572 if (mp.getSubjects().length <= 0 || noOverwrite) {
1573 Arrays.stream(mp.getSubjects()).forEach(mp::removeSubject);
1574 for (String subject : scheduledMp.getSubjects()) {
1575 mp.addSubject(subject);
1576 }
1577 }
1578 if (noOverwrite || mp.getContributors().length == 0) {
1579 Arrays.stream(mp.getContributors()).forEach(mp::removeContributor);
1580 for (String contributor : scheduledMp.getContributors()) {
1581 mp.addContributor(contributor);
1582 }
1583 }
1584 if (noOverwrite || mp.getCreators().length == 0) {
1585 Arrays.stream(mp.getCreators()).forEach(mp::removeCreator);
1586 for (String creator : scheduledMp.getCreators()) {
1587 mp.addCreator(creator);
1588 }
1589 }
1590 }
1591
1592
1593
1594
1595
1596
1597
1598
1599 private Map<String, String> removePrefixFromProperties(Map<String, String> properties) {
1600 Map<String, String> fixedProperties = new HashMap<>();
1601 if (properties != null) {
1602 for (Entry<String, String> entry : properties.entrySet()) {
1603 if (entry.getKey().startsWith(WORKFLOW_CONFIGURATION_PREFIX)) {
1604 logger.debug("Removing prefix from key '" + entry.getKey() + " with value '" + entry.getValue() + "'");
1605 fixedProperties.put(entry.getKey().replace(WORKFLOW_CONFIGURATION_PREFIX, ""), entry.getValue());
1606 } else {
1607 fixedProperties.put(entry.getKey(), entry.getValue());
1608 }
1609 }
1610 }
1611 return fixedProperties;
1612 }
1613
1614 private WorkflowDefinition getWorkflowDefinition(String workflowDefinitionID, MediaPackage mediapackage)
1615 throws NotFoundException, WorkflowDatabaseException, IngestException {
1616
1617 if (isBlank(workflowDefinitionID)) {
1618 String mediaPackageId = mediapackage.getIdentifier().toString();
1619 if (schedulerService != null) {
1620 logger.info("Determining workflow template for ingested mediapckage {} from capture event {}", mediapackage,
1621 mediaPackageId);
1622 try {
1623 Map<String, String> recordingProperties = schedulerService.getCaptureAgentConfiguration(mediaPackageId);
1624 workflowDefinitionID = recordingProperties.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION);
1625 if (isBlank(workflowDefinitionID)) {
1626 workflowDefinitionID = defaultWorkflowDefinionId;
1627 logger.debug("No workflow set. Falling back to default.");
1628 }
1629 if (isBlank(workflowDefinitionID)) {
1630 throw new IngestException("No value found for key '" + CaptureParameters.INGEST_WORKFLOW_DEFINITION
1631 + "' from capture event configuration of scheduler event '" + mediaPackageId + "'");
1632 }
1633 logger.info("Ingested event {} will be processed using workflow '{}'", mediapackage, workflowDefinitionID);
1634 } catch (NotFoundException e) {
1635 logger.warn("Specified capture event {} was not found", mediaPackageId);
1636 } catch (UnauthorizedException e) {
1637 throw new IllegalStateException(e);
1638 } catch (SchedulerException e) {
1639 logger.warn("Unable to get the workflow definition id from scheduler event {}", mediaPackageId, e);
1640 throw new IngestException(e);
1641 }
1642 } else {
1643 logger.warn(
1644 "Scheduler service not bound, unable to determine the workflow template to use for ingested "
1645 + "mediapackage {}", mediapackage);
1646 }
1647
1648 } else {
1649 logger.info("Ingested mediapackage {} is processed using workflow template '{}', specified during ingest",
1650 mediapackage, workflowDefinitionID);
1651 }
1652
1653
1654 if (isBlank(workflowDefinitionID) && defaultWorkflowDefinionId != null) {
1655 logger.info("Using default workflow definition '{}' to process ingested mediapackage {}",
1656 defaultWorkflowDefinionId, mediapackage);
1657 workflowDefinitionID = defaultWorkflowDefinionId;
1658 }
1659
1660
1661 if (StringUtils.isNotBlank(workflowDefinitionID) && StringUtils.isNotBlank(defaultWorkflowDefinionId)) {
1662 try {
1663 workflowService.getWorkflowDefinitionById(workflowDefinitionID);
1664 } catch (WorkflowDatabaseException e) {
1665 throw new IngestException(e);
1666 } catch (NotFoundException nfe) {
1667 logger.warn("Workflow definition {} not found, using default workflow {} instead", workflowDefinitionID,
1668 defaultWorkflowDefinionId);
1669 workflowDefinitionID = defaultWorkflowDefinionId;
1670 }
1671 }
1672
1673
1674 if (isBlank(workflowDefinitionID)) {
1675 throw new IllegalStateException("Can not ingest a workflow without a workflow definition or an existing "
1676 + "instance. No default definition is specified");
1677 }
1678
1679
1680 return workflowService.getWorkflowDefinitionById(workflowDefinitionID);
1681 }
1682
1683
1684
1685
1686
1687
1688
1689
1690 @Override
1691 public void discardMediaPackage(MediaPackage mp) throws IOException {
1692 String mediaPackageId = mp.getIdentifier().toString();
1693 for (MediaPackageElement element : mp.getElements()) {
1694 if (!workingFileRepository.delete(mediaPackageId, element.getIdentifier())) {
1695 logger.warn("Unable to find (and hence, delete), this mediapackage element");
1696 }
1697 }
1698 logger.info("Successfully discarded media package {}", mp);
1699 }
1700
1701 protected URI addContentToRepo(MediaPackage mp, String elementId, URI uri) throws IOException {
1702 InputStream in = null;
1703 HttpResponse response = null;
1704 CloseableHttpClient externalHttpClient = null;
1705 try {
1706 if (uri.toString().startsWith("http")) {
1707 HttpGet get = new HttpGet(uri);
1708
1709 if (!isBlank(downloadSource) && uri.toString().matches(downloadSource)) {
1710
1711 externalHttpClient = getAuthedHttpClient();
1712 get.setHeader("X-Requested-Auth", downloadAuthMethod);
1713 if ("Basic".equals(downloadAuthMethod) && downloadAuthForceBasic) {
1714 String auth = downloadUser + ":" + downloadPassword;
1715 byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.ISO_8859_1));
1716 String authHeader = "Basic " + new String(encodedAuth);
1717 get.setHeader(HttpHeaders.AUTHORIZATION, authHeader);
1718 }
1719 response = externalHttpClient.execute(get);
1720 } else {
1721
1722 response = httpClient.execute(get);
1723 }
1724
1725 if (null == response) {
1726
1727
1728 throw new IOException("Null response object from the http client, refer to code for explanation");
1729 }
1730
1731 int httpStatusCode = response.getStatusLine().getStatusCode();
1732 if (httpStatusCode != 200) {
1733 throw new IOException(uri + " returns http " + httpStatusCode);
1734 }
1735 in = response.getEntity().getContent();
1736
1737 } else if (!uri.toString().startsWith("file") || testMode) {
1738 in = uri.toURL().openStream();
1739 } else {
1740 throw new IOException("Refusing to fetch files from the local filesystem");
1741 }
1742 String fileName = FilenameUtils.getName(uri.getPath());
1743 if (isBlank(FilenameUtils.getExtension(fileName))) {
1744 fileName = getContentDispositionFileName(response);
1745 }
1746
1747 if (isBlank(FilenameUtils.getExtension(fileName))) {
1748 throw new IOException("No filename extension found: " + fileName);
1749 }
1750 return addContentToRepo(mp, elementId, fileName, in);
1751 } finally {
1752 if (in != null) {
1753 in.close();
1754 }
1755 if (externalHttpClient != null) {
1756 externalHttpClient.close();
1757 }
1758 httpClient.close(response);
1759 }
1760 }
1761
1762 private String getContentDispositionFileName(HttpResponse response) {
1763 if (response == null) {
1764 return null;
1765 }
1766
1767 Header header = response.getFirstHeader("Content-Disposition");
1768 ContentDisposition contentDisposition = new ContentDisposition(header.getValue());
1769 return contentDisposition.getParameter("filename");
1770 }
1771
1772 private URI addContentToRepo(MediaPackage mp, String elementId, String filename, InputStream file)
1773 throws IOException {
1774 ProgressInputStream progressInputStream = new ProgressInputStream(file);
1775 return workingFileRepository.put(mp.getIdentifier().toString(), elementId, filename, progressInputStream);
1776 }
1777
1778 private MediaPackage addContentToMediaPackage(MediaPackage mp, String elementId, URI uri,
1779 MediaPackageElement.Type type, MediaPackageElementFlavor flavor) {
1780 logger.info("Adding element of type {} to mediapackage {}", type, mp);
1781 MediaPackageElement mpe = mp.add(uri, type, flavor);
1782 mpe.setIdentifier(elementId);
1783 return mp;
1784 }
1785
1786
1787
1788
1789 @Reference
1790 public void setWorkflowService(WorkflowService workflowService) {
1791 this.workflowService = workflowService;
1792 }
1793
1794 @Reference
1795 public void setWorkingFileRepository(WorkingFileRepository workingFileRepository) {
1796 this.workingFileRepository = workingFileRepository;
1797 }
1798
1799 @Reference
1800 public void setSeriesService(SeriesService seriesService) {
1801 this.seriesService = seriesService;
1802 }
1803
1804 @Reference
1805 public void setDublinCoreService(DublinCoreCatalogService dublinCoreService) {
1806 this.dublinCoreService = dublinCoreService;
1807 }
1808
1809
1810
1811
1812
1813
1814 @Override
1815 protected ServiceRegistry getServiceRegistry() {
1816 return serviceRegistry;
1817 }
1818
1819
1820
1821
1822
1823
1824 @Override
1825 protected String process(Job job) throws Exception {
1826 throw new IllegalStateException("Ingest jobs are not expected to be dispatched");
1827 }
1828
1829
1830
1831
1832
1833
1834
1835 @Reference
1836 public void setSecurityService(SecurityService securityService) {
1837 this.securityService = securityService;
1838 }
1839
1840
1841
1842
1843
1844
1845
1846 @Reference
1847 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
1848 this.userDirectoryService = userDirectoryService;
1849 }
1850
1851
1852
1853
1854
1855
1856
1857 @Reference(
1858 policy = ReferencePolicy.DYNAMIC,
1859 cardinality = ReferenceCardinality.OPTIONAL,
1860 unbind = "unsetSchedulerService"
1861 )
1862 public void setSchedulerService(SchedulerService schedulerService) {
1863 this.schedulerService = schedulerService;
1864 }
1865
1866 public void unsetSchedulerService(SchedulerService schedulerService) {
1867 if (this.schedulerService == schedulerService) {
1868 this.schedulerService = null;
1869 }
1870 }
1871
1872
1873
1874
1875
1876
1877
1878 @Reference
1879 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
1880 organizationDirectoryService = organizationDirectory;
1881 }
1882
1883
1884
1885
1886
1887
1888 @Override
1889 protected SecurityService getSecurityService() {
1890 return securityService;
1891 }
1892
1893
1894
1895
1896
1897
1898 @Override
1899 protected UserDirectoryService getUserDirectoryService() {
1900 return userDirectoryService;
1901 }
1902
1903
1904
1905
1906
1907
1908 @Override
1909 protected OrganizationDirectoryService getOrganizationDirectoryService() {
1910 return organizationDirectoryService;
1911 }
1912
1913 protected CloseableHttpClient getAuthedHttpClient() {
1914 HttpClientBuilder cb = HttpClientBuilder.create();
1915 CredentialsProvider provider = new BasicCredentialsProvider();
1916 String schema = AuthSchemes.DIGEST;
1917 if ("Basic".equals(downloadAuthMethod)) {
1918 schema = AuthSchemes.BASIC;
1919 }
1920 provider.setCredentials(
1921 new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, schema),
1922 new UsernamePasswordCredentials(downloadUser, downloadPassword));
1923 return cb.setDefaultCredentialsProvider(provider).build();
1924 }
1925
1926 private MediaPackage createSmil(MediaPackage mediaPackage) throws IOException, IngestException {
1927 List<Track> partialTracks = new ArrayList<>();
1928 for (Track track : mediaPackage.getTracks()) {
1929 Long startTime = partialTrackStartTimes.getIfPresent(track.getIdentifier());
1930 if (startTime != null) {
1931 partialTracks.add(track);
1932 }
1933 }
1934
1935
1936 if (partialTracks.isEmpty()) {
1937 return mediaPackage;
1938 }
1939
1940
1941 List<Track> tracks = partialTracks.stream()
1942 .map(track -> {
1943 try {
1944
1945 return mediaInspectionService.enrich(track, true);
1946 } catch (Exception e) {
1947 throw new RuntimeException("Error enriching track", e);
1948 }
1949 })
1950 .map(job -> {
1951 try {
1952
1953
1954 waitForJob(getServiceRegistry(), Optional.empty(), job);
1955 return (Track) MediaPackageElementParser.getFromXml(job.getPayload());
1956 } catch (Exception e) {
1957 throw new RuntimeException("Error parsing job payload as track", e);
1958 }
1959 })
1960 .peek(MediaPackageSupport.updateElement(mediaPackage))
1961 .collect(Collectors.toList());
1962
1963
1964 org.w3c.dom.Document smilDocument = SmilUtil.createSmil();
1965 for (Track track : tracks) {
1966 Long startTime = partialTrackStartTimes.getIfPresent(track.getIdentifier());
1967 if (startTime == null) {
1968 logger.error("No start time found for track {}", track);
1969 throw new IngestException("No start time found for track " + track.getIdentifier());
1970 }
1971 smilDocument = addSmilTrack(smilDocument, track, startTime);
1972 partialTrackStartTimes.invalidate(track.getIdentifier());
1973 }
1974
1975
1976 return addSmilCatalog(smilDocument, mediaPackage);
1977 }
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992 private MediaPackage addSmilCatalog(org.w3c.dom.Document smilDocument, MediaPackage mediaPackage)
1993 throws IOException, IngestException {
1994 Optional<org.w3c.dom.Document> optSmilDocument = loadSmilDocument(workingFileRepository, mediaPackage);
1995 if (optSmilDocument.isPresent()) {
1996 throw new IngestException("SMIL already exists!");
1997 }
1998
1999 InputStream in = null;
2000 try {
2001 in = XmlUtil.serializeDocument(smilDocument);
2002 String elementId = UUID.randomUUID().toString();
2003 URI uri = workingFileRepository.put(mediaPackage.getIdentifier().toString(), elementId, PARTIAL_SMIL_NAME, in);
2004 MediaPackageElement mpe = mediaPackage.add(uri, MediaPackageElement.Type.Catalog, MediaPackageElements.SMIL);
2005 mpe.setIdentifier(elementId);
2006
2007 mpe.setChecksum(null);
2008 mpe.setMimeType(MimeTypes.SMIL);
2009 return mediaPackage;
2010 } finally {
2011 IoSupport.closeQuietly(in);
2012 }
2013 }
2014
2015
2016
2017
2018
2019
2020 private Optional<org.w3c.dom.Document> loadSmilDocument(final WorkingFileRepository workingFileRepository,
2021 MediaPackage mp) {
2022 return Arrays.stream(mp.getElements())
2023 .filter(MediaPackageSupport.Filters::isSmilCatalog)
2024 .findFirst()
2025 .map(mpe -> {
2026 try (InputStream in = workingFileRepository.get(
2027 mpe.getMediaPackage().getIdentifier().toString(),
2028 mpe.getIdentifier())) {
2029 return SmilUtil.loadSmilDocument(in, mpe);
2030 } catch (Exception e) {
2031 logger.warn("Unable to load smil document from catalog '{}'", mpe, e);
2032 return Misc.chuck(e);
2033 }
2034 });
2035 }
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050 private org.w3c.dom.Document addSmilTrack(org.w3c.dom.Document smilDocument, Track track, long startTime)
2051 throws IngestException {
2052 if (MediaPackageElements.PRESENTER_SOURCE.getType().equals(track.getFlavor().getType())) {
2053 return SmilUtil.addTrack(smilDocument, SmilUtil.TrackType.PRESENTER, track.hasVideo(), startTime,
2054 track.getDuration(), track.getURI(), track.getIdentifier());
2055 } else if (MediaPackageElements.PRESENTATION_SOURCE.getType().equals(track.getFlavor().getType())) {
2056 return SmilUtil.addTrack(smilDocument, SmilUtil.TrackType.PRESENTATION, track.hasVideo(), startTime,
2057 track.getDuration(), track.getURI(), track.getIdentifier());
2058 } else {
2059 logger.warn("Invalid partial flavor type {} of track {}", track.getFlavor(), track);
2060 throw new IngestException(
2061 "Invalid partial flavor type " + track.getFlavor().getType() + " of track " + track.getURI().toString());
2062 }
2063 }
2064
2065 private MediaPackage checkForCASeries(MediaPackage mp, String seriesAppendName) {
2066
2067 if (mp == null || seriesAppendName == null) {
2068 logger.debug("No series name provided");
2069 return mp;
2070 }
2071
2072
2073 User user = securityService.getUser();
2074 if (!user.hasRole(GLOBAL_ADMIN_ROLE) && !user.hasRole(GLOBAL_CAPTURE_AGENT_ROLE)) {
2075 logger.info("User '{}' is missing capture agent roles, won't apply CASeries", user.getUsername());
2076 return mp;
2077 }
2078
2079 String captureAgentId = null;
2080 Catalog[] catalog = mp.getCatalogs(MediaPackageElementFlavor.flavor("dublincore", "episode"));
2081 if (catalog.length == 1) {
2082 try (InputStream catalogInputStream = workingFileRepository.get(mp.getIdentifier().toString(),
2083 catalog[0].getIdentifier())) {
2084 DublinCoreCatalog dc = dublinCoreService.load(catalogInputStream);
2085 captureAgentId = getCaptureAgent(dc);
2086 } catch (Exception e) {
2087 logger.info("Unable to determine capture agent name");
2088 }
2089 }
2090 if (captureAgentId == null) {
2091 logger.info("No Capture Agent ID defined for MediaPackage {}, won't apply CASeries", mp.getIdentifier());
2092 return mp;
2093 }
2094 logger.info("Applying CASeries to MediaPackage {} for capture agent '{}'", mp.getIdentifier(), captureAgentId);
2095
2096
2097 String seriesId = captureAgentId.replaceAll("[^\\w-_.:;()]+", "_");
2098 String seriesName = captureAgentId + seriesAppendName;
2099
2100 try {
2101 seriesService.getSeries(seriesId);
2102 } catch (NotFoundException nfe) {
2103 try {
2104 List<String> roleNames = new ArrayList<>();
2105 String roleName = SecurityUtil.getCaptureAgentRole(captureAgentId);
2106 roleNames.add(roleName);
2107 logger.debug("Capture agent role name: {}", roleName);
2108
2109 String username = user.getUsername();
2110 roleNames.add(UserIdRoleProvider.getUserIdRole(username));
2111
2112 logger.info("Creating new series for capture agent '{}' and user '{}'", captureAgentId, username);
2113 createSeries(seriesId, seriesName, roleNames);
2114 } catch (Exception e) {
2115 logger.error("Unable to create series {} for event {}", seriesName, mp, e);
2116 return mp;
2117 }
2118 } catch (SeriesException | UnauthorizedException e) {
2119 logger.error("Exception while searching for series {}", seriesName, e);
2120 return mp;
2121 }
2122
2123
2124 mp.setSeries(seriesId);
2125 mp.setSeriesTitle(seriesName);
2126
2127 return mp;
2128 }
2129
2130 private DublinCoreCatalog createSeries(String seriesId, String seriesName, List<String> roleNames)
2131 throws SeriesException, UnauthorizedException, NotFoundException {
2132 DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
2133 dc.set(PROPERTY_IDENTIFIER, seriesId);
2134 dc.set(PROPERTY_TITLE, seriesName);
2135 dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
2136
2137 DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
2138
2139
2140 List<AccessControlEntry> aces = new ArrayList();
2141 for (String roleName : roleNames) {
2142 AccessControlEntry aceRead = new AccessControlEntry(roleName, Permissions.Action.READ.toString(), true);
2143 AccessControlEntry aceWrite = new AccessControlEntry(roleName, Permissions.Action.WRITE.toString(), true);
2144 aces.add(aceRead);
2145 aces.add(aceWrite);
2146 }
2147 AccessControlList acl = new AccessControlList(aces);
2148 seriesService.updateAccessControl(seriesId, acl);
2149 logger.info("Created capture agent series with name {} and id {}", seriesName, seriesId);
2150
2151 return dc;
2152 }
2153 }