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