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.index.service.impl;
23
24 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER;
25 import static org.opencastproject.security.api.DefaultOrganization.DEFAULT_ORGANIZATION_ID;
26 import static org.opencastproject.workflow.api.ConfiguredWorkflow.workflow;
27
28 import org.opencastproject.assetmanager.api.AssetManager;
29 import org.opencastproject.assetmanager.api.AssetManagerException;
30 import org.opencastproject.assetmanager.api.Snapshot;
31 import org.opencastproject.assetmanager.util.WorkflowPropertiesUtil;
32 import org.opencastproject.assetmanager.util.Workflows;
33 import org.opencastproject.authorization.xacml.manager.api.AclService;
34 import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory;
35 import org.opencastproject.capture.CaptureParameters;
36 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
37 import org.opencastproject.elasticsearch.api.SearchIndexException;
38 import org.opencastproject.elasticsearch.api.SearchResult;
39 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
40 import org.opencastproject.elasticsearch.index.objects.event.Event;
41 import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
42 import org.opencastproject.elasticsearch.index.objects.series.Series;
43 import org.opencastproject.event.comment.EventComment;
44 import org.opencastproject.event.comment.EventCommentException;
45 import org.opencastproject.event.comment.EventCommentParser;
46 import org.opencastproject.event.comment.EventCommentService;
47 import org.opencastproject.index.service.api.IndexService;
48 import org.opencastproject.index.service.catalog.adapter.DublinCoreMetadataUtil;
49 import org.opencastproject.index.service.catalog.adapter.MetadataUtils;
50 import org.opencastproject.index.service.catalog.adapter.events.CommonEventCatalogUIAdapter;
51 import org.opencastproject.index.service.catalog.adapter.series.CommonSeriesCatalogUIAdapter;
52 import org.opencastproject.index.service.exception.IndexServiceException;
53 import org.opencastproject.index.service.exception.UnsupportedAssetException;
54 import org.opencastproject.index.service.impl.util.EventHttpServletRequest;
55 import org.opencastproject.index.service.impl.util.EventUtils;
56 import org.opencastproject.index.service.impl.util.Retraction;
57 import org.opencastproject.index.service.impl.util.RetractionListener;
58 import org.opencastproject.index.service.util.JSONUtils;
59 import org.opencastproject.index.service.util.RequestUtils;
60 import org.opencastproject.index.service.util.RestUtils;
61 import org.opencastproject.ingest.api.IngestException;
62 import org.opencastproject.ingest.api.IngestService;
63 import org.opencastproject.list.api.ListProvidersService;
64 import org.opencastproject.mediapackage.Attachment;
65 import org.opencastproject.mediapackage.Catalog;
66 import org.opencastproject.mediapackage.EName;
67 import org.opencastproject.mediapackage.MediaPackage;
68 import org.opencastproject.mediapackage.MediaPackageElement;
69 import org.opencastproject.mediapackage.MediaPackageElement.Type;
70 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
71 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
72 import org.opencastproject.mediapackage.MediaPackageElements;
73 import org.opencastproject.mediapackage.MediaPackageException;
74 import org.opencastproject.mediapackage.Track;
75 import org.opencastproject.metadata.dublincore.DCMIPeriod;
76 import org.opencastproject.metadata.dublincore.DublinCore;
77 import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
78 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
79 import org.opencastproject.metadata.dublincore.DublinCoreUtil;
80 import org.opencastproject.metadata.dublincore.DublinCoreValue;
81 import org.opencastproject.metadata.dublincore.DublinCores;
82 import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
83 import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
84 import org.opencastproject.metadata.dublincore.MetadataField;
85 import org.opencastproject.metadata.dublincore.MetadataJson;
86 import org.opencastproject.metadata.dublincore.MetadataList;
87 import org.opencastproject.metadata.dublincore.Precision;
88 import org.opencastproject.metadata.dublincore.SeriesCatalogUIAdapter;
89 import org.opencastproject.scheduler.api.SchedulerException;
90 import org.opencastproject.scheduler.api.SchedulerService;
91 import org.opencastproject.security.api.AccessControlList;
92 import org.opencastproject.security.api.AccessControlParser;
93 import org.opencastproject.security.api.AclScope;
94 import org.opencastproject.security.api.AuthorizationService;
95 import org.opencastproject.security.api.Permissions;
96 import org.opencastproject.security.api.SecurityService;
97 import org.opencastproject.security.api.UnauthorizedException;
98 import org.opencastproject.security.api.User;
99 import org.opencastproject.security.api.UserDirectoryService;
100 import org.opencastproject.security.util.SecurityContext;
101 import org.opencastproject.security.util.SecurityUtil;
102 import org.opencastproject.series.api.SeriesException;
103 import org.opencastproject.series.api.SeriesService;
104 import org.opencastproject.util.Checksum;
105 import org.opencastproject.util.ChecksumType;
106 import org.opencastproject.util.DateTimeSupport;
107 import org.opencastproject.util.NotFoundException;
108 import org.opencastproject.util.XmlNamespaceBinding;
109 import org.opencastproject.util.XmlNamespaceContext;
110 import org.opencastproject.util.data.Tuple;
111 import org.opencastproject.workflow.api.ConfiguredWorkflow;
112 import org.opencastproject.workflow.api.WorkflowDatabaseException;
113 import org.opencastproject.workflow.api.WorkflowDefinition;
114 import org.opencastproject.workflow.api.WorkflowException;
115 import org.opencastproject.workflow.api.WorkflowInstance;
116 import org.opencastproject.workflow.api.WorkflowInstance.WorkflowState;
117 import org.opencastproject.workflow.api.WorkflowParsingException;
118 import org.opencastproject.workflow.api.WorkflowService;
119 import org.opencastproject.workspace.api.Workspace;
120
121 import com.google.common.net.MediaType;
122
123 import net.fortuna.ical4j.model.Period;
124 import net.fortuna.ical4j.model.property.RRule;
125
126 import org.apache.commons.fileupload.FileItemIterator;
127 import org.apache.commons.fileupload.FileItemStream;
128 import org.apache.commons.fileupload.FileUploadException;
129 import org.apache.commons.fileupload.servlet.ServletFileUpload;
130 import org.apache.commons.fileupload.util.Streams;
131 import org.apache.commons.io.IOUtils;
132 import org.apache.commons.lang3.StringUtils;
133 import org.codehaus.jettison.json.JSONException;
134 import org.joda.time.DateTimeZone;
135 import org.json.simple.JSONArray;
136 import org.json.simple.JSONObject;
137 import org.json.simple.parser.JSONParser;
138 import org.osgi.service.component.ComponentContext;
139 import org.osgi.service.component.annotations.Activate;
140 import org.osgi.service.component.annotations.Component;
141 import org.osgi.service.component.annotations.Deactivate;
142 import org.osgi.service.component.annotations.Reference;
143 import org.osgi.service.component.annotations.ReferenceCardinality;
144 import org.osgi.service.component.annotations.ReferencePolicy;
145 import org.slf4j.Logger;
146 import org.slf4j.LoggerFactory;
147
148 import java.io.ByteArrayInputStream;
149 import java.io.IOException;
150 import java.io.InputStream;
151 import java.net.URI;
152 import java.text.ParseException;
153 import java.text.SimpleDateFormat;
154 import java.util.ArrayList;
155 import java.util.Arrays;
156 import java.util.Collections;
157 import java.util.Date;
158 import java.util.HashMap;
159 import java.util.HashSet;
160 import java.util.LinkedList;
161 import java.util.List;
162 import java.util.Map;
163 import java.util.Map.Entry;
164 import java.util.Optional;
165 import java.util.Properties;
166 import java.util.Set;
167 import java.util.TimeZone;
168 import java.util.UUID;
169 import java.util.concurrent.ConcurrentHashMap;
170 import java.util.concurrent.ExecutorService;
171 import java.util.concurrent.Executors;
172 import java.util.regex.Pattern;
173 import java.util.stream.Collectors;
174
175 import javax.servlet.http.HttpServletRequest;
176
177 @Component(
178 immediate = true,
179 service = IndexService.class,
180 property = {
181 "service.description=Index Services Implementation"
182 }
183 )
184 public class IndexServiceImpl implements IndexService {
185
186 private static final String WORKFLOW_CONFIG_PREFIX = "org.opencastproject.workflow.config.";
187
188 public static final String THEME_PROPERTY_NAME = "theme";
189
190
191 private static final Logger logger = LoggerFactory.getLogger(IndexServiceImpl.class);
192
193 private final List<EventCatalogUIAdapter> eventCatalogUIAdapters = new ArrayList<>();
194 private final List<SeriesCatalogUIAdapter> seriesCatalogUIAdapters = new ArrayList<>();
195
196
197 private static final JSONParser parser = new JSONParser();
198
199 private String attachmentRegex = "^attachment.*";
200 private String catalogRegex = "^catalog.*";
201 private String trackRegex = "^track.*";
202 private String numberedAssetRegex = "^\\*$";
203
204 private Pattern patternAttachment = Pattern.compile(attachmentRegex);
205 private Pattern patternCatalog = Pattern.compile(catalogRegex);
206 private Pattern patternTrack = Pattern.compile(trackRegex);
207 private Pattern patternNumberedAsset = Pattern.compile(numberedAssetRegex);
208
209 private AclServiceFactory aclServiceFactory;
210 private AuthorizationService authorizationService;
211 private CaptureAgentStateService captureAgentStateService;
212 private EventCommentService eventCommentService;
213 private IngestService ingestService;
214 private ListProvidersService listProvidersService;
215 private AssetManager assetManager;
216 private SchedulerService schedulerService;
217 private SecurityService securityService;
218 private SeriesService seriesService;
219 private UserDirectoryService userDirectoryService;
220 private WorkflowService workflowService;
221 private Workspace workspace;
222 private ElasticsearchIndex elasticsearchIndex;
223
224
225 private ExecutorService executorService = Executors.newSingleThreadExecutor();
226
227 private Map<Long, Retraction> retractions = new ConcurrentHashMap<>();
228
229
230
231
232
233
234
235 @Reference
236 public void setAclServiceFactory(AclServiceFactory aclServiceFactory) {
237 this.aclServiceFactory = aclServiceFactory;
238 }
239
240 @Reference
241 public void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
242 this.elasticsearchIndex = elasticsearchIndex;
243 }
244
245
246
247
248
249
250
251 @Reference
252 public void setAuthorizationService(AuthorizationService authorizationService) {
253 this.authorizationService = authorizationService;
254 }
255
256
257
258
259
260
261
262 @Reference
263 public void setCaptureAgentStateService(CaptureAgentStateService captureAgentStateService) {
264 this.captureAgentStateService = captureAgentStateService;
265 }
266
267
268
269
270
271
272
273 @Reference
274 public void setEventCommentService(EventCommentService eventCommentService) {
275 this.eventCommentService = eventCommentService;
276 }
277
278
279
280
281
282
283
284 @Reference(
285 name = "EventCatalogUIAdapter",
286 cardinality = ReferenceCardinality.MULTIPLE,
287 policy = ReferencePolicy.DYNAMIC,
288 unbind = "removeCatalogUIAdapter"
289 )
290 public void addCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
291 eventCatalogUIAdapters.add(catalogUIAdapter);
292 }
293
294
295
296
297
298
299
300 public void removeCatalogUIAdapter(EventCatalogUIAdapter catalogUIAdapter) {
301 eventCatalogUIAdapters.remove(catalogUIAdapter);
302 }
303
304
305
306
307
308
309
310 @Reference(
311 name = "SeriesCatalogUIAdapter",
312 cardinality = ReferenceCardinality.MULTIPLE,
313 policy = ReferencePolicy.DYNAMIC,
314 unbind = "removeCatalogUIAdapter"
315 )
316 public void addCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
317 seriesCatalogUIAdapters.add(catalogUIAdapter);
318 }
319
320
321
322
323
324
325
326 public void removeCatalogUIAdapter(SeriesCatalogUIAdapter catalogUIAdapter) {
327 seriesCatalogUIAdapters.remove(catalogUIAdapter);
328 }
329
330
331
332
333
334
335
336 @Reference
337 public void setIngestService(IngestService ingestService) {
338 this.ingestService = ingestService;
339 }
340
341
342
343
344
345
346
347 @Reference
348 public void setListProvidersService(ListProvidersService listProvidersService) {
349 this.listProvidersService = listProvidersService;
350 }
351
352
353
354
355
356
357
358 @Reference
359 public void setAssetManager(AssetManager assetManager) {
360 this.assetManager = assetManager;
361 }
362
363
364
365
366
367
368
369 @Reference
370 public void setSchedulerService(SchedulerService schedulerService) {
371 this.schedulerService = schedulerService;
372 }
373
374
375
376
377
378
379
380 @Reference
381 public void setSecurityService(SecurityService securityService) {
382 this.securityService = securityService;
383 }
384
385
386
387
388
389
390
391 @Reference
392 public void setSeriesService(SeriesService seriesService) {
393 this.seriesService = seriesService;
394 }
395
396
397
398
399
400
401
402 @Reference
403 public void setWorkflowService(WorkflowService workflowService) {
404 this.workflowService = workflowService;
405 }
406
407
408
409
410
411
412
413 @Reference
414 public void setWorkspace(Workspace workspace) {
415 this.workspace = workspace;
416 }
417
418
419
420
421
422
423
424 @Reference
425 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
426 this.userDirectoryService = userDirectoryService;
427 }
428
429
430
431
432
433 public AclService getAclService() {
434 return aclServiceFactory.serviceFor(securityService.getOrganization());
435 }
436
437 public List<EventCatalogUIAdapter> getEventCatalogUIAdapters(String organization) {
438 return eventCatalogUIAdapters.stream().filter(a -> a.handlesOrganization(organization))
439 .collect(Collectors.toList());
440 }
441
442
443
444
445
446
447 public List<SeriesCatalogUIAdapter> getSeriesCatalogUIAdapters(String organization) {
448 return seriesCatalogUIAdapters.stream().filter(a -> a.handlesOrganization(organization))
449 .collect(Collectors.toList());
450 }
451
452 public EventCatalogUIAdapter getCommonEventCatalogUIAdapter(String organization) {
453 Optional<EventCatalogUIAdapter> orgEventCatalogUIAdapter = eventCatalogUIAdapters.stream()
454 .filter(a -> a instanceof CommonEventCatalogUIAdapter)
455 .filter(a -> a.handlesOrganization(organization))
456 .findFirst();
457
458 if (orgEventCatalogUIAdapter.isPresent()) {
459 return orgEventCatalogUIAdapter.get();
460 } else if (!organization.equals(DEFAULT_ORGANIZATION_ID)) {
461 return getCommonEventCatalogUIAdapter(DEFAULT_ORGANIZATION_ID);
462 } else {
463 throw new IllegalStateException("Common event metadata for " + DEFAULT_ORGANIZATION_ID + " needs to be "
464 + "configured!");
465 }
466 }
467
468 public SeriesCatalogUIAdapter getCommonSeriesCatalogUIAdapter(String organization) {
469 Optional<SeriesCatalogUIAdapter> orgSeriesCatalogUIAdapter = seriesCatalogUIAdapters.stream()
470 .filter(a -> a instanceof CommonSeriesCatalogUIAdapter)
471 .filter(a -> a.handlesOrganization(organization))
472 .findFirst();
473
474 if (orgSeriesCatalogUIAdapter.isPresent()) {
475 return orgSeriesCatalogUIAdapter.get();
476 } else if (!organization.equals(DEFAULT_ORGANIZATION_ID)) {
477 return getCommonSeriesCatalogUIAdapter(DEFAULT_ORGANIZATION_ID);
478 } else {
479 throw new IllegalStateException("Common series metadata for " + DEFAULT_ORGANIZATION_ID + " needs to be "
480 + "configured!");
481 }
482 }
483
484 @Override
485 public List<EventCatalogUIAdapter> getEventCatalogUIAdapters() {
486 return new ArrayList<>(getEventCatalogUIAdapters(securityService.getOrganization().getId()));
487 }
488
489 @Override
490 public List<EventCatalogUIAdapter> getExtendedEventCatalogUIAdapters() {
491 String organization = securityService.getOrganization().getId();
492 return eventCatalogUIAdapters.stream().filter(a -> !(a instanceof CommonEventCatalogUIAdapter))
493 .filter(a -> a.handlesOrganization(organization)).collect(Collectors.toList());
494 }
495
496 @Override
497 public List<SeriesCatalogUIAdapter> getSeriesCatalogUIAdapters() {
498 return new LinkedList<>(getSeriesCatalogUIAdapters(securityService.getOrganization().getId()));
499 }
500
501 @Override
502 public EventCatalogUIAdapter getCommonEventCatalogUIAdapter() {
503 return getCommonEventCatalogUIAdapter(securityService.getOrganization().getId());
504 }
505
506 @Override
507 public SeriesCatalogUIAdapter getCommonSeriesCatalogUIAdapter() {
508 return getCommonSeriesCatalogUIAdapter(securityService.getOrganization().getId());
509 }
510
511 @Activate
512 public void activate(ComponentContext cc) {
513 workflowService.addWorkflowListener(new RetractionListener(this, securityService, retractions));
514 }
515
516 @Deactivate
517 public void deactivate(ComponentContext cc) {
518 executorService.shutdown();
519 }
520
521 @Override
522 public String createEvent(HttpServletRequest request) throws IndexServiceException, UnsupportedAssetException {
523 JSONObject metadataJson = null;
524 MediaPackage mp = null;
525
526
527
528
529
530 List<String> assetList = new LinkedList<String>();
531 try {
532 if (ServletFileUpload.isMultipartContent(request)) {
533 mp = ingestService.createMediaPackage();
534
535 for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
536 FileItemStream item = iter.next();
537
538 String fieldName = item.getFieldName();
539 if (item.isFormField()) {
540 if ("metadata".equals(fieldName)) {
541 String metadata = Streams.asString(item.openStream());
542 try {
543 metadataJson = (JSONObject) new JSONParser().parse(metadata);
544
545 if (metadataJson.containsKey("source")) {
546 final JSONObject sourceJson = (JSONObject) metadataJson.get("source");
547 if (sourceJson.containsKey("metadata")) {
548 final JSONObject sourceMetadataJson = (JSONObject) sourceJson.get("metadata");
549 if (sourceMetadataJson.containsKey("device")) {
550 SecurityUtil.checkAgentAccess(securityService, (String) sourceMetadataJson.get("device"));
551 }
552 }
553 }
554 } catch (Exception e) {
555 logger.warn("Unable to parse metadata {}", metadata);
556 throw new IllegalArgumentException("Unable to parse metadata");
557 }
558 }
559 } else {
560
561 fieldName = fieldName.substring(0, fieldName.lastIndexOf("."));
562 final MediaType mediaType = MediaType.parse(item.getContentType());
563 final boolean accepted = RequestUtils.typeIsAccepted(item.getName(), fieldName, mediaType,
564 listProvidersService);
565 if (!accepted) {
566 throw new UnsupportedAssetException("Provided file format " + mediaType.toString() + " not allowed.");
567 }
568 if ("presenter".equals(item.getFieldName())) {
569 mp = ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTER_SOURCE, mp);
570 } else if ("presentation".equals(item.getFieldName())) {
571 mp = ingestService.addTrack(item.openStream(), item.getName(), MediaPackageElements.PRESENTATION_SOURCE,
572 mp);
573 } else if ("audio".equals(item.getFieldName())) {
574 mp = ingestService.addTrack(item.openStream(), item.getName(),
575 new MediaPackageElementFlavor("presenter-audio", "source"), mp);
576
577 } else if (item.getFieldName().toLowerCase().matches(attachmentRegex)) {
578 assetList.add(item.getFieldName());
579 mp = ingestService.addAttachment(item.openStream(), item.getName(),
580 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
581 } else if (item.getFieldName().toLowerCase().matches(catalogRegex)) {
582
583 assetList.add(item.getFieldName());
584 mp = ingestService.addCatalog(item.openStream(), item.getName(),
585 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
586 } else if (item.getFieldName().toLowerCase().matches(trackRegex)) {
587
588 assetList.add(item.getFieldName());
589 mp = ingestService.addTrack(item.openStream(), item.getName(),
590 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
591 } else {
592 logger.warn("Unknown field name found {}", item.getFieldName());
593 }
594 }
595 }
596
597 try {
598 JSONArray assetMetadata = (JSONArray)((JSONObject) metadataJson.get("assets")).get("options");
599 if (assetMetadata != null) {
600 mp = updateMpAssetFlavor(assetList, mp, assetMetadata);
601 }
602 } catch (Exception e) {
603
604 logger.warn("Unable to process asset metadata {}", metadataJson.get("assets"), e);
605 throw new IllegalArgumentException("Unable to parse metadata", e);
606 }
607
608 } else {
609 throw new IllegalArgumentException("No multipart content");
610 }
611
612
613 if (mp.getTracks().length == 1
614 && mp.getTracks()[0].getFlavor().equals(new MediaPackageElementFlavor("presenter-audio", "source"))) {
615 Track audioTrack = mp.getTracks()[0];
616 mp.remove(audioTrack);
617 audioTrack.setFlavor(MediaPackageElements.PRESENTER_SOURCE);
618 mp.add(audioTrack);
619 }
620
621 return createEvent(metadataJson, mp);
622 } catch (FileUploadException | UnauthorizedException | ParseException | IngestException | SchedulerException
623 | MediaPackageException | IOException | NotFoundException e) {
624 logger.error("Unable to create event:", e);
625 throw new IndexServiceException("Unable to create event", e);
626 }
627 }
628
629 @Override
630 public String updateEventAssets(MediaPackage mp, HttpServletRequest request)
631 throws IndexServiceException, UnsupportedAssetException {
632 JSONObject metadataJson = null;
633
634
635
636
637
638
639
640 List<String> assetList = new LinkedList<String>();
641
642 try {
643 if (!ServletFileUpload.isMultipartContent(request)) {
644 throw new IllegalArgumentException("No multipart content");
645 }
646 for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
647 FileItemStream item = iter.next();
648 String fieldName = item.getFieldName();
649 if (item.isFormField()) {
650 if ("metadata".equals(fieldName)) {
651 String metadata = Streams.asString(item.openStream());
652 try {
653 metadataJson = (JSONObject) parser.parse(metadata);
654 } catch (Exception e) {
655 logger.warn("Unable to parse metadata {}", metadata);
656 throw new IllegalArgumentException("Unable to parse metadata");
657 }
658 }
659 } else {
660
661 fieldName = fieldName.substring(0, fieldName.lastIndexOf("."));
662 final MediaType mediaType = MediaType.parse(item.getContentType());
663 final boolean accepted = RequestUtils.typeIsAccepted(item.getName(), fieldName, mediaType,
664 listProvidersService);
665 if (!accepted) {
666 throw new UnsupportedAssetException("Provided file format " + mediaType.toString() + " not allowed.");
667 }
668 if (item.getFieldName().toLowerCase().matches(attachmentRegex)) {
669 assetList.add(item.getFieldName());
670
671 mp = ingestService.addAttachment(item.openStream(), item.getName(),
672 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
673 } else if (item.getFieldName().toLowerCase().matches(catalogRegex)) {
674 assetList.add(item.getFieldName());
675
676 mp = ingestService.addCatalog(item.openStream(), item.getName(),
677 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
678 } else if (item.getFieldName().toLowerCase().matches(trackRegex)) {
679
680 assetList.add(item.getFieldName());
681 mp = ingestService.addTrack(item.openStream(), item.getName(),
682 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
683 } else {
684 logger.warn("Unknown field name found {}", item.getFieldName());
685 }
686 }
687 }
688
689
690 try {
691 JSONArray assetMetadata = (JSONArray)((JSONObject) metadataJson.get("assets")).get("options");
692 if (assetMetadata != null) {
693 mp = updateMpAssetFlavor(assetList, mp, assetMetadata);
694 } else {
695 logger.warn("The asset option mapping parameter was not found");
696 throw new IndexServiceException("The asset option mapping parameter was not found");
697 }
698 } catch (Exception e) {
699
700 logger.warn("Unable to process asset metadata {}", metadataJson.get("assets"), e);
701 throw new IllegalArgumentException("Unable to parse metadata", e);
702 }
703
704 return startAddAssetWorkflow(metadataJson, mp);
705 } catch (MediaPackageException | FileUploadException | IOException | IngestException e) {
706 logger.error("Unable to create event:", e);
707 throw new IndexServiceException("Unable to create event", e);
708 }
709 }
710
711
712
713
714
715
716
717
718
719
720
721
722
723 private String startAddAssetWorkflow(JSONObject metadataJson, MediaPackage mediaPackage)
724 throws IndexServiceException {
725 String wfId = null;
726 String mpId = mediaPackage.getIdentifier().toString();
727
728 JSONObject processing = (JSONObject) metadataJson.get("processing");
729 if (processing == null) {
730 throw new IllegalArgumentException("No processing field in metadata");
731 }
732
733 String workflowDefId = (String) processing.get("workflow");
734 if (workflowDefId == null) {
735 throw new IllegalArgumentException("No workflow definition field in processing metadata");
736 }
737
738 JSONObject configJson = (JSONObject) processing.get("configuration");
739
740 try {
741
742
743 Map<String, String> params = new HashMap<String, String>();
744 if (configJson != null) {
745 for (Object key: configJson.keySet()) {
746 params.put((String)key, (String) configJson.get(key));
747 }
748 }
749
750 WorkflowInstance workflowInstance = workflowService.start(
751 workflowService.getWorkflowDefinitionById(workflowDefId), mediaPackage, params);
752 logger.info("Asset update and publish workflow {} scheduled for mp {}", workflowInstance.getId(), mpId);
753 } catch (AssetManagerException | WorkflowParsingException | UnauthorizedException e) {
754 throw new IndexServiceException("Unable to start workflow " + workflowDefId + " on " + mpId);
755 } catch (WorkflowDatabaseException e) {
756 logger.warn("Unable to load workflow '{}' from workflow service:", wfId, e);
757 } catch (NotFoundException e) {
758 logger.warn("Workflow '{}' not found", wfId);
759 }
760 return wfId;
761 }
762
763
764
765
766
767
768
769
770
771
772 private SourceType getSourceType(JSONObject source) {
773 SourceType type;
774 try {
775 type = SourceType.valueOf((String) source.get("type"));
776 } catch (Exception e) {
777 logger.error("Unknown source type '{}'", source.get("type"));
778 throw new IllegalArgumentException("Unknown source type");
779 }
780 return type;
781 }
782
783
784
785
786
787
788
789
790
791
792 private AccessControlList getAccessControlList(JSONObject metadataJson) {
793 AccessControlList acl = new AccessControlList();
794 JSONObject accessJson = (JSONObject) metadataJson.get("access");
795 if (accessJson != null) {
796 try {
797 acl = AccessControlParser.parseAcl(accessJson.toJSONString());
798 } catch (Exception e) {
799 throw new IllegalArgumentException("Unable to parse access control list: " + accessJson.toJSONString());
800 }
801 }
802 return acl;
803 }
804
805 public String createEvent(JSONObject metadataJson, MediaPackage mp) throws ParseException, IOException,
806 MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException {
807 if (metadataJson == null) {
808 throw new IllegalArgumentException("No metadata set");
809 }
810
811 JSONObject source = (JSONObject) metadataJson.get("source");
812 if (source == null) {
813 throw new IllegalArgumentException("No source field in metadata");
814 }
815
816 JSONObject processing = (JSONObject) metadataJson.get("processing");
817 if (processing == null) {
818 throw new IllegalArgumentException("No processing field in metadata");
819 }
820
821 JSONArray allEventMetadataJson = (JSONArray) metadataJson.get("metadata");
822 if (allEventMetadataJson == null) {
823 throw new IllegalArgumentException("No metadata field in metadata");
824 }
825
826 AccessControlList acl = getAccessControlList(metadataJson);
827
828 MetadataList metadataList = getMetadataListWithAllEventCatalogUIAdapters();
829 MetadataJson.fillListFromJson(metadataList, allEventMetadataJson);
830
831 EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
832 eventHttpServletRequest.setAcl(acl);
833 eventHttpServletRequest.setMetadataList(metadataList);
834 eventHttpServletRequest.setMediaPackage(mp);
835 eventHttpServletRequest.setProcessing(processing);
836 eventHttpServletRequest.setSource(source);
837
838 return createEvent(eventHttpServletRequest);
839 }
840
841 @Override
842 public String createEvent(EventHttpServletRequest eventHttpServletRequest) throws ParseException, IOException,
843 MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException {
844
845 if (eventHttpServletRequest.getAcl().isEmpty()) {
846 throw new IllegalArgumentException("No access control list available to create new event.");
847 }
848 if (eventHttpServletRequest.getMediaPackage().isEmpty()) {
849 throw new IllegalArgumentException("No mediapackage available to create new event.");
850 }
851 if (eventHttpServletRequest.getMetadataList().isEmpty()) {
852 throw new IllegalArgumentException("No metadata list available to create new event.");
853 }
854 if (eventHttpServletRequest.getProcessing().isEmpty()) {
855 throw new IllegalArgumentException("No processing metadata available to create new event.");
856 }
857 if (eventHttpServletRequest.getSource().isEmpty()) {
858 throw new IllegalArgumentException("No source field metadata available to create new event.");
859 }
860
861
862 String workflowTemplate = (String) eventHttpServletRequest.getProcessing().get().get("workflow");
863 if (workflowTemplate == null) {
864 throw new IllegalArgumentException("No workflow template in metadata");
865 }
866
867
868 SourceType type = getSourceType(eventHttpServletRequest.getSource().get());
869
870 DublinCoreMetadataCollection eventMetadata = eventHttpServletRequest.getMetadataList().get()
871 .getMetadataByAdapter(getCommonEventCatalogUIAdapter());
872
873 Date currentStartDate = null;
874 JSONObject sourceMetadata = (JSONObject) eventHttpServletRequest.getSource().get().get("metadata");
875 if (sourceMetadata != null
876 && (type.equals(SourceType.SCHEDULE_SINGLE) || type.equals(SourceType.SCHEDULE_MULTIPLE))) {
877 try {
878 MetadataField current = eventMetadata.getOutputFields().get("location");
879 eventMetadata.updateStringField(current, (String) sourceMetadata.get("device"));
880 } catch (Exception e) {
881 logger.warn("Unable to parse device {}", sourceMetadata.get("device"));
882 throw new IllegalArgumentException("Unable to parse device");
883 }
884 if (StringUtils.isNotEmpty((String) sourceMetadata.get("start"))) {
885 currentStartDate = EncodingSchemeUtils.decodeDate((String) sourceMetadata.get("start"));
886 }
887 }
888
889 MetadataField startDate = eventMetadata.getOutputFields().get("startDate");
890 if (startDate != null && startDate.isUpdated() && startDate.getValue() != null) {
891 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(startDate.getPattern());
892 currentStartDate = sdf.parse((String) startDate.getValue());
893 } else if (currentStartDate != null) {
894 eventMetadata.removeField(startDate);
895 MetadataField newStartDate = new MetadataField(startDate);
896 newStartDate.setValue(EncodingSchemeUtils.encodeDate(currentStartDate, Precision.Fraction).getValue());
897 eventMetadata.addField(newStartDate);
898 }
899
900
901
902
903
904
905 MetadataField created = eventMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName());
906 if (created != null && (!created.isUpdated() || created.getValue() == null)) {
907 eventMetadata.removeField(created);
908 MetadataField newCreated = new MetadataField(created);
909 if (currentStartDate != null) {
910 newCreated.setValue(EncodingSchemeUtils.encodeDate(currentStartDate, Precision.Second).getValue());
911 } else {
912 newCreated.setValue(EncodingSchemeUtils.encodeDate(new Date(), Precision.Second).getValue());
913 }
914 eventMetadata.addField(newCreated);
915 }
916
917
918 Set<String> presenterUsernames = new HashSet<>();
919 Optional<Set<String>> technicalPresenters = updatePresenters(eventMetadata);
920 if (technicalPresenters.isPresent()) {
921 presenterUsernames = technicalPresenters.get();
922 }
923
924 eventHttpServletRequest.getMetadataList().get().add(getCommonEventCatalogUIAdapter(), eventMetadata);
925 updateMediaPackageMetadata(eventHttpServletRequest.getMediaPackage().get(),
926 eventHttpServletRequest.getMetadataList().get());
927
928 DublinCoreCatalog dc = getDublinCoreCatalog(eventHttpServletRequest);
929 String captureAgentId = null;
930 TimeZone tz = null;
931 org.joda.time.DateTime start = null;
932 org.joda.time.DateTime end = null;
933 long duration = 0L;
934 Properties caProperties = new Properties();
935 RRule rRule = null;
936 if (sourceMetadata != null
937 && (type.equals(SourceType.SCHEDULE_SINGLE) || type.equals(SourceType.SCHEDULE_MULTIPLE))) {
938 Properties configuration;
939 try {
940 captureAgentId = (String) sourceMetadata.get("device");
941 configuration = captureAgentStateService.getAgentConfiguration((String) sourceMetadata.get("device"));
942 } catch (Exception e) {
943 logger.warn("Unable to parse device {}: because:", sourceMetadata.get("device"), e);
944 throw new IllegalArgumentException("Unable to parse device");
945 }
946
947 String durationString = (String) sourceMetadata.get("duration");
948 if (StringUtils.isBlank(durationString)) {
949 throw new IllegalArgumentException("No duration in source metadata");
950 }
951
952
953 String agentTimeZone = configuration.getProperty("capture.device.timezone");
954 if (StringUtils.isNotBlank(agentTimeZone)) {
955 tz = TimeZone.getTimeZone(agentTimeZone);
956 dc.set(DublinCores.OC_PROPERTY_AGENT_TIMEZONE, tz.getID());
957 } else {
958 tz = TimeZone.getDefault();
959 logger.debug("The field 'capture.device.timezone' has not been set in the agent configuration. The default "
960 + "server timezone will be used.");
961 }
962
963 org.joda.time.DateTime now = new org.joda.time.DateTime(DateTimeZone.UTC);
964 start = now.withMillis(DateTimeSupport.fromUTC((String) sourceMetadata.get("start")));
965 end = now.withMillis(DateTimeSupport.fromUTC((String) sourceMetadata.get("end")));
966 duration = Long.parseLong(durationString);
967 DublinCoreValue period = EncodingSchemeUtils
968 .encodePeriod(new DCMIPeriod(start.toDate(), start.plus(duration).toDate()), Precision.Second);
969 String inputs = (String) sourceMetadata.get("inputs");
970
971 caProperties.putAll(configuration);
972 dc.set(DublinCore.PROPERTY_TEMPORAL, period);
973 caProperties.put(CaptureParameters.CAPTURE_DEVICE_NAMES, inputs);
974 }
975
976 if (type.equals(SourceType.SCHEDULE_MULTIPLE)) {
977 rRule = new RRule((String) sourceMetadata.get("rrule"));
978 }
979
980 Map<String, String> configuration = new HashMap<>();
981 if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
982 configuration = new HashMap<>((JSONObject) eventHttpServletRequest.getProcessing().get().get("configuration"));
983
984 }
985 for (Entry<String, String> entry : configuration.entrySet()) {
986 caProperties.put(WORKFLOW_CONFIG_PREFIX.concat(entry.getKey()), entry.getValue());
987 }
988 caProperties.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowTemplate);
989
990 eventHttpServletRequest.setMediaPackage(authorizationService.setAcl(eventHttpServletRequest.getMediaPackage().get(),
991 AclScope.Episode, eventHttpServletRequest.getAcl().get()).getA());
992
993 MediaPackage mediaPackage;
994 switch (type) {
995 case UPLOAD:
996 case UPLOAD_LATER:
997 eventHttpServletRequest
998 .setMediaPackage(updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc));
999 configuration.put("workflowDefinitionId", workflowTemplate);
1000 WorkflowInstance ingest = ingestService.ingest(eventHttpServletRequest.getMediaPackage().get(),
1001 workflowTemplate, configuration);
1002 return eventHttpServletRequest.getMediaPackage().get().getIdentifier().toString();
1003 case SCHEDULE_SINGLE:
1004 mediaPackage = updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc);
1005 eventHttpServletRequest.setMediaPackage(mediaPackage);
1006 try {
1007 schedulerService.addEvent(start.toDate(), start.plus(duration).toDate(), captureAgentId, presenterUsernames,
1008 mediaPackage, configuration, (Map) caProperties, Optional.empty());
1009 } finally {
1010 for (MediaPackageElement mediaPackageElement : mediaPackage.getElements()) {
1011 try {
1012 workspace.delete(mediaPackage.getIdentifier().toString(), mediaPackageElement.getIdentifier());
1013 } catch (NotFoundException | IOException e) {
1014 logger.warn("Failed to delete media package element", e);
1015 }
1016 }
1017 }
1018 return mediaPackage.getIdentifier().toString();
1019 case SCHEDULE_MULTIPLE:
1020 final Map<String, Period> scheduled = schedulerService.addMultipleEvents(rRule, start.toDate(), end.toDate(),
1021 duration, tz, captureAgentId, presenterUsernames, eventHttpServletRequest.getMediaPackage().get(),
1022 configuration, (Map) caProperties, Optional.empty());
1023 return StringUtils.join(scheduled.keySet(), ",");
1024 default:
1025 throw new IllegalArgumentException("Unknown source type: " + type);
1026 }
1027 }
1028
1029
1030
1031
1032
1033
1034
1035
1036 private DublinCoreCatalog getDublinCoreCatalog(EventHttpServletRequest eventHttpServletRequest) {
1037 DublinCoreCatalog dc;
1038 Optional<DublinCoreCatalog> dcOpt = DublinCoreUtil.loadEpisodeDublinCore(workspace,
1039 eventHttpServletRequest.getMediaPackage().get());
1040 if (dcOpt.isPresent()) {
1041 dc = dcOpt.get();
1042
1043 dc.addBindings(XmlNamespaceContext
1044 .mk(XmlNamespaceBinding.mk(DublinCores.OC_PROPERTY_NS_PREFIX, DublinCores.OC_PROPERTY_NS_URI)));
1045 } else {
1046 dc = DublinCores.mkOpencastEpisode().getCatalog();
1047 }
1048 return dc;
1049 }
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060 private Optional<Set<String>> updatePresenters(DublinCoreMetadataCollection eventMetadata) {
1061 MetadataField presentersMetadataField = eventMetadata.getOutputFields()
1062 .get(DublinCore.PROPERTY_CREATOR.getLocalName());
1063 if (presentersMetadataField.isUpdated()) {
1064 Tuple<List<String>, Set<String>> updatedPresenters = getTechnicalPresenters(eventMetadata);
1065 Set<String> presenterUsernames = updatedPresenters.getB();
1066 eventMetadata.removeField(presentersMetadataField);
1067 MetadataField newPresentersMetadataField = new MetadataField(presentersMetadataField);
1068 newPresentersMetadataField.setValue(updatedPresenters.getA());
1069 eventMetadata.addField(newPresentersMetadataField);
1070 return Optional.of(presenterUsernames);
1071 } else {
1072 return Optional.empty();
1073 }
1074 }
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090 private MediaPackage updateDublincCoreCatalog(MediaPackage mp, DublinCoreCatalog dc)
1091 throws IOException, MediaPackageException, IngestException {
1092 try (InputStream inputStream = IOUtils.toInputStream(dc.toXmlString(), "UTF-8")) {
1093
1094 Catalog[] catalogs = mp.getCatalogs(MediaPackageElements.EPISODE);
1095 if (catalogs.length > 0) {
1096 Catalog catalog = catalogs[0];
1097 URI uri = workspace.put(mp.getIdentifier().toString(), catalog.getIdentifier(), "dublincore.xml", inputStream);
1098 catalog.setURI(uri);
1099
1100 catalog.setChecksum(null);
1101 } else {
1102 mp = ingestService.addCatalog(inputStream, "dublincore.xml", MediaPackageElements.EPISODE, mp);
1103 }
1104 }
1105 return mp;
1106 }
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119 @SuppressWarnings("unchecked")
1120 protected MediaPackage updateMpAssetFlavor(List<String> assetList, MediaPackage mp, JSONArray assetMetadata) {
1121
1122 JSONObject assetDataMap = new JSONObject();
1123 for (int i = 0; i < assetMetadata.size(); i++) {
1124 try {
1125 assetDataMap.put(((JSONObject) assetMetadata.get(i)).get("id"), assetMetadata.get(i));
1126 } catch (Exception e) {
1127 throw new IllegalArgumentException("Unable to parse metadata", e);
1128 }
1129 }
1130
1131 for (String assetOrig: assetList) {
1132
1133 String asset = assetOrig;
1134 String assetNumber = null;
1135 String[] assetNameParts = asset.split(Pattern.quote("."));
1136 if (assetNameParts.length > 1) {
1137 asset = assetNameParts[0];
1138 assetNumber = assetNameParts[1];
1139 }
1140 try {
1141 if ((assetMetadata != null) && (assetDataMap.get(asset) != null)) {
1142 String type = (String)((JSONObject) assetDataMap.get(asset)).get("type");
1143 String flavorType = (String)((JSONObject) assetDataMap.get(asset)).get("flavorType");
1144 String flavorSubType = (String)((JSONObject) assetDataMap.get(asset)).get("flavorSubType");
1145 String tags = (String)((JSONObject) assetDataMap.get(asset)).get("tags");
1146 String[] tagsArray = null;
1147
1148 String langTag = null;
1149 if (tags != null) {
1150 tagsArray = tags.split(",");
1151 for (String tag : tagsArray) {
1152 if (StringUtils.startsWith(StringUtils.trimToEmpty(tag), "lang:")) {
1153 langTag = StringUtils.trimToEmpty(tag);
1154 break;
1155 }
1156 }
1157 }
1158
1159 boolean overwriteExisting = !(Boolean) ((JSONObject) assetDataMap.get(asset)).getOrDefault("multiple", false);
1160 if (patternNumberedAsset.matcher(flavorSubType).matches() && (assetNumber != null)) {
1161 flavorSubType = assetNumber;
1162 }
1163 MediaPackageElementFlavor newElemflavor = new MediaPackageElementFlavor(flavorType, flavorSubType);
1164 if (patternAttachment.matcher(type).matches()) {
1165 if (overwriteExisting) {
1166
1167 Attachment[] existing = mp.getAttachments(newElemflavor);
1168 for (int i = 0; i < existing.length; i++) {
1169
1170 if (null == langTag || existing[i].containsTag(langTag)) {
1171 mp.remove(existing[i]);
1172 logger.info("Overwriting existing asset {} {}", type, newElemflavor);
1173 }
1174 }
1175 }
1176
1177 Attachment[] elArray = mp.getAttachments(new MediaPackageElementFlavor(assetOrig, "*"));
1178 elArray[0].setFlavor(newElemflavor);
1179 if (tags != null && tagsArray.length > 0) {
1180 for (String tag : tagsArray) {
1181 elArray[0].addTag(tag);
1182 }
1183 }
1184 logger.info("Updated asset {} {}", type, newElemflavor);
1185 } else if (patternCatalog.matcher(type).matches()) {
1186 if (overwriteExisting) {
1187
1188 Catalog[] existing = mp.getCatalogs(newElemflavor);
1189 for (int i = 0; i < existing.length; i++) {
1190
1191 if (null == langTag || existing[i].containsTag(langTag)) {
1192 mp.remove(existing[i]);
1193 logger.info("Overwriting existing asset {} {}", type, newElemflavor);
1194 }
1195 }
1196 }
1197 Catalog[] catArray = mp.getCatalogs(new MediaPackageElementFlavor(assetOrig, "*"));
1198 if (catArray.length > 1) {
1199 throw new IllegalArgumentException("More than one " + asset + " found, only one expected.");
1200 }
1201 catArray[0].setFlavor(newElemflavor);
1202 if (tags != null && tagsArray.length > 0) {
1203 for (String tag : tagsArray) {
1204 catArray[0].addTag(tag);
1205 }
1206 }
1207 logger.info("Update asset {} {}", type, newElemflavor);
1208 } else if (patternTrack.matcher(type).matches()) {
1209 if (overwriteExisting) {
1210
1211 Track[] existing = mp.getTracks(newElemflavor);
1212 for (int i = 0; i < existing.length; i++) {
1213
1214 if (null == langTag || existing[i].containsTag(langTag)) {
1215 mp.remove(existing[i]);
1216 logger.info("Overwriting existing asset {} {}", type, newElemflavor);
1217 }
1218 }
1219 }
1220 Track[] trackArray = mp.getTracks(new MediaPackageElementFlavor(assetOrig, "*"));
1221 if (trackArray.length > 1) {
1222 throw new IllegalArgumentException("More than one " + asset + " found, only one expected.");
1223 }
1224 trackArray[0].setFlavor(newElemflavor);
1225 if (tags != null && tagsArray.length > 0) {
1226 for (String tag : tagsArray) {
1227 trackArray[0].addTag(tag);
1228 }
1229 }
1230 logger.info("Update asset {} {}", type, newElemflavor);
1231 } else {
1232 logger.warn("Unknown asset type {} {} for field {}", type, newElemflavor, asset);
1233 }
1234 }
1235 } catch (Exception e) {
1236
1237 throw new IllegalArgumentException("Unable to parse metadata: " + assetMetadata.toJSONString(), e);
1238 }
1239 }
1240 return mp;
1241 }
1242
1243 @Override
1244 public MetadataList updateAllEventMetadata(
1245 final String id, final String metadataJSON, final ElasticsearchIndex index)
1246 throws IllegalArgumentException, IndexServiceException, NotFoundException, SearchIndexException,
1247 UnauthorizedException {
1248 final MetadataList metadataList;
1249 try {
1250 metadataList = getMetadataListWithAllEventCatalogUIAdapters();
1251 MetadataJson.fillListFromJson(metadataList, (JSONArray) new JSONParser().parse(metadataJSON));
1252 } catch (final org.json.simple.parser.ParseException e) {
1253 throw new IllegalArgumentException("Not able to parse the event metadata " + metadataJSON, e);
1254 }
1255 return updateEventMetadata(id, metadataList, index);
1256 }
1257
1258 @Override
1259 public void removeCatalogByFlavor(Event event, MediaPackageElementFlavor flavor)
1260 throws IndexServiceException, NotFoundException, UnauthorizedException {
1261 MediaPackage mediaPackage = getEventMediapackage(event);
1262 Catalog[] catalogs = mediaPackage.getCatalogs(flavor);
1263 if (catalogs.length == 0) {
1264 throw new NotFoundException(String.format("Cannot find a catalog with flavor '%s' for event with id '%s'.",
1265 flavor.toString(), event.getIdentifier()));
1266 }
1267 for (Catalog catalog : catalogs) {
1268 mediaPackage.remove(catalog);
1269 }
1270 switch (getEventSource(event)) {
1271 case WORKFLOW:
1272 try {
1273 Optional<WorkflowInstance> workflowInstance = workflowService.
1274 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
1275 if (workflowInstance.isEmpty()) {
1276 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1277 }
1278 WorkflowInstance instance = workflowInstance.get();
1279 instance.setMediaPackage(mediaPackage);
1280 updateWorkflowInstance(instance);
1281 } catch (WorkflowException e) {
1282 throw new IndexServiceException("Unable to remove catalog with flavor '" + flavor
1283 + "' by updating workflow event " + event.getIdentifier(), e);
1284 }
1285 break;
1286 case ARCHIVE:
1287 assetManager.takeSnapshot(mediaPackage);
1288 break;
1289 case SCHEDULE:
1290 try {
1291 schedulerService.updateEvent(event.getIdentifier(), Optional.empty(), Optional.empty(), Optional.empty(),
1292 Optional.empty(), Optional.of(mediaPackage), Optional.empty(), Optional.empty());
1293 } catch (SchedulerException e) {
1294 throw new IndexServiceException("Unable to remove catalog with flavor " + flavor + " by updating scheduled "
1295 + "event " + event.getIdentifier(), e);
1296 }
1297 break;
1298 default:
1299 throw new IndexServiceException(
1300 String.format("Unable to handle event source type '%s'", getEventSource(event)));
1301 }
1302 }
1303
1304 @Override
1305 public void removeCatalogByFlavor(Series series, MediaPackageElementFlavor flavor)
1306 throws NotFoundException, IndexServiceException {
1307 if (series == null) {
1308 throw new IllegalArgumentException("The series cannot be null.");
1309 }
1310 if (flavor == null) {
1311 throw new IllegalArgumentException("The flavor cannot be null.");
1312 }
1313 boolean found = false;
1314 try {
1315 found = seriesService.deleteSeriesElement(series.getIdentifier(), flavor.getType());
1316 } catch (SeriesException e) {
1317 throw new IndexServiceException(String.format("Unable to delete catalog from series '%s' with type '%s'",
1318 series.getIdentifier(), flavor.getType()), e);
1319 }
1320
1321 if (!found) {
1322 throw new NotFoundException(String.format("Unable to find a catalog for series '%s' with flavor '%s'",
1323 series.getIdentifier(), flavor));
1324 }
1325 }
1326
1327 @Override
1328 public MetadataList updateEventMetadata(String id, MetadataList metadataList, ElasticsearchIndex index)
1329 throws IndexServiceException, SearchIndexException, NotFoundException, UnauthorizedException {
1330 Optional<Event> optEvent = getEvent(id, index);
1331 if (optEvent.isEmpty()) {
1332 throw new NotFoundException("Cannot find an event with id " + id);
1333 }
1334
1335 Event event = optEvent.get();
1336 MediaPackage mediaPackage = getEventMediapackage(event);
1337 updateMediaPackageMetadata(mediaPackage, metadataList);
1338 switch (getEventSource(event)) {
1339 case WORKFLOW:
1340 try {
1341 Optional<WorkflowInstance> workflowInstance = workflowService.
1342 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
1343 if (workflowInstance.isEmpty()) {
1344 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1345 }
1346 WorkflowInstance instance = workflowInstance.get();
1347 instance.setMediaPackage(mediaPackage);
1348 updateWorkflowInstance(instance);
1349 } catch (WorkflowException e) {
1350 throw new IndexServiceException("Unable to update workflow event " + id + " with metadata "
1351 + RestUtils.getJsonStringSilent(MetadataJson.listToJson(metadataList, true)), e);
1352 }
1353 break;
1354 case ARCHIVE:
1355 assetManager.takeSnapshot(mediaPackage);
1356 break;
1357 case SCHEDULE:
1358 DublinCoreMetadataCollection eventCatalog = metadataList.getMetadataByAdapter(getCommonEventCatalogUIAdapter());
1359 Optional<Set<String>> presenters = eventCatalog == null ? Optional.empty() : updatePresenters(eventCatalog);
1360 try {
1361 schedulerService.updateEvent(id, Optional.empty(), Optional.empty(), Optional.empty(), presenters,
1362 Optional.of(mediaPackage), Optional.empty(), Optional.empty());
1363 } catch (SchedulerException e) {
1364 throw new IndexServiceException("Unable to update scheduled event " + id + " with metadata "
1365 + RestUtils.getJsonStringSilent(MetadataJson.listToJson(metadataList, true)), e);
1366 }
1367 break;
1368 default:
1369 logger.error("Unknown event source!");
1370 }
1371 return metadataList;
1372 }
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383 protected Tuple<List<String>, Set<String>> getTechnicalPresenters(DublinCoreMetadataCollection eventMetadata) {
1384 MetadataField presentersMetadataField = eventMetadata.getOutputFields()
1385 .get(DublinCore.PROPERTY_CREATOR.getLocalName());
1386 List<String> presenters = new ArrayList<>();
1387 Set<String> technicalPresenters = new HashSet<>();
1388 for (String presenter : MetadataUtils.getIterableStringMetadata(presentersMetadataField)) {
1389 User user = userDirectoryService.loadUser(presenter);
1390 if (user == null) {
1391 presenters.add(presenter);
1392 } else {
1393 String fullname = StringUtils.isNotBlank(user.getName()) ? user.getName() : user.getUsername();
1394 presenters.add(fullname);
1395 technicalPresenters.add(user.getUsername());
1396 }
1397 }
1398 return Tuple.tuple(presenters, technicalPresenters);
1399 }
1400
1401 @Override
1402 public AccessControlList updateEventAcl(String id, AccessControlList acl, ElasticsearchIndex index)
1403 throws IllegalArgumentException, IndexServiceException, SearchIndexException, NotFoundException,
1404 UnauthorizedException {
1405 Optional<Event> optEvent = getEvent(id, index);
1406 if (optEvent.isEmpty()) {
1407 throw new NotFoundException("Cannot find an event with id " + id);
1408 }
1409
1410 Event event = optEvent.get();
1411 MediaPackage mediaPackage = getEventMediapackage(event);
1412 switch (getEventSource(event)) {
1413 case WORKFLOW:
1414
1415 throw new IllegalArgumentException("Unable to update the ACL of this event as it is currently processing.");
1416 case ARCHIVE:
1417 try {
1418 mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
1419 } catch (MediaPackageException e) {
1420 throw new IndexServiceException("Unable to update acl", e);
1421 }
1422 assetManager.takeSnapshot(mediaPackage);
1423 return acl;
1424 case SCHEDULE:
1425 try {
1426 mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
1427 schedulerService.updateEvent(id, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
1428 Optional.of(mediaPackage), Optional.empty(), Optional.empty());
1429 } catch (SchedulerException | MediaPackageException e) {
1430 throw new IndexServiceException("Unable to update the acl for the scheduled event", e);
1431 }
1432 return acl;
1433 default:
1434 throw new IndexServiceException(
1435 String.format("Unable to update the ACL as '%s' is an unknown event source.", getEventSource(event)));
1436 }
1437 }
1438
1439 private boolean hasSnapshots(String eventId) {
1440 return assetManager.snapshotExists(eventId);
1441 }
1442
1443 @Override
1444 public Map<String, Map<String, String>> getEventWorkflowProperties(final List<String> eventIds) {
1445 return WorkflowPropertiesUtil.getLatestWorkflowPropertiesForEvents(assetManager, eventIds);
1446 }
1447
1448 @Override
1449 public Optional<Event> getEvent(String id, ElasticsearchIndex index) throws SearchIndexException {
1450 SearchResult<Event> result = index
1451 .getByQuery(new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser())
1452 .withIdentifier(id));
1453
1454 if (result.getPageSize() == 0) {
1455 logger.debug("Didn't find event with id {}", id);
1456 return Optional.empty();
1457 }
1458 return Optional.of(result.getItems()[0].getSource());
1459 }
1460
1461 @Override
1462 public EventRemovalResult removeEvent(Event event, String retractWorkflowId)
1463 throws UnauthorizedException, WorkflowDatabaseException, NotFoundException {
1464 final boolean hasOnlyEngageLive = event.getPublications().size() == 1
1465 && EventUtils.ENGAGE_LIVE_CHANNEL_ID.equals(event.getPublications().get(0).getChannel());
1466 final boolean retract = event.hasPreview()
1467 || (!event.getPublications().isEmpty() && !hasOnlyEngageLive && this.hasSnapshots(event.getIdentifier()));
1468 if (retract) {
1469 retractAndRemoveEvent(event.getIdentifier(), retractWorkflowId);
1470 return EventRemovalResult.RETRACTING;
1471 } else {
1472 try {
1473 final boolean success = removeEvent(event.getIdentifier());
1474 return success ? EventRemovalResult.SUCCESS : EventRemovalResult.GENERAL_FAILURE;
1475 } catch (NotFoundException e) {
1476 return EventRemovalResult.NOT_FOUND;
1477 }
1478 }
1479 }
1480
1481 private void retractAndRemoveEvent(String id, String retractWorkflowId)
1482 throws WorkflowDatabaseException, NotFoundException {
1483 final WorkflowDefinition wfd = workflowService.getWorkflowDefinitionById(retractWorkflowId);
1484 final Workflows workflows = new Workflows(assetManager, workflowService);
1485 final ConfiguredWorkflow workflow = workflow(wfd);
1486 final List<WorkflowInstance> result = workflows.applyWorkflowToLatestVersion(Collections.singleton(id), workflow);
1487 if (result.size() != 1) {
1488 throw new IllegalStateException("Couldn't start workflow to retract media package" + id);
1489 }
1490 this.retractions.put(
1491 result.get(0).getId(),
1492 new Retraction(securityService.getUser(), securityService.getOrganization())
1493 );
1494 }
1495
1496 @Override
1497 public boolean removeEvent(String id) throws NotFoundException, UnauthorizedException {
1498 boolean unauthorizedWorkflow = false;
1499 boolean notFoundWorkflow = false;
1500 boolean removedWorkflow = false;
1501 try {
1502 List<WorkflowInstance> workflowInstances = workflowService.getWorkflowInstancesByMediaPackage(id);
1503 if (workflowInstances.isEmpty()) {
1504 notFoundWorkflow = true;
1505 } else {
1506 var toRemove = workflowInstances.size();
1507 for (WorkflowInstance instance : workflowInstances) {
1508 try {
1509 workflowService.stop(instance.getId());
1510 workflowService.remove(instance.getId());
1511 toRemove--;
1512 } catch (WorkflowDatabaseException e) {
1513 if (e.getCause() instanceof NotFoundException) {
1514
1515 logger.warn("Workflow {} has already been removed", instance.getId());
1516 } else {
1517 throw e;
1518 }
1519 }
1520 }
1521 removedWorkflow = toRemove == 0;
1522 notFoundWorkflow = toRemove >= 1;
1523 }
1524 } catch (UnauthorizedException e) {
1525 unauthorizedWorkflow = true;
1526 } catch (WorkflowException e) {
1527 logger.error("Unable to remove the event '{}' because removing workflow failed:", id, e);
1528 }
1529
1530 boolean unauthorizedScheduler = false;
1531 boolean notFoundScheduler = false;
1532 boolean removedScheduler = false;
1533 try {
1534 schedulerService.removeEvent(id);
1535 removedScheduler = true;
1536 } catch (NotFoundException e) {
1537 notFoundScheduler = true;
1538 } catch (UnauthorizedException e) {
1539 unauthorizedScheduler = true;
1540 } catch (SchedulerException e) {
1541 logger.error("Unable to remove the event '{}' from scheduler service:", id, e);
1542 }
1543
1544 boolean unauthorizedArchive = false;
1545 boolean notFoundArchive = false;
1546 boolean removedArchive = false;
1547 try {
1548 List<Snapshot> snapshots = assetManager.getSnapshotsById(id);
1549 if (snapshots.size() > 0) {
1550 assetManager.deleteSnapshots(id);
1551 removedArchive = true;
1552 } else {
1553 notFoundArchive = true;
1554 }
1555 } catch (AssetManagerException e) {
1556 if (e.getCause() instanceof UnauthorizedException) {
1557 unauthorizedArchive = true;
1558 } else if (e.getCause() instanceof NotFoundException) {
1559 notFoundArchive = true;
1560 } else {
1561 logger.error("Unable to remove the event '{}' from the archive:", id, e);
1562 }
1563 }
1564
1565 if (unauthorizedScheduler || unauthorizedWorkflow || unauthorizedArchive) {
1566 throw new UnauthorizedException("Not authorized to remove event id " + id);
1567 }
1568
1569
1570
1571 if ((removedScheduler || notFoundScheduler) && (removedWorkflow || notFoundWorkflow)
1572 && (removedArchive || notFoundArchive)) {
1573 try {
1574 elasticsearchIndex.deleteEvent(id, securityService.getOrganization().getId());
1575 } catch (SearchIndexException e) {
1576 logger.error("Removing event {} from the {} index failed", id, elasticsearchIndex.getIndexName(), e);
1577 }
1578 }
1579
1580 try {
1581 eventCommentService.deleteComments(id);
1582 } catch (EventCommentException e) {
1583 logger.error("Unable to remove comments for event '{}':", id, e);
1584 }
1585
1586 if (notFoundScheduler && notFoundWorkflow && notFoundArchive) {
1587 throw new NotFoundException("Event id " + id + " not found.");
1588 }
1589
1590 return ((removedScheduler || notFoundScheduler) && (removedWorkflow || notFoundWorkflow)
1591 && (removedArchive || notFoundArchive));
1592 }
1593
1594 private void updateWorkflowInstance(WorkflowInstance workflowInstance)
1595 throws WorkflowException, UnauthorizedException {
1596
1597 if (WorkflowInstance.WorkflowState.FAILED.equals(workflowInstance.getState())
1598 || WorkflowInstance.WorkflowState.FAILING.equals(workflowInstance.getState())
1599 || WorkflowInstance.WorkflowState.STOPPED.equals(workflowInstance.getState())
1600 || WorkflowInstance.WorkflowState.SUCCEEDED.equals(workflowInstance.getState())) {
1601 logger.info("Skip updating {} workflow mediapackage {} with updated comments catalog",
1602 workflowInstance.getState(), workflowInstance.getMediaPackage().getIdentifier().toString());
1603 return;
1604 }
1605 workflowService.update(workflowInstance);
1606 }
1607
1608 @Override
1609 public MediaPackage getEventMediapackage(Event event) throws IndexServiceException {
1610 switch (getEventSource(event)) {
1611 case WORKFLOW:
1612 try {
1613 Optional<WorkflowInstance> currentWorkflowInstance = workflowService.
1614 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.READ.toString());
1615 if (currentWorkflowInstance.isEmpty()) {
1616 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1617 }
1618 return currentWorkflowInstance.get().getMediaPackage();
1619 } catch (WorkflowDatabaseException e) {
1620 throw new IndexServiceException("Unable to get current workflow instance for event with id "
1621 + event.getIdentifier() + " from workflow service", e);
1622 } catch (UnauthorizedException e) {
1623 throw new IndexServiceException("Not authorized to read media package " + event.getIdentifier()
1624 + " from workflow", e);
1625 } catch (WorkflowException e) {
1626 throw new IndexServiceException("Unable to get event media package " + event.getIdentifier()
1627 + " from WorkflowService because", e);
1628 }
1629 case ARCHIVE:
1630 Optional<MediaPackage> mpOpt = assetManager.getMediaPackage(event.getIdentifier());
1631 if (mpOpt.isPresent()) {
1632 logger.debug("Found event in archive with id {}", event.getIdentifier());
1633 return mpOpt.get();
1634 }
1635 throw new IndexServiceException("No archived event found with id " + event.getIdentifier());
1636 case SCHEDULE:
1637 try {
1638 MediaPackage mediaPackage = schedulerService.getMediaPackage(event.getIdentifier());
1639 logger.debug("Found event in scheduler with id {}", event.getIdentifier());
1640 return mediaPackage;
1641 } catch (NotFoundException e) {
1642 throw new IndexServiceException("No scheduled event with id " + event.getIdentifier(), e);
1643 } catch (UnauthorizedException e) {
1644 throw new IndexServiceException("Unauthorized to get event " + event.getIdentifier() + " from scheduler", e);
1645 } catch (SchedulerException e) {
1646 throw new IndexServiceException("Unable to get event " + event.getIdentifier() + " from scheduler", e);
1647 }
1648 default:
1649 throw new IllegalStateException("Unknown event type!");
1650 }
1651 }
1652
1653
1654
1655
1656
1657
1658
1659
1660 @Override
1661 public Source getEventSource(Event event) {
1662 if (event.getWorkflowId() != null && isWorkflowActive(event.getWorkflowState())) {
1663 return Source.WORKFLOW;
1664 } else if (event.isScheduledEvent() && !event.hasRecordingStarted()) {
1665 return Source.SCHEDULE;
1666 } else if (event.getArchiveVersion() != null) {
1667 return Source.ARCHIVE;
1668 } else if (event.getWorkflowId() != null) {
1669 return Source.WORKFLOW;
1670 } else {
1671 return Source.SCHEDULE;
1672 }
1673 }
1674
1675 private void updateMediaPackageMetadata(MediaPackage mp, MetadataList metadataList) {
1676 String oldSeriesId = mp.getSeries();
1677 for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
1678 final DublinCoreMetadataCollection metadata = metadataList.getMetadataByAdapter(catalogUIAdapter);
1679 if (metadata != null && metadata.isUpdated()) {
1680 catalogUIAdapter.storeFields(mp, metadata);
1681 }
1682 }
1683
1684
1685 if (!StringUtils.equals(oldSeriesId, mp.getSeries())) {
1686 List<String> seriesDcTags = new ArrayList<>();
1687 List<String> seriesAclTags = new ArrayList<>();
1688 Map<String, List<String>> seriesExtDcTags = new HashMap<>();
1689 if (StringUtils.isNotBlank(oldSeriesId)) {
1690
1691 for (MediaPackageElement mpe : mp.getElementsByFlavor(MediaPackageElements.SERIES)) {
1692 mp.remove(mpe);
1693 seriesDcTags.addAll(Arrays.asList(mpe.getTags()));
1694 }
1695 if (mp.getSeries() != null || mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_EPISODE).length > 0) {
1696
1697
1698 for (MediaPackageElement mpe : mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_SERIES)) {
1699 mp.remove(mpe);
1700 seriesAclTags.addAll(Arrays.asList(mpe.getTags()));
1701 }
1702 } else {
1703
1704
1705
1706
1707 Tuple<AccessControlList, AclScope> activeAcl = authorizationService.getActiveAcl(mp);
1708 try {
1709 authorizationService.setAcl(mp, AclScope.Episode, activeAcl.getA());
1710 authorizationService.removeAcl(mp, AclScope.Series);
1711 } catch (MediaPackageException e) {
1712 throw new IllegalStateException("Unable to set episode ACL on media package", e);
1713 }
1714 }
1715
1716 try {
1717 Optional<Map<String, byte[]>> oldSeriesElementsOpt = seriesService.getSeriesElements(oldSeriesId);
1718 if (oldSeriesElementsOpt.isPresent()) {
1719 var oldSeriesElements = oldSeriesElementsOpt.get();
1720 for (String oldSeriesElementType : oldSeriesElements.keySet()) {
1721 for (MediaPackageElement mpe : mp
1722 .getElementsByFlavor(MediaPackageElementFlavor.flavor(oldSeriesElementType, "series"))) {
1723 mp.remove(mpe);
1724 String elementType = mpe.getFlavor().getType();
1725 if (StringUtils.isNotBlank(elementType)) {
1726
1727 if (!seriesExtDcTags.containsKey(elementType)) {
1728
1729 seriesExtDcTags.put(elementType, new ArrayList<>());
1730 }
1731 for (String tag : mpe.getTags()) {
1732 seriesExtDcTags.get(elementType).add(tag);
1733 }
1734 }
1735 }
1736 }
1737 }
1738 } catch (SeriesException e) {
1739 logger.info("Unable to retrieve series element types from series service for the series {}", oldSeriesId, e);
1740 }
1741 }
1742
1743 if (StringUtils.isNotBlank(mp.getSeries())) {
1744
1745 try {
1746 DublinCoreCatalog seriesDC = seriesService.getSeries(mp.getSeries());
1747 if (seriesDC != null) {
1748 mp.setSeriesTitle(seriesDC.getFirst(DublinCore.PROPERTY_TITLE));
1749 try (InputStream in = IOUtils.toInputStream(seriesDC.toXmlString(), "UTF-8")) {
1750 String elementId = UUID.randomUUID().toString();
1751 URI catalogUrl = workspace.put(mp.getIdentifier().toString(), elementId, "dublincore.xml", in);
1752 MediaPackageElement mpe = mp.add(catalogUrl, MediaPackageElement.Type.Catalog,
1753 MediaPackageElements.SERIES);
1754 mpe.setIdentifier(elementId);
1755 mpe.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, workspace.read(catalogUrl)));
1756 if (StringUtils.isNotBlank(oldSeriesId)) {
1757 for (String tag : seriesDcTags) {
1758 mpe.addTag(tag);
1759 }
1760 } else {
1761
1762 mpe.addTag("archive");
1763 }
1764 } catch (IOException e) {
1765 throw new IllegalStateException("Unable to add the series dublincore to the media package "
1766 + mp.getIdentifier(), e);
1767 }
1768 }
1769 } catch (SeriesException e) {
1770 throw new IllegalStateException("Unable to retrieve series dublincore catalog for the series "
1771 + mp.getSeries(), e);
1772 } catch (NotFoundException | UnauthorizedException e) {
1773 throw new IllegalArgumentException("Unable to retrieve series dublincore catalog for the series "
1774 + mp.getSeries(), e);
1775 }
1776
1777 try {
1778 AccessControlList seriesAccessControl = seriesService.getSeriesAccessControl(mp.getSeries());
1779 if (seriesAccessControl != null) {
1780 mp = authorizationService.setAcl(mp, AclScope.Series, seriesAccessControl).getA();
1781 for (MediaPackageElement seriesAclMpe : mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_SERIES)) {
1782 if (StringUtils.isNotBlank(oldSeriesId)) {
1783 for (String tag : seriesAclTags) {
1784 seriesAclMpe.addTag(tag);
1785 }
1786 } else {
1787
1788 seriesAclMpe.addTag("archive");
1789 }
1790 }
1791 }
1792 } catch (SeriesException | MediaPackageException e) {
1793 throw new IllegalStateException("Unable to retrieve series ACL for series " + oldSeriesId, e);
1794 } catch (NotFoundException e) {
1795 logger.debug("There is no ACL set for the series {}", mp.getSeries());
1796 }
1797
1798 try {
1799 Optional<Map<String, byte[]>> seriesElementsOpt = seriesService.getSeriesElements(mp.getSeries());
1800 if (seriesElementsOpt.isPresent()) {
1801 var seriesElements = seriesElementsOpt.get();
1802 for (String seriesElementType : seriesElements.keySet()) {
1803 try (InputStream in = new ByteArrayInputStream(seriesElements.get(seriesElementType))) {
1804 String elementId = UUID.randomUUID().toString();
1805 URI catalogUrl = workspace.put(mp.getIdentifier().toString(), elementId, "dublincore.xml", in);
1806 MediaPackageElement mpe = mp.add(catalogUrl, MediaPackageElement.Type.Catalog,
1807 MediaPackageElementFlavor.flavor(seriesElementType, "series"));
1808 mpe.setIdentifier(elementId);
1809 mpe.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, workspace.read(catalogUrl)));
1810 if (StringUtils.isNotBlank(oldSeriesId)) {
1811 if (seriesExtDcTags.containsKey(seriesElementType)) {
1812 for (String tag : seriesExtDcTags.get(seriesElementType)) {
1813 mpe.addTag(tag);
1814 }
1815 }
1816 } else {
1817
1818 mpe.addTag("archive");
1819 }
1820 } catch (IOException e) {
1821 throw new IllegalStateException(String.format("Unable to serialize series element %s for the series %s",
1822 seriesElementType, mp.getSeries()), e);
1823 } catch (NotFoundException e) {
1824 throw new IllegalArgumentException("Unable to retrieve series element dublincore catalog for the "
1825 + "series " + mp.getSeries(), e);
1826 }
1827 }
1828 }
1829 } catch (SeriesException e) {
1830 throw new IllegalStateException("Unable to retrieve series elements for the series " + mp.getSeries(), e);
1831 }
1832 }
1833 }
1834 }
1835
1836 @Override
1837 public String createSeries(MetadataList metadataList, Map<String, String> options, Optional<AccessControlList> optAcl,
1838 Optional<Long> optThemeId) throws IndexServiceException {
1839 DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
1840 dc.set(PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
1841 dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
1842 for (Entry<String, String> entry : options.entrySet()) {
1843 dc.set(new EName(DublinCores.OC_PROPERTY_NS_URI, entry.getKey()), entry.getValue());
1844 }
1845
1846 DublinCoreMetadataCollection seriesMetadata = metadataList.getMetadataByFlavor(
1847 MediaPackageElements.SERIES.toString());
1848 if (seriesMetadata != null) {
1849 DublinCoreMetadataUtil.updateDublincoreCatalog(dc, seriesMetadata);
1850 }
1851
1852 AccessControlList acl;
1853 if (optAcl.isPresent()) {
1854 acl = optAcl.get();
1855 } else {
1856 acl = new AccessControlList();
1857 }
1858
1859 String seriesId;
1860 try {
1861 DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
1862 seriesId = createdSeries.getFirst(PROPERTY_IDENTIFIER);
1863 seriesService.updateAccessControl(seriesId, acl);
1864 if (optThemeId.isPresent()) {
1865 seriesService.updateSeriesProperty(seriesId, THEME_PROPERTY_NAME, Long.toString(optThemeId.get()));
1866 }
1867 } catch (Exception e) {
1868 logger.error("Unable to create new series:", e);
1869 throw new IndexServiceException("Unable to create new series");
1870 }
1871
1872 updateSeriesMetadata(seriesId, metadataList);
1873
1874 return seriesId;
1875 }
1876
1877 @Override
1878 public String createSeries(JSONObject metadata)
1879 throws IllegalArgumentException, IndexServiceException, UnauthorizedException {
1880
1881 JSONArray seriesMetadataJson = (JSONArray) metadata.get("metadata");
1882 if (seriesMetadataJson == null) {
1883 throw new IllegalArgumentException("No metadata field in metadata");
1884 }
1885
1886 JSONObject options = (JSONObject) metadata.get("options");
1887 if (options == null) {
1888 throw new IllegalArgumentException("No options field in metadata");
1889 }
1890
1891 Optional<Long> themeId = Optional.empty();
1892 Long theme = (Long) metadata.get("theme");
1893 if (theme != null) {
1894 themeId = Optional.of(theme);
1895 }
1896
1897 Map<String, String> optionsMap;
1898 try {
1899 optionsMap = JSONUtils.toMap(new org.codehaus.jettison.json.JSONObject(options.toJSONString()));
1900 } catch (JSONException e) {
1901 throw new IllegalArgumentException("Unable to parse options to map", e);
1902 }
1903
1904 DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
1905 dc.set(PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
1906 dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
1907 for (Entry<String, String> entry : optionsMap.entrySet()) {
1908 dc.set(new EName(DublinCores.OC_PROPERTY_NS_URI, entry.getKey()), entry.getValue());
1909 }
1910
1911 final MetadataList metadataList = getMetadataListWithAllSeriesCatalogUIAdapters();
1912 MetadataJson.fillListFromJson(metadataList, seriesMetadataJson);
1913
1914 DublinCoreMetadataCollection seriesMetadata = metadataList.getMetadataByFlavor(
1915 MediaPackageElements.SERIES.toString());
1916 if (seriesMetadata != null) {
1917 DublinCoreMetadataUtil.updateDublincoreCatalog(dc, seriesMetadata);
1918 }
1919
1920 AccessControlList acl = getAccessControlList(metadata);
1921
1922 String seriesId;
1923 try {
1924 DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
1925 seriesId = createdSeries.getFirst(PROPERTY_IDENTIFIER);
1926 seriesService.updateAccessControl(seriesId, acl);
1927 if (themeId.isPresent()) {
1928 seriesService.updateSeriesProperty(seriesId, THEME_PROPERTY_NAME, Long.toString(themeId.get()));
1929 }
1930 } catch (Exception e) {
1931 throw new IndexServiceException("Unable to create new series", e);
1932 }
1933
1934 updateSeriesMetadata(seriesId, metadataList);
1935
1936 return seriesId;
1937 }
1938
1939 @Override
1940 public void removeSeries(String id) throws NotFoundException, SeriesException, UnauthorizedException {
1941 seriesService.deleteSeries(id);
1942 }
1943
1944 @Override
1945 public MetadataList updateAllSeriesMetadata(String id, String metadataJSON, ElasticsearchIndex index)
1946 throws IllegalArgumentException, IndexServiceException, NotFoundException {
1947 MetadataList metadataList = getMetadataListWithAllSeriesCatalogUIAdapters();
1948 return updateSeriesMetadata(id, metadataJSON, index, metadataList);
1949 }
1950
1951 @Override
1952 public MetadataList updateAllSeriesMetadata(String id, MetadataList metadataList, ElasticsearchIndex index)
1953 throws IndexServiceException, NotFoundException {
1954 checkSeriesExists(id, index);
1955 updateSeriesMetadata(id, metadataList);
1956 return metadataList;
1957 }
1958
1959 @Override
1960 public void updateCommentCatalog(final Event event, final List<EventComment> comments) throws Exception {
1961 final SecurityContext securityContext = new SecurityContext(securityService, securityService.getOrganization(),
1962 securityService.getUser());
1963 executorService.execute(() -> securityContext.runInContext(() -> {
1964 try {
1965 MediaPackage mediaPackage = getEventMediapackage(event);
1966 updateMediaPackageCommentCatalog(mediaPackage, comments);
1967 switch (getEventSource(event)) {
1968 case WORKFLOW:
1969 logger.info("Update workflow media pacakge {} with updated comments catalog.", event.getIdentifier());
1970 Optional<WorkflowInstance> workflowInstance = workflowService.getRunningWorkflowInstanceByMediaPackage(
1971 event.getIdentifier(), Permissions.Action.WRITE.toString());
1972 if (workflowInstance.isEmpty()) {
1973 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1974 }
1975 WorkflowInstance instance = workflowInstance.get();
1976 instance.setMediaPackage(mediaPackage);
1977 updateWorkflowInstance(instance);
1978 break;
1979 case ARCHIVE:
1980 logger.info("Update archive mediapacakge {} with updated comments catalog.", event.getIdentifier());
1981 assetManager.takeSnapshot(mediaPackage);
1982 break;
1983 case SCHEDULE:
1984 logger.info("Update scheduled mediapacakge {} with updated comments catalog.", event.getIdentifier());
1985 schedulerService.updateEvent(event.getIdentifier(), Optional.empty(), Optional.empty(), Optional.empty(),
1986 Optional.empty(), Optional.of(mediaPackage), Optional.empty(), Optional.empty());
1987 break;
1988 default:
1989 logger.error("Unknown event source {}!", event.getSource());
1990 }
1991 } catch (Exception e) {
1992 logger.error("Unable to update event {} comment catalog", event.getIdentifier(), e);
1993 }
1994 }));
1995 }
1996
1997 private void updateMediaPackageCommentCatalog(MediaPackage mediaPackage, List<EventComment> comments)
1998 throws EventCommentException, IOException {
1999
2000 Catalog[] commentCatalogs = mediaPackage.getCatalogs(MediaPackageElements.COMMENTS);
2001 Catalog c = null;
2002 if (commentCatalogs.length == 1) {
2003 c = commentCatalogs[0];
2004 }
2005
2006 if (comments.size() > 0) {
2007
2008 if (c == null) {
2009 c = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder().newElement(Type.Catalog,
2010 MediaPackageElements.COMMENTS);
2011 c.generateIdentifier();
2012 mediaPackage.add(c);
2013 }
2014
2015
2016 InputStream in = null;
2017 try {
2018 String commentCatalog = EventCommentParser.getAsXml(comments);
2019 in = IOUtils.toInputStream(commentCatalog, "UTF-8");
2020 URI uri = workspace.put(mediaPackage.getIdentifier().toString(), c.getIdentifier(), "comments.xml", in);
2021 c.setURI(uri);
2022
2023 c.setChecksum(null);
2024 } finally {
2025 IOUtils.closeQuietly(in);
2026 }
2027 } else {
2028
2029 if (c != null) {
2030 mediaPackage.remove(c);
2031 try {
2032 workspace.delete(c.getURI());
2033 } catch (NotFoundException e) {
2034 logger.warn("Comments catalog {} not found to delete!", c.getURI());
2035 }
2036 }
2037 }
2038 }
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052 private void checkSeriesExists(String seriesID, ElasticsearchIndex index)
2053 throws NotFoundException, IndexServiceException {
2054 try {
2055 Optional<Series> optSeries = index.getSeries(seriesID, securityService.getOrganization().getId(),
2056 securityService.getUser());
2057 if (optSeries.isEmpty()) {
2058 throw new NotFoundException("Cannot find a series with id " + seriesID);
2059 }
2060 } catch (SearchIndexException e) {
2061 throw new IndexServiceException("Unable to get a series with id: " + seriesID, e);
2062 }
2063 }
2064
2065 private MetadataList updateSeriesMetadata(
2066 final String seriesID,
2067 final String metadataJSON,
2068 final ElasticsearchIndex index,
2069 final MetadataList metadataList)
2070 throws IllegalArgumentException, IndexServiceException, NotFoundException {
2071 checkSeriesExists(seriesID, index);
2072 try {
2073 MetadataJson.fillListFromJson(metadataList, (JSONArray) new JSONParser().parse(metadataJSON));
2074 } catch (final org.json.simple.parser.ParseException e) {
2075 throw new IllegalArgumentException("Not able to parse the event metadata: " + metadataJSON, e);
2076 }
2077
2078 updateSeriesMetadata(seriesID, metadataList);
2079 return metadataList;
2080 }
2081
2082
2083
2084
2085
2086 @Override
2087 public MetadataList getMetadataListWithAllSeriesCatalogUIAdapters() {
2088 MetadataList metadataList = new MetadataList();
2089 for (SeriesCatalogUIAdapter adapter : getSeriesCatalogUIAdapters()) {
2090 metadataList.add(adapter.getFlavor().toString(), adapter.getUITitle(), adapter.getRawFields());
2091 }
2092 return metadataList;
2093 }
2094
2095 @Override
2096 public MetadataList getMetadataListWithAllEventCatalogUIAdapters() {
2097 MetadataList metadataList = new MetadataList();
2098 for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
2099 metadataList.add(catalogUIAdapter, catalogUIAdapter.getRawFields());
2100 }
2101 return metadataList;
2102 }
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112 private void updateSeriesMetadata(String seriesId, MetadataList metadataList) {
2113 for (SeriesCatalogUIAdapter adapter : seriesCatalogUIAdapters) {
2114 final DublinCoreMetadataCollection metadata = metadataList.getMetadataByFlavor(adapter.getFlavor().toString());
2115 if (metadata != null && metadata.isUpdated()) {
2116 adapter.storeFields(seriesId, metadata);
2117 }
2118 }
2119 }
2120
2121 public boolean isWorkflowActive(String workflowState) {
2122 return WorkflowState.INSTANTIATED.toString().equals(workflowState)
2123 || WorkflowState.RUNNING.toString().equals(workflowState)
2124 || WorkflowState.PAUSED.toString().equals(workflowState);
2125 }
2126
2127 }