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) throws IndexServiceException, UnsupportedAssetException {
631 JSONObject metadataJson = null;
632
633
634
635
636
637
638
639 List<String> assetList = new LinkedList<String>();
640
641 try {
642 if (!ServletFileUpload.isMultipartContent(request)) {
643 throw new IllegalArgumentException("No multipart content");
644 }
645 for (FileItemIterator iter = new ServletFileUpload().getItemIterator(request); iter.hasNext();) {
646 FileItemStream item = iter.next();
647 String fieldName = item.getFieldName();
648 if (item.isFormField()) {
649 if ("metadata".equals(fieldName)) {
650 String metadata = Streams.asString(item.openStream());
651 try {
652 metadataJson = (JSONObject) parser.parse(metadata);
653 } catch (Exception e) {
654 logger.warn("Unable to parse metadata {}", metadata);
655 throw new IllegalArgumentException("Unable to parse metadata");
656 }
657 }
658 } else {
659
660 fieldName = fieldName.substring(0, fieldName.lastIndexOf("."));
661 final MediaType mediaType = MediaType.parse(item.getContentType());
662 final boolean accepted = RequestUtils.typeIsAccepted(item.getName(), fieldName, mediaType,
663 listProvidersService);
664 if (!accepted) {
665 throw new UnsupportedAssetException("Provided file format " + mediaType.toString() + " not allowed.");
666 }
667 if (item.getFieldName().toLowerCase().matches(attachmentRegex)) {
668 assetList.add(item.getFieldName());
669
670 mp = ingestService.addAttachment(item.openStream(), item.getName(),
671 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
672 } else if (item.getFieldName().toLowerCase().matches(catalogRegex)) {
673 assetList.add(item.getFieldName());
674
675 mp = ingestService.addCatalog(item.openStream(), item.getName(),
676 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
677 } else if (item.getFieldName().toLowerCase().matches(trackRegex)) {
678
679 assetList.add(item.getFieldName());
680 mp = ingestService.addTrack(item.openStream(), item.getName(),
681 new MediaPackageElementFlavor(item.getFieldName(), "*"), mp);
682 } else {
683 logger.warn("Unknown field name found {}", item.getFieldName());
684 }
685 }
686 }
687
688
689 try {
690 JSONArray assetMetadata = (JSONArray)((JSONObject) metadataJson.get("assets")).get("options");
691 if (assetMetadata != null) {
692 mp = updateMpAssetFlavor(assetList, mp, assetMetadata);
693 } else {
694 logger.warn("The asset option mapping parameter was not found");
695 throw new IndexServiceException("The asset option mapping parameter was not found");
696 }
697 } catch (Exception e) {
698
699 logger.warn("Unable to process asset metadata {}", metadataJson.get("assets"), e);
700 throw new IllegalArgumentException("Unable to parse metadata", e);
701 }
702
703 return startAddAssetWorkflow(metadataJson, mp);
704 } catch (MediaPackageException | FileUploadException | IOException | IngestException e) {
705 logger.error("Unable to create event:", e);
706 throw new IndexServiceException("Unable to create event", e);
707 }
708 }
709
710
711
712
713
714
715
716
717
718
719
720
721
722 private String startAddAssetWorkflow(JSONObject metadataJson, MediaPackage mediaPackage)
723 throws IndexServiceException {
724 String wfId = null;
725 String mpId = mediaPackage.getIdentifier().toString();
726
727 JSONObject processing = (JSONObject) metadataJson.get("processing");
728 if (processing == null)
729 throw new IllegalArgumentException("No processing field in metadata");
730
731 String workflowDefId = (String) processing.get("workflow");
732 if (workflowDefId == null)
733 throw new IllegalArgumentException("No workflow definition field in processing metadata");
734
735 JSONObject configJson = (JSONObject) processing.get("configuration");
736
737 try {
738
739
740 Map<String, String> params = new HashMap<String, String>();
741 if (configJson != null) {
742 for (Object key: configJson.keySet()) {
743 params.put((String)key, (String) configJson.get(key));
744 }
745 }
746
747 WorkflowInstance workflowInstance = workflowService.start(
748 workflowService.getWorkflowDefinitionById(workflowDefId), mediaPackage, params);
749 logger.info("Asset update and publish workflow {} scheduled for mp {}", workflowInstance.getId(), mpId);
750 } catch (AssetManagerException | WorkflowParsingException | UnauthorizedException e) {
751 throw new IndexServiceException("Unable to start workflow " + workflowDefId + " on " + mpId);
752 } catch (WorkflowDatabaseException e) {
753 logger.warn("Unable to load workflow '{}' from workflow service:", wfId, e);
754 } catch (NotFoundException e) {
755 logger.warn("Workflow '{}' not found", wfId);
756 }
757 return wfId;
758 }
759
760
761
762
763
764
765
766
767
768
769 private SourceType getSourceType(JSONObject source) {
770 SourceType type;
771 try {
772 type = SourceType.valueOf((String) source.get("type"));
773 } catch (Exception e) {
774 logger.error("Unknown source type '{}'", source.get("type"));
775 throw new IllegalArgumentException("Unknown source type");
776 }
777 return type;
778 }
779
780
781
782
783
784
785
786
787
788
789 private AccessControlList getAccessControlList(JSONObject metadataJson) {
790 AccessControlList acl = new AccessControlList();
791 JSONObject accessJson = (JSONObject) metadataJson.get("access");
792 if (accessJson != null) {
793 try {
794 acl = AccessControlParser.parseAcl(accessJson.toJSONString());
795 } catch (Exception e) {
796 throw new IllegalArgumentException("Unable to parse access control list: " + accessJson.toJSONString());
797 }
798 }
799 return acl;
800 }
801
802 public String createEvent(JSONObject metadataJson, MediaPackage mp) throws ParseException, IOException,
803 MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException {
804 if (metadataJson == null)
805 throw new IllegalArgumentException("No metadata set");
806
807 JSONObject source = (JSONObject) metadataJson.get("source");
808 if (source == null)
809 throw new IllegalArgumentException("No source field in metadata");
810
811 JSONObject processing = (JSONObject) metadataJson.get("processing");
812 if (processing == null)
813 throw new IllegalArgumentException("No processing field in metadata");
814
815 JSONArray allEventMetadataJson = (JSONArray) metadataJson.get("metadata");
816 if (allEventMetadataJson == null)
817 throw new IllegalArgumentException("No metadata field in metadata");
818
819 AccessControlList acl = getAccessControlList(metadataJson);
820
821 MetadataList metadataList = getMetadataListWithAllEventCatalogUIAdapters();
822 MetadataJson.fillListFromJson(metadataList, allEventMetadataJson);
823
824 EventHttpServletRequest eventHttpServletRequest = new EventHttpServletRequest();
825 eventHttpServletRequest.setAcl(acl);
826 eventHttpServletRequest.setMetadataList(metadataList);
827 eventHttpServletRequest.setMediaPackage(mp);
828 eventHttpServletRequest.setProcessing(processing);
829 eventHttpServletRequest.setSource(source);
830
831 return createEvent(eventHttpServletRequest);
832 }
833
834 @Override
835 public String createEvent(EventHttpServletRequest eventHttpServletRequest) throws ParseException, IOException,
836 MediaPackageException, IngestException, NotFoundException, SchedulerException, UnauthorizedException {
837
838 if (eventHttpServletRequest.getAcl().isEmpty()) {
839 throw new IllegalArgumentException("No access control list available to create new event.");
840 }
841 if (eventHttpServletRequest.getMediaPackage().isEmpty()) {
842 throw new IllegalArgumentException("No mediapackage available to create new event.");
843 }
844 if (eventHttpServletRequest.getMetadataList().isEmpty()) {
845 throw new IllegalArgumentException("No metadata list available to create new event.");
846 }
847 if (eventHttpServletRequest.getProcessing().isEmpty()) {
848 throw new IllegalArgumentException("No processing metadata available to create new event.");
849 }
850 if (eventHttpServletRequest.getSource().isEmpty()) {
851 throw new IllegalArgumentException("No source field metadata available to create new event.");
852 }
853
854
855 String workflowTemplate = (String) eventHttpServletRequest.getProcessing().get().get("workflow");
856 if (workflowTemplate == null)
857 throw new IllegalArgumentException("No workflow template in metadata");
858
859
860 SourceType type = getSourceType(eventHttpServletRequest.getSource().get());
861
862 DublinCoreMetadataCollection eventMetadata = eventHttpServletRequest.getMetadataList().get()
863 .getMetadataByAdapter(getCommonEventCatalogUIAdapter());
864
865 Date currentStartDate = null;
866 JSONObject sourceMetadata = (JSONObject) eventHttpServletRequest.getSource().get().get("metadata");
867 if (sourceMetadata != null
868 && (type.equals(SourceType.SCHEDULE_SINGLE) || type.equals(SourceType.SCHEDULE_MULTIPLE))) {
869 try {
870 MetadataField current = eventMetadata.getOutputFields().get("location");
871 eventMetadata.updateStringField(current, (String) sourceMetadata.get("device"));
872 } catch (Exception e) {
873 logger.warn("Unable to parse device {}", sourceMetadata.get("device"));
874 throw new IllegalArgumentException("Unable to parse device");
875 }
876 if (StringUtils.isNotEmpty((String) sourceMetadata.get("start"))) {
877 currentStartDate = EncodingSchemeUtils.decodeDate((String) sourceMetadata.get("start"));
878 }
879 }
880
881 MetadataField startDate = eventMetadata.getOutputFields().get("startDate");
882 if (startDate != null && startDate.isUpdated() && startDate.getValue() != null) {
883 SimpleDateFormat sdf = MetadataField.getSimpleDateFormatter(startDate.getPattern());
884 currentStartDate = sdf.parse((String) startDate.getValue());
885 } else if (currentStartDate != null) {
886 eventMetadata.removeField(startDate);
887 MetadataField newStartDate = new MetadataField(startDate);
888 newStartDate.setValue(EncodingSchemeUtils.encodeDate(currentStartDate, Precision.Fraction).getValue());
889 eventMetadata.addField(newStartDate);
890 }
891
892
893
894
895
896
897 MetadataField created = eventMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName());
898 if (created != null && (!created.isUpdated() || created.getValue() == null)) {
899 eventMetadata.removeField(created);
900 MetadataField newCreated = new MetadataField(created);
901 if (currentStartDate != null) {
902 newCreated.setValue(EncodingSchemeUtils.encodeDate(currentStartDate, Precision.Second).getValue());
903 } else {
904 newCreated.setValue(EncodingSchemeUtils.encodeDate(new Date(), Precision.Second).getValue());
905 }
906 eventMetadata.addField(newCreated);
907 }
908
909
910 Set<String> presenterUsernames = new HashSet<>();
911 Optional<Set<String>> technicalPresenters = updatePresenters(eventMetadata);
912 if (technicalPresenters.isPresent()) {
913 presenterUsernames = technicalPresenters.get();
914 }
915
916 eventHttpServletRequest.getMetadataList().get().add(getCommonEventCatalogUIAdapter(), eventMetadata);
917 updateMediaPackageMetadata(eventHttpServletRequest.getMediaPackage().get(),
918 eventHttpServletRequest.getMetadataList().get());
919
920 DublinCoreCatalog dc = getDublinCoreCatalog(eventHttpServletRequest);
921 String captureAgentId = null;
922 TimeZone tz = null;
923 org.joda.time.DateTime start = null;
924 org.joda.time.DateTime end = null;
925 long duration = 0L;
926 Properties caProperties = new Properties();
927 RRule rRule = null;
928 if (sourceMetadata != null
929 && (type.equals(SourceType.SCHEDULE_SINGLE) || type.equals(SourceType.SCHEDULE_MULTIPLE))) {
930 Properties configuration;
931 try {
932 captureAgentId = (String) sourceMetadata.get("device");
933 configuration = captureAgentStateService.getAgentConfiguration((String) sourceMetadata.get("device"));
934 } catch (Exception e) {
935 logger.warn("Unable to parse device {}: because:", sourceMetadata.get("device"), e);
936 throw new IllegalArgumentException("Unable to parse device");
937 }
938
939 String durationString = (String) sourceMetadata.get("duration");
940 if (StringUtils.isBlank(durationString))
941 throw new IllegalArgumentException("No duration in source metadata");
942
943
944 String agentTimeZone = configuration.getProperty("capture.device.timezone");
945 if (StringUtils.isNotBlank(agentTimeZone)) {
946 tz = TimeZone.getTimeZone(agentTimeZone);
947 dc.set(DublinCores.OC_PROPERTY_AGENT_TIMEZONE, tz.getID());
948 } else {
949 tz = TimeZone.getDefault();
950 logger.debug(
951 "The field 'capture.device.timezone' has not been set in the agent configuration. The default server timezone will be used.");
952 }
953
954 org.joda.time.DateTime now = new org.joda.time.DateTime(DateTimeZone.UTC);
955 start = now.withMillis(DateTimeSupport.fromUTC((String) sourceMetadata.get("start")));
956 end = now.withMillis(DateTimeSupport.fromUTC((String) sourceMetadata.get("end")));
957 duration = Long.parseLong(durationString);
958 DublinCoreValue period = EncodingSchemeUtils
959 .encodePeriod(new DCMIPeriod(start.toDate(), start.plus(duration).toDate()), Precision.Second);
960 String inputs = (String) sourceMetadata.get("inputs");
961
962 caProperties.putAll(configuration);
963 dc.set(DublinCore.PROPERTY_TEMPORAL, period);
964 caProperties.put(CaptureParameters.CAPTURE_DEVICE_NAMES, inputs);
965 }
966
967 if (type.equals(SourceType.SCHEDULE_MULTIPLE)) {
968 rRule = new RRule((String) sourceMetadata.get("rrule"));
969 }
970
971 Map<String, String> configuration = new HashMap<>();
972 if (eventHttpServletRequest.getProcessing().get().get("configuration") != null) {
973 configuration = new HashMap<>((JSONObject) eventHttpServletRequest.getProcessing().get().get("configuration"));
974
975 }
976 for (Entry<String, String> entry : configuration.entrySet()) {
977 caProperties.put(WORKFLOW_CONFIG_PREFIX.concat(entry.getKey()), entry.getValue());
978 }
979 caProperties.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowTemplate);
980
981 eventHttpServletRequest.setMediaPackage(authorizationService.setAcl(eventHttpServletRequest.getMediaPackage().get(),
982 AclScope.Episode, eventHttpServletRequest.getAcl().get()).getA());
983
984 MediaPackage mediaPackage;
985 switch (type) {
986 case UPLOAD:
987 case UPLOAD_LATER:
988 eventHttpServletRequest
989 .setMediaPackage(updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc));
990 configuration.put("workflowDefinitionId", workflowTemplate);
991 WorkflowInstance ingest = ingestService.ingest(eventHttpServletRequest.getMediaPackage().get(),
992 workflowTemplate, configuration);
993 return eventHttpServletRequest.getMediaPackage().get().getIdentifier().toString();
994 case SCHEDULE_SINGLE:
995 mediaPackage = updateDublincCoreCatalog(eventHttpServletRequest.getMediaPackage().get(), dc);
996 eventHttpServletRequest.setMediaPackage(mediaPackage);
997 try {
998 schedulerService.addEvent(start.toDate(), start.plus(duration).toDate(), captureAgentId, presenterUsernames,
999 mediaPackage, configuration, (Map) caProperties, Optional.empty());
1000 } finally {
1001 for (MediaPackageElement mediaPackageElement : mediaPackage.getElements()) {
1002 try {
1003 workspace.delete(mediaPackage.getIdentifier().toString(), mediaPackageElement.getIdentifier());
1004 } catch (NotFoundException | IOException e) {
1005 logger.warn("Failed to delete media package element", e);
1006 }
1007 }
1008 }
1009 return mediaPackage.getIdentifier().toString();
1010 case SCHEDULE_MULTIPLE:
1011 final Map<String, Period> scheduled = schedulerService.addMultipleEvents(rRule, start.toDate(), end.toDate(), duration, tz, captureAgentId,
1012 presenterUsernames, eventHttpServletRequest.getMediaPackage().get(), configuration, (Map) caProperties, Optional.empty());
1013 return StringUtils.join(scheduled.keySet(), ",");
1014 default:
1015 throw new IllegalArgumentException("Unknown source type: " + type);
1016 }
1017 }
1018
1019
1020
1021
1022
1023
1024
1025
1026 private DublinCoreCatalog getDublinCoreCatalog(EventHttpServletRequest eventHttpServletRequest) {
1027 DublinCoreCatalog dc;
1028 Optional<DublinCoreCatalog> dcOpt = DublinCoreUtil.loadEpisodeDublinCore(workspace,
1029 eventHttpServletRequest.getMediaPackage().get());
1030 if (dcOpt.isPresent()) {
1031 dc = dcOpt.get();
1032
1033 dc.addBindings(XmlNamespaceContext
1034 .mk(XmlNamespaceBinding.mk(DublinCores.OC_PROPERTY_NS_PREFIX, DublinCores.OC_PROPERTY_NS_URI)));
1035 } else {
1036 dc = DublinCores.mkOpencastEpisode().getCatalog();
1037 }
1038 return dc;
1039 }
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050 private Optional<Set<String>> updatePresenters(DublinCoreMetadataCollection eventMetadata) {
1051 MetadataField presentersMetadataField = eventMetadata.getOutputFields()
1052 .get(DublinCore.PROPERTY_CREATOR.getLocalName());
1053 if (presentersMetadataField.isUpdated()) {
1054 Tuple<List<String>, Set<String>> updatedPresenters = getTechnicalPresenters(eventMetadata);
1055 Set<String> presenterUsernames = updatedPresenters.getB();
1056 eventMetadata.removeField(presentersMetadataField);
1057 MetadataField newPresentersMetadataField = new MetadataField(presentersMetadataField);
1058 newPresentersMetadataField.setValue(updatedPresenters.getA());
1059 eventMetadata.addField(newPresentersMetadataField);
1060 return Optional.of(presenterUsernames);
1061 } else {
1062 return Optional.empty();
1063 }
1064 }
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080 private MediaPackage updateDublincCoreCatalog(MediaPackage mp, DublinCoreCatalog dc)
1081 throws IOException, MediaPackageException, IngestException {
1082 try (InputStream inputStream = IOUtils.toInputStream(dc.toXmlString(), "UTF-8")) {
1083
1084 Catalog[] catalogs = mp.getCatalogs(MediaPackageElements.EPISODE);
1085 if (catalogs.length > 0) {
1086 Catalog catalog = catalogs[0];
1087 URI uri = workspace.put(mp.getIdentifier().toString(), catalog.getIdentifier(), "dublincore.xml", inputStream);
1088 catalog.setURI(uri);
1089
1090 catalog.setChecksum(null);
1091 } else {
1092 mp = ingestService.addCatalog(inputStream, "dublincore.xml", MediaPackageElements.EPISODE, mp);
1093 }
1094 }
1095 return mp;
1096 }
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109 @SuppressWarnings("unchecked")
1110 protected MediaPackage updateMpAssetFlavor(List<String> assetList, MediaPackage mp, JSONArray assetMetadata) {
1111
1112 JSONObject assetDataMap = new JSONObject();
1113 for (int i = 0; i < assetMetadata.size(); i++) {
1114 try {
1115 assetDataMap.put(((JSONObject) assetMetadata.get(i)).get("id"), assetMetadata.get(i));
1116 } catch (Exception e) {
1117 throw new IllegalArgumentException("Unable to parse metadata", e);
1118 }
1119 }
1120
1121 for (String assetOrig: assetList) {
1122
1123 String asset = assetOrig;
1124 String assetNumber = null;
1125 String[] assetNameParts = asset.split(Pattern.quote("."));
1126 if (assetNameParts.length > 1) {
1127 asset = assetNameParts[0];
1128 assetNumber = assetNameParts[1];
1129 }
1130 try {
1131 if ((assetMetadata != null) && (assetDataMap.get(asset) != null)) {
1132 String type = (String)((JSONObject) assetDataMap.get(asset)).get("type");
1133 String flavorType = (String)((JSONObject) assetDataMap.get(asset)).get("flavorType");
1134 String flavorSubType = (String)((JSONObject) assetDataMap.get(asset)).get("flavorSubType");
1135 String tags = (String)((JSONObject) assetDataMap.get(asset)).get("tags");
1136 String[] tagsArray = null;
1137
1138 String langTag = null;
1139 if (tags != null) {
1140 tagsArray = tags.split(",");
1141 for (String tag : tagsArray) {
1142 if (StringUtils.startsWith(StringUtils.trimToEmpty(tag), "lang:")) {
1143 langTag = StringUtils.trimToEmpty(tag);
1144 break;
1145 }
1146 }
1147 }
1148
1149 boolean overwriteExisting = !(Boolean) ((JSONObject) assetDataMap.get(asset)).getOrDefault("multiple", false);
1150 if (patternNumberedAsset.matcher(flavorSubType).matches() && (assetNumber != null)) {
1151 flavorSubType = assetNumber;
1152 }
1153 MediaPackageElementFlavor newElemflavor = new MediaPackageElementFlavor(flavorType, flavorSubType);
1154 if (patternAttachment.matcher(type).matches()) {
1155 if (overwriteExisting) {
1156
1157 Attachment[] existing = mp.getAttachments(newElemflavor);
1158 for (int i = 0; i < existing.length; i++) {
1159
1160 if (null == langTag || existing[i].containsTag(langTag)) {
1161 mp.remove(existing[i]);
1162 logger.info("Overwriting existing asset {} {}", type, newElemflavor);
1163 }
1164 }
1165 }
1166
1167 Attachment[] elArray = mp.getAttachments(new MediaPackageElementFlavor(assetOrig, "*"));
1168 elArray[0].setFlavor(newElemflavor);
1169 if (tags != null && tagsArray.length > 0) {
1170 for (String tag : tagsArray) {
1171 elArray[0].addTag(tag);
1172 }
1173 }
1174 logger.info("Updated asset {} {}", type, newElemflavor);
1175 } else if (patternCatalog.matcher(type).matches()) {
1176 if (overwriteExisting) {
1177
1178 Catalog[] existing = mp.getCatalogs(newElemflavor);
1179 for (int i = 0; i < existing.length; i++) {
1180
1181 if (null == langTag || existing[i].containsTag(langTag)) {
1182 mp.remove(existing[i]);
1183 logger.info("Overwriting existing asset {} {}", type, newElemflavor);
1184 }
1185 }
1186 }
1187 Catalog[] catArray = mp.getCatalogs(new MediaPackageElementFlavor(assetOrig, "*"));
1188 if (catArray.length > 1) {
1189 throw new IllegalArgumentException("More than one " + asset + " found, only one expected.");
1190 }
1191 catArray[0].setFlavor(newElemflavor);
1192 if (tags != null && tagsArray.length > 0) {
1193 for (String tag : tagsArray) {
1194 catArray[0].addTag(tag);
1195 }
1196 }
1197 logger.info("Update asset {} {}", type, newElemflavor);
1198 } else if (patternTrack.matcher(type).matches()) {
1199 if (overwriteExisting) {
1200
1201 Track[] existing = mp.getTracks(newElemflavor);
1202 for (int i = 0; i < existing.length; i++) {
1203
1204 if (null == langTag || existing[i].containsTag(langTag)) {
1205 mp.remove(existing[i]);
1206 logger.info("Overwriting existing asset {} {}", type, newElemflavor);
1207 }
1208 }
1209 }
1210 Track[] trackArray = mp.getTracks(new MediaPackageElementFlavor(assetOrig, "*"));
1211 if (trackArray.length > 1) {
1212 throw new IllegalArgumentException("More than one " + asset + " found, only one expected.");
1213 }
1214 trackArray[0].setFlavor(newElemflavor);
1215 if (tags != null && tagsArray.length > 0) {
1216 for (String tag : tagsArray) {
1217 trackArray[0].addTag(tag);
1218 }
1219 }
1220 logger.info("Update asset {} {}", type, newElemflavor);
1221 } else {
1222 logger.warn("Unknown asset type {} {} for field {}", type, newElemflavor, asset);
1223 }
1224 }
1225 } catch (Exception e) {
1226
1227 throw new IllegalArgumentException("Unable to parse metadata: " + assetMetadata.toJSONString(), e);
1228 }
1229 }
1230 return mp;
1231 }
1232
1233 @Override
1234 public MetadataList updateAllEventMetadata(
1235 final String id, final String metadataJSON, final ElasticsearchIndex index)
1236 throws IllegalArgumentException, IndexServiceException, NotFoundException, SearchIndexException,
1237 UnauthorizedException {
1238 final MetadataList metadataList;
1239 try {
1240 metadataList = getMetadataListWithAllEventCatalogUIAdapters();
1241 MetadataJson.fillListFromJson(metadataList, (JSONArray) new JSONParser().parse(metadataJSON));
1242 } catch (final org.json.simple.parser.ParseException e) {
1243 throw new IllegalArgumentException("Not able to parse the event metadata " + metadataJSON, e);
1244 }
1245 return updateEventMetadata(id, metadataList, index);
1246 }
1247
1248 @Override
1249 public void removeCatalogByFlavor(Event event, MediaPackageElementFlavor flavor)
1250 throws IndexServiceException, NotFoundException, UnauthorizedException {
1251 MediaPackage mediaPackage = getEventMediapackage(event);
1252 Catalog[] catalogs = mediaPackage.getCatalogs(flavor);
1253 if (catalogs.length == 0) {
1254 throw new NotFoundException(String.format("Cannot find a catalog with flavor '%s' for event with id '%s'.",
1255 flavor.toString(), event.getIdentifier()));
1256 }
1257 for (Catalog catalog : catalogs) {
1258 mediaPackage.remove(catalog);
1259 }
1260 switch (getEventSource(event)) {
1261 case WORKFLOW:
1262 try {
1263 Optional<WorkflowInstance> workflowInstance = workflowService.
1264 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
1265 if (workflowInstance.isEmpty()) {
1266 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1267 }
1268 WorkflowInstance instance = workflowInstance.get();
1269 instance.setMediaPackage(mediaPackage);
1270 updateWorkflowInstance(instance);
1271 } catch (WorkflowException e) {
1272 throw new IndexServiceException("Unable to remove catalog with flavor '" + flavor
1273 + "' by updating workflow event " + event.getIdentifier(), e);
1274 }
1275 break;
1276 case ARCHIVE:
1277 assetManager.takeSnapshot(mediaPackage);
1278 break;
1279 case SCHEDULE:
1280 try {
1281 schedulerService.updateEvent(event.getIdentifier(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
1282 Optional.of(mediaPackage), Optional.empty(), Optional.empty());
1283 } catch (SchedulerException e) {
1284 throw new IndexServiceException("Unable to remove catalog with flavor " + flavor + " by updating scheduled "
1285 + "event " + event.getIdentifier(), e);
1286 }
1287 break;
1288 default:
1289 throw new IndexServiceException(
1290 String.format("Unable to handle event source type '%s'", getEventSource(event)));
1291 }
1292 }
1293
1294 @Override
1295 public void removeCatalogByFlavor(Series series, MediaPackageElementFlavor flavor)
1296 throws NotFoundException, IndexServiceException {
1297 if (series == null) {
1298 throw new IllegalArgumentException("The series cannot be null.");
1299 }
1300 if (flavor == null) {
1301 throw new IllegalArgumentException("The flavor cannot be null.");
1302 }
1303 boolean found = false;
1304 try {
1305 found = seriesService.deleteSeriesElement(series.getIdentifier(), flavor.getType());
1306 } catch (SeriesException e) {
1307 throw new IndexServiceException(String.format("Unable to delete catalog from series '%s' with type '%s'",
1308 series.getIdentifier(), flavor.getType()), e);
1309 }
1310
1311 if (!found) {
1312 throw new NotFoundException(String.format("Unable to find a catalog for series '%s' with flavor '%s'",
1313 series.getIdentifier(), flavor));
1314 }
1315 }
1316
1317 @Override
1318 public MetadataList updateEventMetadata(String id, MetadataList metadataList, ElasticsearchIndex index)
1319 throws IndexServiceException, SearchIndexException, NotFoundException, UnauthorizedException {
1320 Optional<Event> optEvent = getEvent(id, index);
1321 if (optEvent.isEmpty())
1322 throw new NotFoundException("Cannot find an event with id " + id);
1323
1324 Event event = optEvent.get();
1325 MediaPackage mediaPackage = getEventMediapackage(event);
1326 updateMediaPackageMetadata(mediaPackage, metadataList);
1327 switch (getEventSource(event)) {
1328 case WORKFLOW:
1329 try {
1330 Optional<WorkflowInstance> workflowInstance = workflowService.
1331 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
1332 if (workflowInstance.isEmpty()) {
1333 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1334 }
1335 WorkflowInstance instance = workflowInstance.get();
1336 instance.setMediaPackage(mediaPackage);
1337 updateWorkflowInstance(instance);
1338 } catch (WorkflowException e) {
1339 throw new IndexServiceException("Unable to update workflow event " + id + " with metadata "
1340 + RestUtils.getJsonStringSilent(MetadataJson.listToJson(metadataList, true)), e);
1341 }
1342 break;
1343 case ARCHIVE:
1344 assetManager.takeSnapshot(mediaPackage);
1345 break;
1346 case SCHEDULE:
1347 DublinCoreMetadataCollection eventCatalog = metadataList.getMetadataByAdapter(getCommonEventCatalogUIAdapter());
1348 Optional<Set<String>> presenters = eventCatalog == null ? Optional.empty() : updatePresenters(eventCatalog);
1349 try {
1350 schedulerService.updateEvent(id, Optional.empty(), Optional.empty(), Optional.empty(), presenters, Optional.of(mediaPackage),
1351 Optional.empty(), Optional.empty());
1352 } catch (SchedulerException e) {
1353 throw new IndexServiceException("Unable to update scheduled event " + id + " with metadata "
1354 + RestUtils.getJsonStringSilent(MetadataJson.listToJson(metadataList, true)), e);
1355 }
1356 break;
1357 default:
1358 logger.error("Unknown event source!");
1359 }
1360 return metadataList;
1361 }
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372 protected Tuple<List<String>, Set<String>> getTechnicalPresenters(DublinCoreMetadataCollection eventMetadata) {
1373 MetadataField presentersMetadataField = eventMetadata.getOutputFields()
1374 .get(DublinCore.PROPERTY_CREATOR.getLocalName());
1375 List<String> presenters = new ArrayList<>();
1376 Set<String> technicalPresenters = new HashSet<>();
1377 for (String presenter : MetadataUtils.getIterableStringMetadata(presentersMetadataField)) {
1378 User user = userDirectoryService.loadUser(presenter);
1379 if (user == null) {
1380 presenters.add(presenter);
1381 } else {
1382 String fullname = StringUtils.isNotBlank(user.getName()) ? user.getName() : user.getUsername();
1383 presenters.add(fullname);
1384 technicalPresenters.add(user.getUsername());
1385 }
1386 }
1387 return Tuple.tuple(presenters, technicalPresenters);
1388 }
1389
1390 @Override
1391 public AccessControlList updateEventAcl(String id, AccessControlList acl, ElasticsearchIndex index)
1392 throws IllegalArgumentException, IndexServiceException, SearchIndexException, NotFoundException,
1393 UnauthorizedException {
1394 Optional<Event> optEvent = getEvent(id, index);
1395 if (optEvent.isEmpty())
1396 throw new NotFoundException("Cannot find an event with id " + id);
1397
1398 Event event = optEvent.get();
1399 MediaPackage mediaPackage = getEventMediapackage(event);
1400 switch (getEventSource(event)) {
1401 case WORKFLOW:
1402
1403 throw new IllegalArgumentException("Unable to update the ACL of this event as it is currently processing.");
1404 case ARCHIVE:
1405 try {
1406 mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
1407 } catch (MediaPackageException e) {
1408 throw new IndexServiceException("Unable to update acl", e);
1409 }
1410 assetManager.takeSnapshot(mediaPackage);
1411 return acl;
1412 case SCHEDULE:
1413 try {
1414 mediaPackage = authorizationService.setAcl(mediaPackage, AclScope.Episode, acl).getA();
1415 schedulerService.updateEvent(id, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(mediaPackage),
1416 Optional.empty(), Optional.empty());
1417 } catch (SchedulerException | MediaPackageException e) {
1418 throw new IndexServiceException("Unable to update the acl for the scheduled event", e);
1419 }
1420 return acl;
1421 default:
1422 throw new IndexServiceException(
1423 String.format("Unable to update the ACL as '%s' is an unknown event source.", getEventSource(event)));
1424 }
1425 }
1426
1427 private boolean hasSnapshots(String eventId) {
1428 return assetManager.snapshotExists(eventId);
1429 }
1430
1431 @Override
1432 public Map<String, Map<String, String>> getEventWorkflowProperties(final List<String> eventIds) {
1433 return WorkflowPropertiesUtil.getLatestWorkflowPropertiesForEvents(assetManager, eventIds);
1434 }
1435
1436 @Override
1437 public Optional<Event> getEvent(String id, ElasticsearchIndex index) throws SearchIndexException {
1438 SearchResult<Event> result = index
1439 .getByQuery(new EventSearchQuery(securityService.getOrganization().getId(), securityService.getUser())
1440 .withIdentifier(id));
1441
1442 if (result.getPageSize() == 0) {
1443 logger.debug("Didn't find event with id {}", id);
1444 return Optional.empty();
1445 }
1446 return Optional.of(result.getItems()[0].getSource());
1447 }
1448
1449 @Override
1450 public EventRemovalResult removeEvent(Event event, String retractWorkflowId)
1451 throws UnauthorizedException, WorkflowDatabaseException, NotFoundException {
1452 final boolean hasOnlyEngageLive = event.getPublications().size() == 1
1453 && EventUtils.ENGAGE_LIVE_CHANNEL_ID.equals(event.getPublications().get(0).getChannel());
1454 final boolean retract = event.hasPreview()
1455 || (!event.getPublications().isEmpty() && !hasOnlyEngageLive && this.hasSnapshots(event.getIdentifier()));
1456 if (retract) {
1457 retractAndRemoveEvent(event.getIdentifier(), retractWorkflowId);
1458 return EventRemovalResult.RETRACTING;
1459 } else {
1460 try {
1461 final boolean success = removeEvent(event.getIdentifier());
1462 return success ? EventRemovalResult.SUCCESS : EventRemovalResult.GENERAL_FAILURE;
1463 } catch (NotFoundException e) {
1464 return EventRemovalResult.NOT_FOUND;
1465 }
1466 }
1467 }
1468
1469 private void retractAndRemoveEvent(String id, String retractWorkflowId)
1470 throws WorkflowDatabaseException, NotFoundException {
1471 final WorkflowDefinition wfd = workflowService.getWorkflowDefinitionById(retractWorkflowId);
1472 final Workflows workflows = new Workflows(assetManager, workflowService);
1473 final ConfiguredWorkflow workflow = workflow(wfd);
1474 final List<WorkflowInstance> result = workflows.applyWorkflowToLatestVersion(Collections.singleton(id), workflow);
1475 if (result.size() != 1) {
1476 throw new IllegalStateException("Couldn't start workflow to retract media package" + id);
1477 }
1478 this.retractions.put(
1479 result.get(0).getId(),
1480 new Retraction(securityService.getUser(), securityService.getOrganization())
1481 );
1482 }
1483
1484 @Override
1485 public boolean removeEvent(String id) throws NotFoundException, UnauthorizedException {
1486 boolean unauthorizedWorkflow = false;
1487 boolean notFoundWorkflow = false;
1488 boolean removedWorkflow = false;
1489 try {
1490 List<WorkflowInstance> workflowInstances = workflowService.getWorkflowInstancesByMediaPackage(id);
1491 if (workflowInstances.isEmpty()) {
1492 notFoundWorkflow = true;
1493 } else {
1494 var toRemove = workflowInstances.size();
1495 for (WorkflowInstance instance : workflowInstances) {
1496 try {
1497 workflowService.stop(instance.getId());
1498 workflowService.remove(instance.getId());
1499 toRemove--;
1500 } catch (WorkflowDatabaseException e) {
1501 if (e.getCause() instanceof NotFoundException) {
1502
1503 logger.warn("Workflow {} has already been removed", instance.getId());
1504 } else {
1505 throw e;
1506 }
1507 }
1508 }
1509 removedWorkflow = toRemove == 0;
1510 notFoundWorkflow = toRemove >= 1;
1511 }
1512 } catch (UnauthorizedException e) {
1513 unauthorizedWorkflow = true;
1514 } catch (WorkflowException e) {
1515 logger.error("Unable to remove the event '{}' because removing workflow failed:", id, e);
1516 }
1517
1518 boolean unauthorizedScheduler = false;
1519 boolean notFoundScheduler = false;
1520 boolean removedScheduler = false;
1521 try {
1522 schedulerService.removeEvent(id);
1523 removedScheduler = true;
1524 } catch (NotFoundException e) {
1525 notFoundScheduler = true;
1526 } catch (UnauthorizedException e) {
1527 unauthorizedScheduler = true;
1528 } catch (SchedulerException e) {
1529 logger.error("Unable to remove the event '{}' from scheduler service:", id, e);
1530 }
1531
1532 boolean unauthorizedArchive = false;
1533 boolean notFoundArchive = false;
1534 boolean removedArchive = false;
1535 try {
1536 List<Snapshot> snapshots = assetManager.getSnapshotsById(id);
1537 if (snapshots.size() > 0) {
1538 assetManager.deleteSnapshots(id);
1539 removedArchive = true;
1540 } else {
1541 notFoundArchive = true;
1542 }
1543 } catch (AssetManagerException e) {
1544 if (e.getCause() instanceof UnauthorizedException) {
1545 unauthorizedArchive = true;
1546 } else if (e.getCause() instanceof NotFoundException) {
1547 notFoundArchive = true;
1548 } else {
1549 logger.error("Unable to remove the event '{}' from the archive:", id, e);
1550 }
1551 }
1552
1553 if (unauthorizedScheduler || unauthorizedWorkflow || unauthorizedArchive)
1554 throw new UnauthorizedException("Not authorized to remove event id " + id);
1555
1556
1557
1558 if ((removedScheduler || notFoundScheduler) && (removedWorkflow || notFoundWorkflow)
1559 && (removedArchive || notFoundArchive)) {
1560 try {
1561 elasticsearchIndex.deleteEvent(id, securityService.getOrganization().getId());
1562 } catch (SearchIndexException e) {
1563 logger.error("Removing event {} from the {} index failed", id, elasticsearchIndex.getIndexName(), e);
1564 }
1565 }
1566
1567 try {
1568 eventCommentService.deleteComments(id);
1569 } catch (EventCommentException e) {
1570 logger.error("Unable to remove comments for event '{}':", id, e);
1571 }
1572
1573 if (notFoundScheduler && notFoundWorkflow && notFoundArchive)
1574 throw new NotFoundException("Event id " + id + " not found.");
1575
1576 return ((removedScheduler || notFoundScheduler) && (removedWorkflow || notFoundWorkflow)
1577 && (removedArchive || notFoundArchive));
1578 }
1579
1580 private void updateWorkflowInstance(WorkflowInstance workflowInstance)
1581 throws WorkflowException, UnauthorizedException {
1582
1583 if (WorkflowInstance.WorkflowState.FAILED.equals(workflowInstance.getState())
1584 || WorkflowInstance.WorkflowState.FAILING.equals(workflowInstance.getState())
1585 || WorkflowInstance.WorkflowState.STOPPED.equals(workflowInstance.getState())
1586 || WorkflowInstance.WorkflowState.SUCCEEDED.equals(workflowInstance.getState())) {
1587 logger.info("Skip updating {} workflow mediapackage {} with updated comments catalog",
1588 workflowInstance.getState(), workflowInstance.getMediaPackage().getIdentifier().toString());
1589 return;
1590 }
1591 workflowService.update(workflowInstance);
1592 }
1593
1594 @Override
1595 public MediaPackage getEventMediapackage(Event event) throws IndexServiceException {
1596 switch (getEventSource(event)) {
1597 case WORKFLOW:
1598 try {
1599 Optional<WorkflowInstance> currentWorkflowInstance = workflowService.
1600 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.READ.toString());
1601 if (currentWorkflowInstance.isEmpty()) {
1602 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1603 }
1604 return currentWorkflowInstance.get().getMediaPackage();
1605 } catch (WorkflowDatabaseException e) {
1606 throw new IndexServiceException("Unable to get current workflow instance for event with id " + event.getIdentifier() + " from workflow service", e);
1607 } catch (UnauthorizedException e) {
1608 throw new IndexServiceException("Not authorized to read media package " + event.getIdentifier() + " from workflow", e);
1609 } catch (WorkflowException e) {
1610 throw new IndexServiceException("Unable to get event media package " + event.getIdentifier() + " from WorkflowService because", e);
1611 }
1612 case ARCHIVE:
1613 Optional<MediaPackage> mpOpt = assetManager.getMediaPackage(event.getIdentifier());
1614 if (mpOpt.isPresent()) {
1615 logger.debug("Found event in archive with id {}", event.getIdentifier());
1616 return mpOpt.get();
1617 }
1618 throw new IndexServiceException("No archived event found with id " + event.getIdentifier());
1619 case SCHEDULE:
1620 try {
1621 MediaPackage mediaPackage = schedulerService.getMediaPackage(event.getIdentifier());
1622 logger.debug("Found event in scheduler with id {}", event.getIdentifier());
1623 return mediaPackage;
1624 } catch (NotFoundException e) {
1625 throw new IndexServiceException("No scheduled event with id " + event.getIdentifier(), e);
1626 } catch (UnauthorizedException e) {
1627 throw new IndexServiceException("Unauthorized to get event " + event.getIdentifier() + " from scheduler", e);
1628 } catch (SchedulerException e) {
1629 throw new IndexServiceException("Unable to get event " + event.getIdentifier() + " from scheduler", e);
1630 }
1631 default:
1632 throw new IllegalStateException("Unknown event type!");
1633 }
1634 }
1635
1636
1637
1638
1639
1640
1641
1642
1643 @Override
1644 public Source getEventSource(Event event) {
1645 if (event.getWorkflowId() != null && isWorkflowActive(event.getWorkflowState())) {
1646 return Source.WORKFLOW;
1647 } else if (event.isScheduledEvent() && !event.hasRecordingStarted()) {
1648 return Source.SCHEDULE;
1649 } else if (event.getArchiveVersion() != null) {
1650 return Source.ARCHIVE;
1651 } else if (event.getWorkflowId() != null) {
1652 return Source.WORKFLOW;
1653 } else {
1654 return Source.SCHEDULE;
1655 }
1656 }
1657
1658 private void updateMediaPackageMetadata(MediaPackage mp, MetadataList metadataList) {
1659 String oldSeriesId = mp.getSeries();
1660 for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
1661 final DublinCoreMetadataCollection metadata = metadataList.getMetadataByAdapter(catalogUIAdapter);
1662 if (metadata != null && metadata.isUpdated()) {
1663 catalogUIAdapter.storeFields(mp, metadata);
1664 }
1665 }
1666
1667
1668 if (!StringUtils.equals(oldSeriesId, mp.getSeries())) {
1669 List<String> seriesDcTags = new ArrayList<>();
1670 List<String> seriesAclTags = new ArrayList<>();
1671 Map<String, List<String>> seriesExtDcTags = new HashMap<>();
1672 if (StringUtils.isNotBlank(oldSeriesId)) {
1673
1674 for (MediaPackageElement mpe : mp.getElementsByFlavor(MediaPackageElements.SERIES)) {
1675 mp.remove(mpe);
1676 seriesDcTags.addAll(Arrays.asList(mpe.getTags()));
1677 }
1678 if (mp.getSeries() != null || mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_EPISODE).length > 0) {
1679
1680
1681 for (MediaPackageElement mpe : mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_SERIES)) {
1682 mp.remove(mpe);
1683 seriesAclTags.addAll(Arrays.asList(mpe.getTags()));
1684 }
1685 } else {
1686
1687
1688
1689
1690 Tuple<AccessControlList, AclScope> activeAcl = authorizationService.getActiveAcl(mp);
1691 try {
1692 authorizationService.setAcl(mp, AclScope.Episode, activeAcl.getA());
1693 authorizationService.removeAcl(mp, AclScope.Series);
1694 } catch (MediaPackageException e) {
1695 throw new IllegalStateException("Unable to set episode ACL on media package", e);
1696 }
1697 }
1698
1699 try {
1700 Optional<Map<String, byte[]>> oldSeriesElementsOpt = seriesService.getSeriesElements(oldSeriesId);
1701 if (oldSeriesElementsOpt.isPresent()) {
1702 var oldSeriesElements = oldSeriesElementsOpt.get();
1703 for (String oldSeriesElementType : oldSeriesElements.keySet()) {
1704 for (MediaPackageElement mpe : mp
1705 .getElementsByFlavor(MediaPackageElementFlavor.flavor(oldSeriesElementType, "series"))) {
1706 mp.remove(mpe);
1707 String elementType = mpe.getFlavor().getType();
1708 if (StringUtils.isNotBlank(elementType)) {
1709
1710 if (!seriesExtDcTags.containsKey(elementType)) {
1711
1712 seriesExtDcTags.put(elementType, new ArrayList<>());
1713 }
1714 for (String tag : mpe.getTags()) {
1715 seriesExtDcTags.get(elementType).add(tag);
1716 }
1717 }
1718 }
1719 }
1720 }
1721 } catch (SeriesException e) {
1722 logger.info("Unable to retrieve series element types from series service for the series {}", oldSeriesId, e);
1723 }
1724 }
1725
1726 if (StringUtils.isNotBlank(mp.getSeries())) {
1727
1728 try {
1729 DublinCoreCatalog seriesDC = seriesService.getSeries(mp.getSeries());
1730 if (seriesDC != null) {
1731 mp.setSeriesTitle(seriesDC.getFirst(DublinCore.PROPERTY_TITLE));
1732 try (InputStream in = IOUtils.toInputStream(seriesDC.toXmlString(), "UTF-8")) {
1733 String elementId = UUID.randomUUID().toString();
1734 URI catalogUrl = workspace.put(mp.getIdentifier().toString(), elementId, "dublincore.xml", in);
1735 MediaPackageElement mpe = mp.add(catalogUrl, MediaPackageElement.Type.Catalog, MediaPackageElements.SERIES);
1736 mpe.setIdentifier(elementId);
1737 mpe.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, workspace.read(catalogUrl)));
1738 if (StringUtils.isNotBlank(oldSeriesId)) {
1739 for (String tag : seriesDcTags) {
1740 mpe.addTag(tag);
1741 }
1742 } else {
1743
1744 mpe.addTag("archive");
1745 }
1746 } catch (IOException e) {
1747 throw new IllegalStateException("Unable to add the series dublincore to the media package " + mp.getIdentifier(), e);
1748 }
1749 }
1750 } catch (SeriesException e) {
1751 throw new IllegalStateException("Unable to retrieve series dublincore catalog for the series " + mp.getSeries(), e);
1752 } catch (NotFoundException | UnauthorizedException e) {
1753 throw new IllegalArgumentException("Unable to retrieve series dublincore catalog for the series " + mp.getSeries(), e);
1754 }
1755
1756 try {
1757 AccessControlList seriesAccessControl = seriesService.getSeriesAccessControl(mp.getSeries());
1758 if (seriesAccessControl != null) {
1759 mp = authorizationService.setAcl(mp, AclScope.Series, seriesAccessControl).getA();
1760 for (MediaPackageElement seriesAclMpe : mp.getElementsByFlavor(MediaPackageElements.XACML_POLICY_SERIES)) {
1761 if (StringUtils.isNotBlank(oldSeriesId)) {
1762 for (String tag : seriesAclTags) {
1763 seriesAclMpe.addTag(tag);
1764 }
1765 } else {
1766
1767 seriesAclMpe.addTag("archive");
1768 }
1769 }
1770 }
1771 } catch (SeriesException | MediaPackageException e) {
1772 throw new IllegalStateException("Unable to retrieve series ACL for series " + oldSeriesId, e);
1773 } catch (NotFoundException e) {
1774 logger.debug("There is no ACL set for the series {}", mp.getSeries());
1775 }
1776
1777 try {
1778 Optional<Map<String, byte[]>> seriesElementsOpt = seriesService.getSeriesElements(mp.getSeries());
1779 if (seriesElementsOpt.isPresent()) {
1780 var seriesElements = seriesElementsOpt.get();
1781 for (String seriesElementType : seriesElements.keySet()) {
1782 try (InputStream in = new ByteArrayInputStream(seriesElements.get(seriesElementType))) {
1783 String elementId = UUID.randomUUID().toString();
1784 URI catalogUrl = workspace.put(mp.getIdentifier().toString(), elementId, "dublincore.xml", in);
1785 MediaPackageElement mpe = mp.add(catalogUrl, MediaPackageElement.Type.Catalog,
1786 MediaPackageElementFlavor.flavor(seriesElementType, "series"));
1787 mpe.setIdentifier(elementId);
1788 mpe.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, workspace.read(catalogUrl)));
1789 if (StringUtils.isNotBlank(oldSeriesId)) {
1790 if (seriesExtDcTags.containsKey(seriesElementType)) {
1791 for (String tag : seriesExtDcTags.get(seriesElementType)) {
1792 mpe.addTag(tag);
1793 }
1794 }
1795 } else {
1796
1797 mpe.addTag("archive");
1798 }
1799 } catch (IOException e) {
1800 throw new IllegalStateException(String.format("Unable to serialize series element %s for the series %s",
1801 seriesElementType, mp.getSeries()), e);
1802 } catch (NotFoundException e) {
1803 throw new IllegalArgumentException("Unable to retrieve series element dublincore catalog for the series "
1804 + mp.getSeries(), e);
1805 }
1806 }
1807 }
1808 } catch (SeriesException e) {
1809 throw new IllegalStateException("Unable to retrieve series elements for the series " + mp.getSeries(), e);
1810 }
1811 }
1812 }
1813 }
1814
1815 @Override
1816 public String createSeries(MetadataList metadataList, Map<String, String> options, Optional<AccessControlList> optAcl,
1817 Optional<Long> optThemeId) throws IndexServiceException {
1818 DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
1819 dc.set(PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
1820 dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
1821 for (Entry<String, String> entry : options.entrySet()) {
1822 dc.set(new EName(DublinCores.OC_PROPERTY_NS_URI, entry.getKey()), entry.getValue());
1823 }
1824
1825 DublinCoreMetadataCollection seriesMetadata = metadataList.getMetadataByFlavor(MediaPackageElements.SERIES.toString());
1826 if (seriesMetadata != null) {
1827 DublinCoreMetadataUtil.updateDublincoreCatalog(dc, seriesMetadata);
1828 }
1829
1830 AccessControlList acl;
1831 if (optAcl.isPresent()) {
1832 acl = optAcl.get();
1833 } else {
1834 acl = new AccessControlList();
1835 }
1836
1837 String seriesId;
1838 try {
1839 DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
1840 seriesId = createdSeries.getFirst(PROPERTY_IDENTIFIER);
1841 seriesService.updateAccessControl(seriesId, acl);
1842 if (optThemeId.isPresent())
1843 seriesService.updateSeriesProperty(seriesId, THEME_PROPERTY_NAME, Long.toString(optThemeId.get()));
1844 } catch (Exception e) {
1845 logger.error("Unable to create new series:", e);
1846 throw new IndexServiceException("Unable to create new series");
1847 }
1848
1849 updateSeriesMetadata(seriesId, metadataList);
1850
1851 return seriesId;
1852 }
1853
1854 @Override
1855 public String createSeries(JSONObject metadata)
1856 throws IllegalArgumentException, IndexServiceException, UnauthorizedException {
1857
1858 JSONArray seriesMetadataJson = (JSONArray) metadata.get("metadata");
1859 if (seriesMetadataJson == null)
1860 throw new IllegalArgumentException("No metadata field in metadata");
1861
1862 JSONObject options = (JSONObject) metadata.get("options");
1863 if (options == null)
1864 throw new IllegalArgumentException("No options field in metadata");
1865
1866 Optional<Long> themeId = Optional.empty();
1867 Long theme = (Long) metadata.get("theme");
1868 if (theme != null) {
1869 themeId = Optional.of(theme);
1870 }
1871
1872 Map<String, String> optionsMap;
1873 try {
1874 optionsMap = JSONUtils.toMap(new org.codehaus.jettison.json.JSONObject(options.toJSONString()));
1875 } catch (JSONException e) {
1876 throw new IllegalArgumentException("Unable to parse options to map", e);
1877 }
1878
1879 DublinCoreCatalog dc = DublinCores.mkOpencastSeries().getCatalog();
1880 dc.set(PROPERTY_IDENTIFIER, UUID.randomUUID().toString());
1881 dc.set(DublinCore.PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Second));
1882 for (Entry<String, String> entry : optionsMap.entrySet()) {
1883 dc.set(new EName(DublinCores.OC_PROPERTY_NS_URI, entry.getKey()), entry.getValue());
1884 }
1885
1886 final MetadataList metadataList = getMetadataListWithAllSeriesCatalogUIAdapters();
1887 MetadataJson.fillListFromJson(metadataList, seriesMetadataJson);
1888
1889 DublinCoreMetadataCollection seriesMetadata = metadataList.getMetadataByFlavor(MediaPackageElements.SERIES.toString());
1890 if (seriesMetadata != null) {
1891 DublinCoreMetadataUtil.updateDublincoreCatalog(dc, seriesMetadata);
1892 }
1893
1894 AccessControlList acl = getAccessControlList(metadata);
1895
1896 String seriesId;
1897 try {
1898 DublinCoreCatalog createdSeries = seriesService.updateSeries(dc);
1899 seriesId = createdSeries.getFirst(PROPERTY_IDENTIFIER);
1900 seriesService.updateAccessControl(seriesId, acl);
1901 if (themeId.isPresent())
1902 seriesService.updateSeriesProperty(seriesId, THEME_PROPERTY_NAME, Long.toString(themeId.get()));
1903 } catch (Exception e) {
1904 throw new IndexServiceException("Unable to create new series", e);
1905 }
1906
1907 updateSeriesMetadata(seriesId, metadataList);
1908
1909 return seriesId;
1910 }
1911
1912 @Override
1913 public void removeSeries(String id) throws NotFoundException, SeriesException, UnauthorizedException {
1914 seriesService.deleteSeries(id);
1915 }
1916
1917 @Override
1918 public MetadataList updateAllSeriesMetadata(String id, String metadataJSON, ElasticsearchIndex index)
1919 throws IllegalArgumentException, IndexServiceException, NotFoundException {
1920 MetadataList metadataList = getMetadataListWithAllSeriesCatalogUIAdapters();
1921 return updateSeriesMetadata(id, metadataJSON, index, metadataList);
1922 }
1923
1924 @Override
1925 public MetadataList updateAllSeriesMetadata(String id, MetadataList metadataList, ElasticsearchIndex index)
1926 throws IndexServiceException, NotFoundException {
1927 checkSeriesExists(id, index);
1928 updateSeriesMetadata(id, metadataList);
1929 return metadataList;
1930 }
1931
1932 @Override
1933 public void updateCommentCatalog(final Event event, final List<EventComment> comments) throws Exception {
1934 final SecurityContext securityContext = new SecurityContext(securityService, securityService.getOrganization(),
1935 securityService.getUser());
1936 executorService.execute(() -> securityContext.runInContext(() -> {
1937 try {
1938 MediaPackage mediaPackage = getEventMediapackage(event);
1939 updateMediaPackageCommentCatalog(mediaPackage, comments);
1940 switch (getEventSource(event)) {
1941 case WORKFLOW:
1942 logger.info("Update workflow media pacakge {} with updated comments catalog.", event.getIdentifier());
1943 Optional<WorkflowInstance> workflowInstance = workflowService.
1944 getRunningWorkflowInstanceByMediaPackage(event.getIdentifier(), Permissions.Action.WRITE.toString());
1945 if (workflowInstance.isEmpty()) {
1946 throw new IndexServiceException("No workflow instance found for event " + event.getIdentifier());
1947 }
1948 WorkflowInstance instance = workflowInstance.get();
1949 instance.setMediaPackage(mediaPackage);
1950 updateWorkflowInstance(instance);
1951 break;
1952 case ARCHIVE:
1953 logger.info("Update archive mediapacakge {} with updated comments catalog.", event.getIdentifier());
1954 assetManager.takeSnapshot(mediaPackage);
1955 break;
1956 case SCHEDULE:
1957 logger.info("Update scheduled mediapacakge {} with updated comments catalog.", event.getIdentifier());
1958 schedulerService.updateEvent(event.getIdentifier(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
1959 Optional.of(mediaPackage), Optional.empty(), Optional.empty());
1960 break;
1961 default:
1962 logger.error("Unknown event source {}!", event.getSource());
1963 }
1964 } catch (Exception e) {
1965 logger.error("Unable to update event {} comment catalog", event.getIdentifier(), e);
1966 }
1967 }));
1968 }
1969
1970 private void updateMediaPackageCommentCatalog(MediaPackage mediaPackage, List<EventComment> comments)
1971 throws EventCommentException, IOException {
1972
1973 Catalog[] commentCatalogs = mediaPackage.getCatalogs(MediaPackageElements.COMMENTS);
1974 Catalog c = null;
1975 if (commentCatalogs.length == 1)
1976 c = commentCatalogs[0];
1977
1978 if (comments.size() > 0) {
1979
1980 if (c == null) {
1981 c = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder().newElement(Type.Catalog,
1982 MediaPackageElements.COMMENTS);
1983 c.generateIdentifier();
1984 mediaPackage.add(c);
1985 }
1986
1987
1988 InputStream in = null;
1989 try {
1990 String commentCatalog = EventCommentParser.getAsXml(comments);
1991 in = IOUtils.toInputStream(commentCatalog, "UTF-8");
1992 URI uri = workspace.put(mediaPackage.getIdentifier().toString(), c.getIdentifier(), "comments.xml", in);
1993 c.setURI(uri);
1994
1995 c.setChecksum(null);
1996 } finally {
1997 IOUtils.closeQuietly(in);
1998 }
1999 } else {
2000
2001 if (c != null) {
2002 mediaPackage.remove(c);
2003 try {
2004 workspace.delete(c.getURI());
2005 } catch (NotFoundException e) {
2006 logger.warn("Comments catalog {} not found to delete!", c.getURI());
2007 }
2008 }
2009 }
2010 }
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024 private void checkSeriesExists(String seriesID, ElasticsearchIndex index)
2025 throws NotFoundException, IndexServiceException {
2026 try {
2027 Optional<Series> optSeries = index.getSeries(seriesID, securityService.getOrganization().getId(), securityService.getUser());
2028 if (optSeries.isEmpty())
2029 throw new NotFoundException("Cannot find a series with id " + seriesID);
2030 } catch (SearchIndexException e) {
2031 throw new IndexServiceException("Unable to get a series with id: " + seriesID, e);
2032 }
2033 }
2034
2035 private MetadataList updateSeriesMetadata(
2036 final String seriesID,
2037 final String metadataJSON,
2038 final ElasticsearchIndex index,
2039 final MetadataList metadataList)
2040 throws IllegalArgumentException, IndexServiceException, NotFoundException {
2041 checkSeriesExists(seriesID, index);
2042 try {
2043 MetadataJson.fillListFromJson(metadataList, (JSONArray) new JSONParser().parse(metadataJSON));
2044 } catch (final org.json.simple.parser.ParseException e) {
2045 throw new IllegalArgumentException("Not able to parse the event metadata: " + metadataJSON, e);
2046 }
2047
2048 updateSeriesMetadata(seriesID, metadataList);
2049 return metadataList;
2050 }
2051
2052
2053
2054
2055
2056 @Override
2057 public MetadataList getMetadataListWithAllSeriesCatalogUIAdapters() {
2058 MetadataList metadataList = new MetadataList();
2059 for (SeriesCatalogUIAdapter adapter : getSeriesCatalogUIAdapters()) {
2060 metadataList.add(adapter.getFlavor().toString(), adapter.getUITitle(), adapter.getRawFields());
2061 }
2062 return metadataList;
2063 }
2064
2065 @Override
2066 public MetadataList getMetadataListWithAllEventCatalogUIAdapters() {
2067 MetadataList metadataList = new MetadataList();
2068 for (EventCatalogUIAdapter catalogUIAdapter : getEventCatalogUIAdapters()) {
2069 metadataList.add(catalogUIAdapter, catalogUIAdapter.getRawFields());
2070 }
2071 return metadataList;
2072 }
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082 private void updateSeriesMetadata(String seriesId, MetadataList metadataList) {
2083 for (SeriesCatalogUIAdapter adapter : seriesCatalogUIAdapters) {
2084 final DublinCoreMetadataCollection metadata = metadataList.getMetadataByFlavor(adapter.getFlavor().toString());
2085 if (metadata != null && metadata.isUpdated()) {
2086 adapter.storeFields(seriesId, metadata);
2087 }
2088 }
2089 }
2090
2091 public boolean isWorkflowActive(String workflowState) {
2092 return WorkflowState.INSTANTIATED.toString().equals(workflowState)
2093 || WorkflowState.RUNNING.toString().equals(workflowState)
2094 || WorkflowState.PAUSED.toString().equals(workflowState);
2095 }
2096
2097 }