View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.editor;
23  
24  import static java.util.Collections.emptyList;
25  import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
26  import static org.opencastproject.util.data.Tuple.tuple;
27  
28  import org.opencastproject.assetmanager.api.AssetManager;
29  import org.opencastproject.assetmanager.api.AssetManagerException;
30  import org.opencastproject.assetmanager.util.WorkflowPropertiesUtil;
31  import org.opencastproject.assetmanager.util.Workflows;
32  import org.opencastproject.editor.api.EditingData;
33  import org.opencastproject.editor.api.EditorService;
34  import org.opencastproject.editor.api.EditorServiceException;
35  import org.opencastproject.editor.api.ErrorStatus;
36  import org.opencastproject.editor.api.LockData;
37  import org.opencastproject.editor.api.SegmentData;
38  import org.opencastproject.editor.api.TrackData;
39  import org.opencastproject.editor.api.TrackSubData;
40  import org.opencastproject.editor.api.WorkflowData;
41  import org.opencastproject.elasticsearch.api.SearchIndexException;
42  import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
43  import org.opencastproject.elasticsearch.index.objects.event.Event;
44  import org.opencastproject.index.service.api.IndexService;
45  import org.opencastproject.index.service.exception.IndexServiceException;
46  import org.opencastproject.index.service.impl.util.EventUtils;
47  import org.opencastproject.mediapackage.Attachment;
48  import org.opencastproject.mediapackage.Catalog;
49  import org.opencastproject.mediapackage.MediaPackage;
50  import org.opencastproject.mediapackage.MediaPackageElement;
51  import org.opencastproject.mediapackage.MediaPackageElementBuilder;
52  import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
53  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
54  import org.opencastproject.mediapackage.Publication;
55  import org.opencastproject.mediapackage.Track;
56  import org.opencastproject.mediapackage.attachment.AttachmentImpl;
57  import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
58  import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
59  import org.opencastproject.metadata.dublincore.MetadataJson;
60  import org.opencastproject.metadata.dublincore.MetadataList;
61  import org.opencastproject.security.api.AuthorizationService;
62  import org.opencastproject.security.api.Organization;
63  import org.opencastproject.security.api.SecurityConstants;
64  import org.opencastproject.security.api.SecurityService;
65  import org.opencastproject.security.api.UnauthorizedException;
66  import org.opencastproject.security.api.User;
67  import org.opencastproject.security.urlsigning.exception.UrlSigningException;
68  import org.opencastproject.security.urlsigning.service.UrlSigningService;
69  import org.opencastproject.security.urlsigning.utils.UrlSigningServiceOsgiUtil;
70  import org.opencastproject.smil.api.SmilException;
71  import org.opencastproject.smil.api.SmilResponse;
72  import org.opencastproject.smil.api.SmilService;
73  import org.opencastproject.smil.entity.api.Smil;
74  import org.opencastproject.smil.entity.media.api.SmilMediaObject;
75  import org.opencastproject.smil.entity.media.container.api.SmilMediaContainer;
76  import org.opencastproject.smil.entity.media.element.api.SmilMediaElement;
77  import org.opencastproject.util.MimeType;
78  import org.opencastproject.util.MimeTypes;
79  import org.opencastproject.util.NotFoundException;
80  import org.opencastproject.util.data.Tuple;
81  import org.opencastproject.workflow.api.ConfiguredWorkflow;
82  import org.opencastproject.workflow.api.WorkflowDatabaseException;
83  import org.opencastproject.workflow.api.WorkflowDefinition;
84  import org.opencastproject.workflow.api.WorkflowInstance;
85  import org.opencastproject.workflow.api.WorkflowService;
86  import org.opencastproject.workflow.api.WorkflowUtil;
87  import org.opencastproject.workflow.handler.distribution.InternalPublicationChannel;
88  import org.opencastproject.workspace.api.Workspace;
89  
90  import org.apache.commons.io.FileUtils;
91  import org.apache.commons.io.IOUtils;
92  import org.apache.commons.lang3.BooleanUtils;
93  import org.apache.commons.lang3.StringUtils;
94  import org.osgi.service.component.ComponentContext;
95  import org.osgi.service.component.annotations.Activate;
96  import org.osgi.service.component.annotations.Component;
97  import org.osgi.service.component.annotations.Modified;
98  import org.osgi.service.component.annotations.Reference;
99  import org.slf4j.Logger;
100 import org.slf4j.LoggerFactory;
101 import org.xml.sax.SAXException;
102 
103 import java.awt.datatransfer.MimeTypeParseException;
104 import java.io.ByteArrayInputStream;
105 import java.io.File;
106 import java.io.IOException;
107 import java.io.InputStream;
108 import java.net.URI;
109 import java.net.URISyntaxException;
110 import java.nio.charset.StandardCharsets;
111 import java.text.MessageFormat;
112 import java.util.ArrayList;
113 import java.util.Arrays;
114 import java.util.Base64;
115 import java.util.Collection;
116 import java.util.Collections;
117 import java.util.Comparator;
118 import java.util.Dictionary;
119 import java.util.HashMap;
120 import java.util.HashSet;
121 import java.util.Iterator;
122 import java.util.List;
123 import java.util.Map;
124 import java.util.Objects;
125 import java.util.Optional;
126 import java.util.Set;
127 import java.util.UUID;
128 import java.util.stream.Collectors;
129 
130 import javax.ws.rs.WebApplicationException;
131 import javax.xml.bind.JAXBException;
132 
133 
134 @Component(
135     property = {
136         "service.description=Editor Service"
137     },
138     immediate = true,
139     service = EditorService.class
140 )
141 public class EditorServiceImpl implements EditorService {
142 
143   /** The module specific logger */
144   private static final Logger logger = LoggerFactory.getLogger(EditorServiceImpl.class);
145 
146   /** Tag that marks workflow for being used from the editor tool */
147   private static final String EDITOR_WORKFLOW_TAG = "editor";
148 
149   private static EditorLock editorLock;
150 
151   private long expireSeconds = UrlSigningServiceOsgiUtil.DEFAULT_URL_SIGNING_EXPIRE_DURATION;
152 
153   private Boolean signWithClientIP = UrlSigningServiceOsgiUtil.DEFAULT_SIGN_WITH_CLIENT_IP;
154 
155   // service references
156   private IndexService index;
157   private AssetManager assetManager;
158   private SecurityService securityService;
159   private SmilService smilService;
160   private UrlSigningService urlSigningService;
161   private WorkflowService workflowService;
162   private Workspace workspace;
163   private AuthorizationService authorizationService;
164 
165 
166   private MediaPackageElementFlavor smilCatalogFlavor;
167   private String previewVideoSubtype;
168   private String previewTag;
169   private String previewSubtype;
170   private String waveformSubtype;
171   private String thumbnailSubType;
172   private MediaPackageElementFlavor smilSilenceFlavor;
173   private ElasticsearchIndex searchIndex;
174   private MediaPackageElementFlavor captionsFlavor;
175   private MediaPackageElementFlavor chapterFlavor;
176   private String thumbnailWfProperty;
177   private List<MediaPackageElementFlavor> thumbnailSourcePrimary;
178   private String distributionDirectory;
179   private Boolean localPublication = null;
180 
181   private static final String DEFAULT_PREVIEW_SUBTYPE = "source";
182   private static final String DEFAULT_PREVIEW_TAG = "editor";
183   private static final String DEFAULT_WAVEFORM_SUBTYPE = "waveform";
184   private static final String DEFAULT_SMIL_CATALOG_FLAVOR = "smil/cutting";
185   private static final String DEFAULT_SMIL_CATALOG_TAGS = "archive";
186   private static final String DEFAULT_SMIL_SILENCE_FLAVOR = "*/silence";
187   private static final String DEFAULT_PREVIEW_VIDEO_SUBTYPE = "video+preview";
188   private static final String DEFAULT_CAPTIONS_FLAVOR = "captions/*";
189   private static final String DEFAULT_CHAPTER_FLAVOR = "chapters/*";
190   private static final String DEFAULT_THUMBNAIL_SUBTYPE = "player+preview";
191   private static final String DEFAULT_THUMBNAIL_WF_PROPERTY = "thumbnail_edited";
192   private static final List<MediaPackageElementFlavor> DEFAULT_THUMBNAIL_PRIORITY_FLAVOR = new ArrayList<>();
193   private static final int DEFAULT_LOCK_TIMEOUT_SECONDS = 300; // ( 5 mins )
194   private static final int DEFAULT_LOCK_REFRESH_SECONDS = 60;  // ( 1 min )
195 
196   public static final String OPT_PREVIEW_SUBTYPE = "preview.subtype";
197   public static final String OPT_PREVIEW_TAG = "preview.tag";
198   public static final String OPT_WAVEFORM_SUBTYPE = "waveform.subtype";
199   public static final String OPT_SMIL_CATALOG_FLAVOR = "smil.catalog.flavor";
200   public static final String OPT_SMIL_CATALOG_TAGS = "smil.catalog.tags";
201   public static final String OPT_SMIL_SILENCE_FLAVOR = "smil.silence.flavor";
202   public static final String OPT_PREVIEW_VIDEO_SUBTYPE = "preview.video.subtype";
203   public static final String OPT_CAPTIONS_FLAVOR = "captions.flavor";
204   public static final String OPT_CHAPTER_FLAVOR = "chapter.flavor";
205   public static final String OPT_THUMBNAILSUBTYPE = "thumbnail.subtype";
206   public static final String OPT_THUMBNAIL_WF_PROPERTY = "thumbnail.workflow.property";
207   public static final String OPT_THUMBNAIL_PRIORITY_FLAVOR = "thumbnail.priority.flavor";
208   public static final String OPT_LOCAL_PUBLICATION = "publication.local";
209   public static final String OPT_LOCK_ENABLED = "lock.enable";
210   public static final String OPT_LOCK_TIMEOUT = "lock.release.after.seconds";
211   public static final String OPT_LOCK_REFRESH = "lock.refresh.after.seconds";
212 
213   private Boolean lockingActive;
214   private int lockRefresh = DEFAULT_LOCK_REFRESH_SECONDS;
215   private int lockTimeout = DEFAULT_LOCK_TIMEOUT_SECONDS;
216 
217   private final Set<String> smilCatalogTagSet = new HashSet<>();
218 
219   @Reference
220   void setSecurityService(SecurityService securityService) {
221     this.securityService = securityService;
222   }
223 
224   @Reference
225   void setSmilService(SmilService smilService) {
226     this.smilService = smilService;
227   }
228 
229   @Reference
230   void setWorkflowService(WorkflowService workflowService) {
231     this.workflowService = workflowService;
232   }
233 
234   @Reference
235   void setWorkspace(Workspace workspace) {
236     this.workspace = workspace;
237   }
238 
239   @Reference
240   void setUrlSigningService(UrlSigningService urlSigningService) {
241     this.urlSigningService = urlSigningService;
242   }
243 
244   @Reference
245   void setAssetManager(AssetManager assetManager) {
246     this.assetManager = assetManager;
247   }
248 
249   @Reference
250   public void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
251     this.searchIndex = elasticsearchIndex;
252   }
253 
254   @Reference
255   public void setIndexService(IndexService index) {
256     this.index = index;
257   }
258 
259   @Reference
260   public void setAuthorizationService(AuthorizationService authorizationService) {
261     this.authorizationService = authorizationService;
262   }
263 
264   public MediaPackageElementFlavor getSmilCatalogFlavor() {
265     return smilCatalogFlavor;
266   }
267 
268   public Set<String> getSmilCatalogTags() {
269     return smilCatalogTagSet;
270   }
271 
272   public String getPreviewVideoSubtype() {
273     return previewVideoSubtype;
274   }
275 
276   public MediaPackageElementFlavor getSmilSilenceFlavor() {
277     return smilSilenceFlavor;
278   }
279 
280   private String getPreviewSubtype() {
281     return previewSubtype;
282   }
283 
284   public String getPreviewTag() {
285     return previewTag;
286   }
287 
288   private String getWaveformSubtype() {
289     return waveformSubtype;
290   }
291 
292   private String getThumbnailSubtype() {
293     return thumbnailSubType;
294   }
295 
296   @Activate
297   @Modified
298   public void activate(ComponentContext cc) {
299     Dictionary<String, Object> properties = cc.getProperties();
300     if (properties == null) {
301       return;
302     }
303 
304     expireSeconds =  UrlSigningServiceOsgiUtil.getUpdatedSigningExpiration(properties, this.getClass().getSimpleName());
305     signWithClientIP = UrlSigningServiceOsgiUtil.getUpdatedSignWithClientIP(properties,this.getClass().getSimpleName());
306     // Preview tag
307     previewTag = Objects.toString(properties.get(OPT_PREVIEW_TAG), DEFAULT_PREVIEW_TAG);
308     logger.debug("Preview tag configuration set to '{}'", previewTag);
309 
310     // Preview subtype
311     previewSubtype = Objects.toString(properties.get(OPT_PREVIEW_SUBTYPE), DEFAULT_PREVIEW_SUBTYPE);
312     logger.debug("Preview subtype configuration set to '{}'", previewSubtype);
313 
314     // Waveform subtype
315     waveformSubtype = Objects.toString(properties.get(OPT_WAVEFORM_SUBTYPE), DEFAULT_WAVEFORM_SUBTYPE);
316     logger.debug("Waveform subtype configuration set to '{}'", waveformSubtype);
317 
318     // SMIL catalog flavor
319     smilCatalogFlavor = MediaPackageElementFlavor.parseFlavor(
320             StringUtils.defaultString((String) properties.get(OPT_SMIL_CATALOG_FLAVOR), DEFAULT_SMIL_CATALOG_FLAVOR));
321     logger.debug("Smil catalog flavor configuration set to '{}'", smilCatalogFlavor);
322 
323     // SMIL catalog tags
324     String tags =  Objects.toString(properties.get(OPT_SMIL_CATALOG_TAGS), DEFAULT_SMIL_CATALOG_TAGS);
325     String[] smilCatalogTags = StringUtils.split(tags, ",");
326     smilCatalogTagSet.clear();
327     if (smilCatalogTags != null) {
328       smilCatalogTagSet.addAll(Arrays.asList(smilCatalogTags));
329     }
330 
331     // SMIL silence flavor
332     smilSilenceFlavor = MediaPackageElementFlavor.parseFlavor(
333             StringUtils.defaultString((String) properties.get(OPT_SMIL_SILENCE_FLAVOR), DEFAULT_SMIL_SILENCE_FLAVOR));
334     logger.debug("Smil silence flavor configuration set to '{}'", smilSilenceFlavor);
335 
336     // Preview Video subtype
337     previewVideoSubtype =  Objects.toString(properties.get(OPT_PREVIEW_VIDEO_SUBTYPE), DEFAULT_PREVIEW_VIDEO_SUBTYPE);
338 
339     logger.debug("Preview video subtype set to '{}'", previewVideoSubtype);
340 
341     // Flavor for captions
342     captionsFlavor = MediaPackageElementFlavor.parseFlavor(
343             StringUtils.defaultString((String) properties.get(OPT_CAPTIONS_FLAVOR), DEFAULT_CAPTIONS_FLAVOR));
344     logger.debug("Caption flavor set to '{}'", captionsFlavor);
345 
346     // Flavor for chapters
347     chapterFlavor = MediaPackageElementFlavor.parseFlavor(
348         StringUtils.defaultString((String) properties.get(OPT_CHAPTER_FLAVOR), DEFAULT_CHAPTER_FLAVOR));
349     logger.debug("Chapter flavor set to '{}'", chapterFlavor);
350 
351     thumbnailSubType =  Objects.toString(properties.get(OPT_THUMBNAILSUBTYPE), DEFAULT_THUMBNAIL_SUBTYPE);
352     logger.debug("Thumbnail subtype set to '{}'", thumbnailSubType);
353 
354     thumbnailWfProperty = Objects.toString(properties.get(OPT_THUMBNAIL_WF_PROPERTY), DEFAULT_THUMBNAIL_WF_PROPERTY);
355     logger.debug("Thumbnail workflow property set to '{}'", thumbnailWfProperty);
356 
357     String thumbnailPriorities = Objects.toString(properties.get(OPT_THUMBNAIL_PRIORITY_FLAVOR));
358     if ("null".equals(thumbnailPriorities)  || thumbnailPriorities.isEmpty()) {
359       thumbnailSourcePrimary = DEFAULT_THUMBNAIL_PRIORITY_FLAVOR;
360     } else {
361       thumbnailSourcePrimary = Arrays.stream(thumbnailPriorities.split(",", -1))
362                                 .map(MediaPackageElementFlavor::parseFlavor)
363                                 .collect(Collectors.toList());
364     }
365 
366     String localPublicationConfig = Objects.toString(properties.get(OPT_LOCAL_PUBLICATION), "auto");
367     if (!"auto".equals(localPublicationConfig)) {
368       // If this is not set to `auto`, we expect this to be a boolean
369       localPublication = BooleanUtils.toBoolean(localPublicationConfig);
370     }
371 
372     distributionDirectory = cc.getBundleContext().getProperty("org.opencastproject.download.directory");
373     if (StringUtils.isEmpty(distributionDirectory)) {
374       final String storageDir = cc.getBundleContext().getProperty("org.opencastproject.storage.dir");
375       if (StringUtils.isNotEmpty(storageDir)) {
376         distributionDirectory = new File(storageDir, "downloads").getPath();
377       }
378     }
379     logger.debug("Thumbnail track priority set to '{}'", thumbnailSourcePrimary);
380 
381     lockingActive = Boolean.parseBoolean(StringUtils.trimToEmpty((String) properties.get(OPT_LOCK_ENABLED)));
382 
383     try {
384       lockTimeout = Integer.parseUnsignedInt(
385            Objects.toString(properties.get(OPT_LOCK_TIMEOUT)));
386     } catch (NumberFormatException e) {
387       logger.info("Configuration {} contains invalid value, defaulting to {}", OPT_LOCK_TIMEOUT, lockTimeout);
388     }
389 
390     try {
391       lockRefresh = Integer.parseUnsignedInt(
392             Objects.toString(properties.get(OPT_LOCK_REFRESH)));
393     } catch (NumberFormatException e) {
394       logger.info("Configuration {} contains invalid value, defaulting to {}", OPT_LOCK_REFRESH, lockRefresh);
395     }
396 
397     editorLock = new EditorLock(lockTimeout);
398 
399   }
400 
401   /**
402    * Check if a media URL can be served from this server.
403    *
404    * @param uri
405    *      URL locating a media file
406    * @return
407    *      If the file is available locally
408    */
409   private boolean isLocal(URI uri) {
410     var path = uri.normalize().getPath();
411     if (!path.startsWith("/static/")) {
412       return false;
413     }
414     var localFile = new File(distributionDirectory, path.substring("/static".length()));
415     return localFile.exists();
416   }
417 
418   private Boolean elementHasPreviewTag(MediaPackageElement element) {
419     return element.getTags() != null
420             && Arrays.asList(element.getTags()).contains(getPreviewTag());
421   }
422 
423   private Boolean elementHasPreviewFlavor(MediaPackageElement element) {
424     return element.getFlavor() != null
425             && getPreviewSubtype().equals(element.getFlavor().getSubtype());
426   }
427 
428   private Boolean elementHasWaveformFlavor(MediaPackageElement element) {
429     return element.getFlavor() != null
430             && getWaveformSubtype().equals(element.getFlavor().getSubtype());
431   }
432 
433   private String signIfNecessary(final URI uri) {
434     if (!urlSigningService.accepts(uri.toString())) {
435       return uri.toString();
436     }
437     String clientIP = signWithClientIP ? securityService.getUserIP() : null;
438     try {
439       return new URI(urlSigningService.sign(uri.toString(), expireSeconds, null, clientIP)).toString();
440     } catch (URISyntaxException | UrlSigningException e) {
441       throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
442     }
443   }
444 
445   /**
446    * Creates a SMIL cutting catalog based on the passed editing information and the media package.
447    *
448    * @param editingInfo
449    *          the editing information
450    * @param mediaPackage
451    *          the media package
452    * @return a SMIL catalog
453    * @throws SmilException
454    *           if creating the SMIL catalog failed
455    */
456   Smil createSmilCuttingCatalog(final EditingData editingInfo, final MediaPackage mediaPackage) throws SmilException {
457     // Create initial SMIL catalog
458     SmilResponse smilResponse = smilService.createNewSmil(mediaPackage);
459 
460     // Add tracks to the SMIL catalog
461     ArrayList<Track> tracks = new ArrayList<>();
462 
463     for (final TrackData trackdata : editingInfo.getTracks()) {
464       String trackId = trackdata.getId();
465       Track track = mediaPackage.getTrack(trackId);
466       if (track == null) {
467         track = Arrays.stream(getInternalPublication(mediaPackage)
468             .orElseThrow(() -> new IllegalStateException("Event has no internal publication"))
469             .getTracks())
470             .filter(t -> trackId.equals(t.getIdentifier()))
471             .findFirst()
472             .orElseThrow(() -> new IllegalStateException(
473                   String.format("The track '%s' doesn't exist in media package '%s'", trackId, mediaPackage)));
474       }
475       tracks.add(track);
476     }
477 
478     for (SegmentData segment : editingInfo.getSegments()) {
479       smilResponse = smilService.addParallel(smilResponse.getSmil());
480       final String parentId = smilResponse.getEntity().getId();
481 
482       final long duration = segment.getEnd() - segment.getStart();
483       if (!segment.isDeleted()) {
484         smilResponse = smilService.addClips(smilResponse.getSmil(), parentId, tracks.toArray(new Track[0]),
485                 segment.getStart(), duration);
486       }
487     }
488 
489     return smilResponse.getSmil();
490   }
491 
492   /**
493    * Adds the SMIL file as {@link Catalog} to the media package
494    * Does not send the updated media package to the archive.
495    *
496    * @param mediaPackage
497    *          the media package to at the SMIL catalog
498    * @param smil
499    *          the SMIL catalog
500    * @throws IOException
501    *           if the SMIL catalog cannot be read or not be written to the archive
502    */
503   MediaPackage addSmilToArchive(MediaPackage mediaPackage, final Smil smil) throws IOException {
504     MediaPackageElementFlavor mediaPackageElementFlavor = getSmilCatalogFlavor();
505     //set default catalog Id if there is none existing
506     String catalogId = smil.getId();
507     Catalog[] catalogs = mediaPackage.getCatalogs();
508 
509     //get the first smil/cutting  catalog-ID to overwrite it with new smil info
510     for (Catalog p: catalogs) {
511       if (p.getFlavor().matches(mediaPackageElementFlavor)) {
512         logger.debug("Set Identifier for Smil-Catalog to: {}", p.getIdentifier());
513         catalogId = p.getIdentifier();
514         break;
515       }
516     }
517     Catalog catalog = mediaPackage.getCatalog(catalogId);
518 
519     URI smilURI;
520     try (InputStream is = IOUtils.toInputStream(smil.toXML(), "UTF-8")) {
521       smilURI = workspace.put(mediaPackage.getIdentifier().toString(), catalogId, EditorService.TARGET_FILE_NAME, is);
522     } catch (SAXException | JAXBException e) {
523       throw new IOException("Error while serializing the SMIL catalog to XML" ,e);
524     }
525 
526     if (catalog == null) {
527       MediaPackageElementBuilder mpeBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
528       catalog = (Catalog) mpeBuilder.elementFromURI(smilURI, MediaPackageElement.Type.Catalog, getSmilCatalogFlavor());
529       mediaPackage.add(catalog);
530     }
531     catalog.setURI(smilURI);
532     catalog.setIdentifier(catalogId);
533     catalog.setMimeType(MimeTypes.XML);
534     for (String tag : getSmilCatalogTags()) {
535       catalog.addTag(tag);
536     }
537     // setting the URI to a new source so the checksum will most like be invalid
538     catalog.setChecksum(null);
539     return mediaPackage;
540   }
541 
542   /**
543    * Adds subtitles {@link EditingData.Subtitle} to the media package and sends the updated media package
544    * to the archive. If a subtitle flavor already exists, the subtitle is overwritten
545    *
546    * @param mediaPackage
547    *          the media package to at the SMIL catalog
548    * @param subtitles
549    *          the subtitles to be added
550    * @throws IOException
551    */
552   private MediaPackage processSubtitleTrack(
553       MediaPackage mediaPackage,
554       List<EditingData.Subtitle> subtitles,
555       MediaPackageElementFlavor newTrackFlavor,
556       String newFileName
557   ) throws IOException, IllegalArgumentException {
558     for (EditingData.Subtitle subtitle : subtitles) {
559       // Generate ID for new tracks
560       String subtitleId = UUID.randomUUID().toString();
561       String trackId = null;
562 
563       // Check if subtitle already exists
564       for (Track t : mediaPackage.getTracks()) {
565         if (t.getIdentifier().matches(subtitle.getId())) {
566           logger.debug("Set Identifier for {}-Track to: {}", newTrackFlavor.getType(), t.getIdentifier());
567           subtitleId = t.getIdentifier();
568           trackId = t.getIdentifier();
569           break;
570         }
571       }
572 
573       Track track = mediaPackage.getTrack(trackId);
574 
575       if (subtitle.isDeleted()) {
576         // If the subtitle is empty, remove the track
577         if (trackId != null) {
578           mediaPackage.remove(track);
579         }
580         continue;
581       }
582 
583       // Memorize uri of the previous track file for deletion
584       URI oldTrackURI = null;
585       if (track != null) {
586         oldTrackURI = track.getURI();
587       }
588 
589       // Put updated filename in working file repository and update the track.
590       try (InputStream is = IOUtils.toInputStream(subtitle.getSubtitle(), "UTF-8")) {
591         URI subtitleUri = workspace.put(mediaPackage.getIdentifier().toString(), subtitleId, newFileName + ".vtt", is);
592 
593         // If not exists, create new Track
594         if (track == null) {
595           // TODO: Figure out which flavor new subtitles from the editor should have
596           track = (Track) mediaPackage.add(subtitleUri, MediaPackageElement.Type.Track,
597               new MediaPackageElementFlavor(newTrackFlavor.getType(),"source"));
598           logger.info("Creating new {} track {}", newTrackFlavor.getType(), track.getIdentifier());
599         }
600 
601         track.setURI(subtitleUri);
602         track.setIdentifier(subtitleId);
603         track.setChecksum(null);
604         for (String tag : subtitle.getTags()) {
605           track.addTag(tag);
606         }
607 
608         if (oldTrackURI != null && oldTrackURI != subtitleUri) {
609           // Delete the old files from the working file repository and workspace if they were in there
610           logger.info("Removing old track file {}", oldTrackURI);
611           try {
612             workspace.delete(oldTrackURI);
613           } catch (NotFoundException | IOException e) {
614             logger.info("Could not remove track from workspace. Could be it was never there.");
615           }
616         }
617       }
618     }
619 
620     return mediaPackage;
621   }
622 
623   /**
624    * Adds base64 encoded thumbnail images to the mediapackage and takes a snapshot
625    *
626    * @param editingData
627    *          the editing information
628    * @param mediaPackage
629    *          the media package
630    * @throws MimeTypeParseException
631    * @throws IOException
632    */
633   private MediaPackage addThumbnailsToArchive(EditingData editingData, MediaPackage mediaPackage)
634           throws MimeTypeParseException, IOException {
635     for (TrackData track : editingData.getTracks()) {
636       String id = track.getId();
637       MediaPackageElementFlavor flavor = new MediaPackageElementFlavor(track.getFlavor().getType(),
638               getThumbnailSubtype());
639       String uri = track.getThumbnailURI();
640 
641       // If no uri, what do?
642       if (uri == null || uri.isEmpty()) {
643         continue;
644       }
645       // If uri not base64 encoded, what do?
646       if (!uri.startsWith("data")) {
647         continue;
648       }
649 
650       // Decode
651       uri = uri.substring(uri.indexOf(",") + 1);
652       byte[] byteArray = Base64.getMimeDecoder().decode(uri);
653       InputStream inputStream = new ByteArrayInputStream(byteArray);
654 
655       // Get MimeType
656       String stringMimeType = detectMimeType(uri);
657       MimeType mimeType = MimeType.mimeType(stringMimeType.split("/")[0], stringMimeType.split("/")[1]);
658 
659       // Store image in workspace
660       final String filename = "thumbnail_" + id + "." + mimeType.getSubtype();
661       final String originalThumbnailId = UUID.randomUUID().toString();
662       URI tempThumbnail = null;
663       try {
664         tempThumbnail = workspace
665                 .put(mediaPackage.getIdentifier().toString(), originalThumbnailId, filename, inputStream);
666       } catch (IOException e) {
667         throw new IOException("Could not add thumbnail to workspace", e);
668       }
669 
670       // Build thumbnail attachment
671       final Attachment attachment = AttachmentImpl.fromURI(tempThumbnail);
672       attachment.setFlavor(flavor);
673       attachment.setMimeType(mimeType);
674       Arrays.stream(mediaPackage.getElementsByFlavor(flavor))
675           .map(MediaPackageElement::getTags)
676           .flatMap(Arrays::stream)
677           .distinct()
678           .forEach(attachment::addTag);
679 
680       // Remove old thumbnails
681       Arrays.stream(mediaPackage.getElementsByFlavor(flavor)).forEach(mediaPackage::remove);
682 
683       // Add new thumbnail
684       mediaPackage.add(attachment);
685 
686       // Update publications here in the future?
687 
688       // Set workflow property
689       WorkflowPropertiesUtil
690               .storeProperty(assetManager, mediaPackage,
691                       flavor.getType() + "/" + thumbnailWfProperty, "true");
692     }
693 
694     return mediaPackage;
695   }
696 
697   /**
698    * Determines if mimetype of a base64 encoded string is one of the listed image mimetypes and returns it.
699    *
700    * @param b64
701    *          the encoded string that is supposed to be an image
702    * @return
703    *          the mimetype
704    * @throws MimeTypeParseException
705    */
706   private String detectMimeType(String b64) throws MimeTypeParseException {
707     var signatures = new HashMap<String, String>();
708     signatures.put("R0lGODdh", "image/gif");
709     signatures.put("iVBORw0KGgo", "image/png");
710     signatures.put("/9j/", "image/jpg");
711 
712     for (var s : signatures.entrySet()) {
713       if (b64.indexOf(s.getKey()) == 0) {
714         return s.getValue();
715       }
716     }
717     throw new MimeTypeParseException("No image mimetype found");
718   }
719 
720   private Optional<Publication> getInternalPublication(MediaPackage mp) {
721     return Arrays.stream(mp.getPublications())
722         .filter(publication -> InternalPublicationChannel.CHANNEL_ID.equals(publication.getChannel()))
723         .findFirst();
724   }
725 
726   /**
727    * Get an {@link Event}
728    *
729    * @param mediaPackageId
730    *          The mediapackage id that is also the event id.
731    * @return The event if available or none if it is missing.
732    */
733   private Event getEvent(final String mediaPackageId) throws EditorServiceException {
734     try {
735       Optional<Event> optEvent = index.getEvent(mediaPackageId, searchIndex);
736       if (optEvent.isEmpty()) {
737         errorExit("Event not found", mediaPackageId,
738                 ErrorStatus.MEDIAPACKAGE_NOT_FOUND);
739       } else {
740         return optEvent.get();
741       }
742     } catch (SearchIndexException e) {
743       errorExit("Error while reading event from search index:", mediaPackageId,
744               ErrorStatus.MEDIAPACKAGE_NOT_FOUND, e);
745     }
746     return null;
747   }
748 
749   /**
750    * Returns a list of workflow definitions that may be applied to a media package after segments have been defined with
751    * the editor tool.
752    *
753    * @return a list of workflow definitions
754    */
755   private List<WorkflowDefinition> getEditingWorkflows() {
756     try {
757       return workflowService.listAvailableWorkflowDefinitions().stream()
758           .filter(workflow -> workflow.containsTag(EDITOR_WORKFLOW_TAG))
759           .collect(Collectors.toList());
760     } catch (WorkflowDatabaseException e) {
761       logger.warn("Error while retrieving list of workflow definitions:", e);
762     }
763     return emptyList();
764   }
765 
766   /**
767    * Analyzes the media package and tries to get information about segments out of it.
768    *
769    * @param mediaPackage
770    *          the media package
771    * @return a list of segments or an empty list if no segments could be found.
772    */
773   private List<SegmentData> getSegments(final MediaPackage mediaPackage) {
774     List<SegmentData> segments = new ArrayList<>();
775     for (Catalog smilCatalog : mediaPackage.getCatalogs(getSmilCatalogFlavor())) {
776       try {
777         Smil smil = smilService.fromXml(workspace.get(smilCatalog.getURI())).getSmil();
778         segments = mergeSegments(segments, getSegmentsFromSmil(smil));
779 
780       } catch (NotFoundException e) {
781         logger.warn("File '{}' could not be loaded by workspace service:", smilCatalog.getURI(), e);
782       } catch (IOException e) {
783         logger.warn("Reading file '{}' from workspace service failed:", smilCatalog.getURI(), e);
784       } catch (SmilException e) {
785         logger.warn("Error while parsing SMIL catalog '{}':", smilCatalog.getURI(), e);
786       }
787     }
788 
789     if (!segments.isEmpty()) {
790       return segments;
791     }
792 
793     // Read from silence detection flavors
794     for (Catalog smilCatalog : mediaPackage.getCatalogs(getSmilSilenceFlavor())) {
795       try {
796         Smil smil = smilService.fromXml(workspace.get(smilCatalog.getURI())).getSmil();
797         segments = getSegmentsFromSmil(smil);
798       } catch (NotFoundException e) {
799         logger.warn("File '{}' could not be loaded by workspace service:", smilCatalog.getURI(), e);
800       } catch (IOException e) {
801         logger.warn("Reading file '{}' from workspace service failed:", smilCatalog.getURI(), e);
802       } catch (SmilException e) {
803         logger.warn("Error while parsing SMIL catalog '{}':", smilCatalog.getURI(), e);
804       }
805     }
806 
807     // Check for single segment to ignore
808     if (segments.size() == 1) {
809       SegmentData singleSegment = segments.get(0);
810       if (singleSegment.getStart() == 0 && singleSegment.getEnd() >= mediaPackage.getDuration()) {
811         segments.remove(0);
812       }
813     }
814 
815     return segments;
816   }
817 
818   protected List<SegmentData> getDeletedSegments(MediaPackage mediaPackage, List<SegmentData> segments) {
819     // add deletedElements
820     long lastTime = 0;
821     List<SegmentData> deletedElements = new ArrayList<>();
822     for (int i = 0; i < segments.size(); i++) {
823       SegmentData segmentData = segments.get(i);
824       if (segmentData.getStart() != lastTime) {
825         SegmentData deleted = new SegmentData(lastTime, segmentData.getStart(), true);
826         deletedElements.add(deleted);
827       }
828       lastTime = segmentData.getEnd();
829       // check for last segment
830       if (segments.size() - 1 == i) {
831         if (mediaPackage.getDuration() != null && lastTime < mediaPackage.getDuration()) {
832           deletedElements.add(new SegmentData(lastTime, mediaPackage.getDuration(), true));
833         }
834       }
835     }
836     return deletedElements;
837   }
838 
839   protected List<SegmentData> mergeSegments(List<SegmentData> segments, List<SegmentData> segments2) {
840     // Merge conflicting segments
841     List<SegmentData> mergedSegments = mergeInternal(segments, segments2);
842 
843     // Sort segments
844     sortSegments(mergedSegments);
845 
846     return mergedSegments;
847   }
848 
849   private void sortSegments(List<SegmentData> mergedSegments) {
850     mergedSegments.sort(Comparator.comparing(SegmentData::getStart));
851   }
852 
853   /**
854    * Merges two different segments lists together. Keeps untouched segments and combines touching segments by the
855    * overlapping points.
856    *
857    * @param segments
858    *          the first segments to be merge
859    * @param segments2
860    *          the second segments to be merge
861    * @return the merged segments
862    */
863   private List<SegmentData> mergeInternal(List<SegmentData> segments, List<SegmentData> segments2) {
864     for (Iterator<SegmentData> it = segments.iterator(); it.hasNext();) {
865       SegmentData seg = it.next();
866       for (Iterator<SegmentData> it2 = segments2.iterator(); it2.hasNext();) {
867         SegmentData seg2 = it2.next();
868         long combinedStart = Math.max(seg.getStart(), seg2.getStart());
869         long combinedEnd = Math.min(seg.getEnd(), seg2.getEnd());
870         if (combinedEnd > combinedStart) {
871           it.remove();
872           it2.remove();
873           List<SegmentData> newSegments = new ArrayList<>(segments);
874           newSegments.add(new SegmentData(combinedStart, combinedEnd));
875           return mergeInternal(newSegments, segments2);
876         }
877       }
878     }
879     segments.addAll(segments2);
880     return segments;
881   }
882 
883   /**
884    * Extracts the segments of a SMIL catalog and returns them as a list of tuples (start, end).
885    *
886    * @param smil
887    *          the SMIL catalog
888    * @return the list of segments
889    */
890   List<SegmentData> getSegmentsFromSmil(Smil smil) {
891     List<SegmentData> segments = new ArrayList<>();
892     for (SmilMediaObject elem : smil.getBody().getMediaElements()) {
893       if (elem instanceof SmilMediaContainer) {
894         SmilMediaContainer mediaContainer = (SmilMediaContainer) elem;
895 
896         SegmentData tuple = null;
897         for (SmilMediaObject video : mediaContainer.getElements()) {
898           if (video instanceof SmilMediaElement) {
899             SmilMediaElement videoElem = (SmilMediaElement) video;
900             try {
901               // pick longest element
902               if (tuple == null || (videoElem.getClipEndMS()
903                       - videoElem.getClipBeginMS()) > tuple.getEnd() - tuple.getStart()) {
904                 tuple = new SegmentData(videoElem.getClipBeginMS(), videoElem.getClipEndMS());
905               }
906             } catch (SmilException e) {
907               logger.warn("Media element '{}' of SMIL catalog '{}' seems to be invalid",
908                       videoElem, smil, e);
909             }
910           }
911         }
912         if (tuple != null) {
913           segments.add(tuple);
914         }
915       }
916     }
917     return segments;
918   }
919 
920   @Override
921   public void lockMediaPackage(final String mediaPackageId, LockData lockRequest) throws EditorServiceException {
922     // Does mediaPackage exist
923     getEvent(mediaPackageId);
924 
925     // Try to get lock, throws Exception if not owner
926     editorLock.lock(mediaPackageId, lockRequest);
927   }
928 
929   @Override
930   public void unlockMediaPackage(final String mediaPackageId, LockData lockRequest) throws EditorServiceException {
931     // Does mediaPackage exist
932     getEvent(mediaPackageId);
933 
934     // Try to release lock, throws Exception if not owner
935     editorLock.unlock(mediaPackageId, lockRequest);
936   }
937 
938   @Override
939   public EditingData getEditData(final String mediaPackageId) throws EditorServiceException, UnauthorizedException {
940 
941     Event event = getEvent(mediaPackageId);
942     MediaPackage mp = getMediaPackage(event);
943 
944     if (!isAdmin() && !authorizationService.hasPermission(mp, "write")) {
945       throw new UnauthorizedException("User has no write access to this event");
946     }
947 
948     boolean workflowActive = WorkflowUtil.isActive(event.getWorkflowState());
949 
950     final Optional<Publication> internalPubOpt = getInternalPublication(mp);
951     if (internalPubOpt.isEmpty()) {
952       errorExit("No internal publication", mediaPackageId, ErrorStatus.NO_INTERNAL_PUBLICATION);
953     }
954     Publication internalPub = internalPubOpt.get();
955 
956     // Get existing segments
957     List<SegmentData> segments = getSegments(mp);
958     segments.addAll(getDeletedSegments(mp, segments));
959     sortSegments(segments);
960 
961 
962     // Get workflows
963     List<WorkflowData> workflows = new ArrayList<>();
964     for (WorkflowDefinition workflow : getEditingWorkflows()) {
965       workflows.add(new WorkflowData(workflow.getId(), workflow.getTitle(), workflow.getDisplayOrder(),
966               workflow.getDescription()));
967     }
968 
969     final Map<String, String> latestWfProperties = WorkflowPropertiesUtil
970             .getLatestWorkflowProperties(assetManager, mediaPackageId);
971     // The properties have the format "hide_flavor_audio" or "hide_flavor_video", where flavor is preconfigured.
972     // We filter all the properties that have this format, and then those which have values "true".
973     final Collection<Tuple<String, String>> hiddens = latestWfProperties.entrySet()
974             .stream()
975             .map(property -> tuple(property.getKey().split("_"), property.getValue()))
976             .filter(property -> property.getA().length == 3)
977             .filter(property -> property.getA()[0].equals("hide"))
978             .filter(property -> property.getB().equals("true"))
979             .map(property -> tuple(property.getA()[1], property.getA()[2]))
980             .collect(Collectors.toSet());
981 
982     List<Track> trackList = Arrays.stream(internalPub.getTracks()).filter(this::elementHasPreviewTag)
983             .collect(Collectors.toList());
984     if (trackList.isEmpty()) {
985       trackList = Arrays.stream(internalPub.getTracks()).filter(this::elementHasPreviewFlavor)
986               .collect(Collectors.toList());
987       if (trackList.isEmpty()) {
988         trackList = Arrays.asList(internalPub.getTracks());
989       }
990     }
991 
992     // Get subtitles from the asset manager, so they are guaranteed to be up-to-date after saving
993     Track[] subtitleTracks = mp.getTracks(captionsFlavor);
994     List<EditingData.Subtitle> subtitles = new ArrayList<>();
995     for (Track t: subtitleTracks) {
996       try {
997         File subtitleFile = workspace.get(t.getURI());
998         String subtitleString = FileUtils.readFileToString(subtitleFile, StandardCharsets.UTF_8);
999         subtitles.add(new EditingData.Subtitle(t.getIdentifier(), subtitleString, t.getTags()));
1000       } catch (NotFoundException | IOException e) {
1001         errorExit("Could not read subtitle from file", mediaPackageId, ErrorStatus.UNKNOWN);
1002       }
1003     }
1004 
1005     // Get chapters too
1006     Track[] chapterTracks = mp.getTracks(chapterFlavor);
1007     List<EditingData.Subtitle> chapters = new ArrayList<>();
1008     for (Track t: chapterTracks) {
1009       try {
1010         File chapterFile = workspace.get(t.getURI());
1011         String chapterString = FileUtils.readFileToString(chapterFile, StandardCharsets.UTF_8);
1012         chapters.add(new EditingData.Subtitle(t.getIdentifier(), chapterString, t.getTags()));
1013       } catch (NotFoundException | IOException e) {
1014         errorExit("Could not read chapter from file", mediaPackageId, ErrorStatus.UNKNOWN);
1015       }
1016     }
1017 
1018     // Get tracks from the internal publication because it is a lot faster than getting them from the asset manager
1019     // for some reason.
1020     final List<TrackData> tracks = trackList.stream().map(track -> {
1021       final String uri = signIfNecessary(track.getURI());
1022       final boolean audioEnabled = !hiddens.contains(tuple(track.getFlavor().getType(), "audio"));
1023       final TrackSubData audio = new TrackSubData(track.hasAudio(), null,
1024                         audioEnabled);
1025       final boolean videoEnable = !hiddens.contains(tuple(track.getFlavor().getType(), "video"));
1026       final String videoPreview = Arrays.stream(internalPub.getAttachments())
1027                         .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1028                         .filter(attachment -> attachment.getFlavor().getSubtype().equals(getPreviewVideoSubtype()))
1029                         .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1030                         .findAny()
1031                         .orElse(null);
1032       final TrackSubData video = new TrackSubData(track.hasVideo(), videoPreview,
1033                         videoEnable);
1034 
1035       // Get thumbnail from archive
1036       // If a thumbnail got generated in the frontend, it will be saved to the archive. So if no workflow runs,
1037       // the saved, thumbnail will not show up in the frontend if we get it from the internal publication
1038       String thumbnailURI = Arrays.stream(mp.getAttachments())
1039           .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1040           .filter(attachment -> attachment.getFlavor().getSubtype().equals(getThumbnailSubtype()))
1041           .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1042           .findAny()
1043           .orElse(null);
1044 
1045       // If thumbnail is not in archive, try getting it from the internal publication
1046       // Because our default workflows don't save thumbnails in the archive but only publish them.
1047       if (thumbnailURI == null) {
1048         thumbnailURI = Arrays.stream(internalPub.getAttachments())
1049             .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1050             .filter(attachment -> attachment.getFlavor().getSubtype().equals(getThumbnailSubtype()))
1051             .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1052             .findAny()
1053             .orElse(null);
1054       }
1055 
1056       final int priority = thumbnailSourcePrimary.indexOf(track.getFlavor());
1057 
1058       if (localPublication == null) {
1059         localPublication = isLocal(track.getURI());
1060       }
1061 
1062       return new TrackData(track.getFlavor().getType(), track.getFlavor().getSubtype(), audio, video, uri,
1063           track.getIdentifier(), thumbnailURI, priority);
1064     }).collect(Collectors.toList());
1065 
1066     List<String> waveformList = Arrays.stream(internalPub.getAttachments())
1067             .filter(this::elementHasWaveformFlavor)
1068             .map(Attachment::getURI).map(this::signIfNecessary)
1069             .collect(Collectors.toList());
1070 
1071     User user = securityService.getUser();
1072 
1073     return new EditingData(segments, tracks, workflows, mp.getDuration(), mp.getTitle(), event.getRecordingStartDate(),
1074             event.getSeriesId(), event.getSeriesName(), workflowActive, waveformList, subtitles, chapters,
1075             localPublication, lockingActive, lockRefresh, user, "");
1076   }
1077 
1078 
1079   private boolean isAdmin() {
1080     final User currentUser = securityService.getUser();
1081 
1082     // Global admin
1083     if (currentUser.hasRole(SecurityConstants.GLOBAL_ADMIN_ROLE)) {
1084       return true;
1085     }
1086 
1087     // Organization admin
1088     final Organization currentOrg = securityService.getOrganization();
1089     return currentUser.getOrganization().getId().equals(currentOrg.getId())
1090             && currentUser.hasRole(currentOrg.getAdminRole());
1091   }
1092 
1093   private MediaPackage getMediaPackage(Event event) throws EditorServiceException {
1094     if (event == null) {
1095       errorExit("No Event provided", "", ErrorStatus.UNKNOWN);
1096       return null;
1097     }
1098     try {
1099       return index.getEventMediapackage(event);
1100     } catch (IndexServiceException e) {
1101       errorExit("Not Found", event.getIdentifier(), ErrorStatus.MEDIAPACKAGE_NOT_FOUND);
1102       return null;
1103     }
1104   }
1105 
1106   private void errorExit(final String message, final String mediaPackageId, ErrorStatus status)
1107           throws EditorServiceException {
1108     errorExit(message, mediaPackageId, status, null);
1109   }
1110 
1111   private void errorExit(final String message, final String mediaPackageId, ErrorStatus status, Exception e)
1112           throws EditorServiceException {
1113     String errorMessage = MessageFormat.format("{0}. Event ID: {1}", message, mediaPackageId);
1114     throw new EditorServiceException(errorMessage, status, e);
1115   }
1116 
1117   @Override
1118   public void setEditData(String mediaPackageId, EditingData editingData) throws EditorServiceException,
1119           IOException {
1120     final Event event = getEvent(mediaPackageId);
1121 
1122     if (WorkflowUtil.isActive(event.getWorkflowState())) {
1123       errorExit("Workflow is running", mediaPackageId, ErrorStatus.WORKFLOW_ACTIVE);
1124     }
1125 
1126     MediaPackage mediaPackage = getMediaPackage(event);
1127     Smil smil = null;
1128     try {
1129       smil = createSmilCuttingCatalog(editingData, mediaPackage);
1130     } catch (Exception e) {
1131       errorExit("Unable to create SMIL cutting catalog", mediaPackageId, ErrorStatus.UNABLE_TO_CREATE_CATALOG, e);
1132     }
1133 
1134     final Map<String, String> workflowProperties = new HashMap<String, String>();
1135     for (TrackData track : editingData.getTracks()) {
1136       MediaPackageElementFlavor flavor = track.getFlavor();
1137       String type = null;
1138       if (flavor != null) {
1139         type = flavor.getType();
1140       } else {
1141         Track mpTrack = mediaPackage.getTrack(track.getId());
1142         if (mpTrack != null) {
1143           type = mpTrack.getFlavor().getType();
1144         } else {
1145           errorExit("Unable to determine track type", mediaPackageId, ErrorStatus.UNKNOWN);
1146         }
1147       }
1148       workflowProperties.put("hide_" + type + "_audio", Boolean.toString(!track.getAudio().isEnabled()));
1149       workflowProperties.put("hide_" + type + "_video", Boolean.toString(!track.getVideo().isEnabled()));
1150     }
1151     WorkflowPropertiesUtil.storeProperties(assetManager, mediaPackage, workflowProperties);
1152 
1153     try {
1154       mediaPackage = addSmilToArchive(mediaPackage, smil);
1155     } catch (IOException e) {
1156       errorExit("Unable to add SMIL cutting catalog to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1157     }
1158 
1159     try {
1160       if (editingData.getSubtitles() != null) {
1161         mediaPackage = processSubtitleTrack(mediaPackage, editingData.getSubtitles(), captionsFlavor, "subtitle");
1162       }
1163     } catch (IOException e) {
1164       errorExit("Unable to add subtitle track to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1165     } catch (IllegalArgumentException e) {
1166       errorExit("Illegal subtitle given", mediaPackageId, ErrorStatus.UNKNOWN, e);
1167     }
1168 
1169     try {
1170       if (editingData.getChapters() != null) {
1171         mediaPackage = processSubtitleTrack(mediaPackage, editingData.getChapters(), chapterFlavor, "chapters");
1172       }
1173     } catch (IOException e) {
1174       errorExit("Unable to add subtitle track to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1175     } catch (IllegalArgumentException e) {
1176       errorExit("Illegal subtitle given", mediaPackageId, ErrorStatus.UNKNOWN, e);
1177     }
1178 
1179     try {
1180       mediaPackage = addThumbnailsToArchive(editingData, mediaPackage);
1181     } catch (MimeTypeParseException e) {
1182       errorExit("Thumbnail had an illegal MimeType", mediaPackageId, ErrorStatus.UNKNOWN, e);
1183     } catch (IOException e) {
1184       errorExit("Unable to add thumbnail to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1185     }
1186 
1187     try {
1188       assetManager.takeSnapshot(mediaPackage);
1189     } catch (AssetManagerException e) {
1190       logger.error("Error while adding the updated media package ({}) to the archive",
1191               mediaPackage.getIdentifier(), e);
1192       throw new IOException(e);
1193     }
1194 
1195     // Update Metadata
1196     try {
1197       index.updateAllEventMetadata(mediaPackageId, editingData.getMetadataJSON(), searchIndex);
1198     } catch (SearchIndexException | IndexServiceException | IllegalArgumentException e) {
1199       errorExit("Event metadata can't be updated.", mediaPackageId, ErrorStatus.METADATA_UPDATE_FAIL, e);
1200     } catch (NotFoundException e) {
1201       errorExit("Event not found.", mediaPackageId, ErrorStatus.MEDIAPACKAGE_NOT_FOUND, e);
1202     } catch (UnauthorizedException e) {
1203       errorExit("Not authorized to update event metadata .", mediaPackageId, ErrorStatus.NOT_AUTHORIZED, e);
1204     }
1205 
1206     if (editingData.getPostProcessingWorkflow() != null) {
1207       final String workflowId = editingData.getPostProcessingWorkflow();
1208       try {
1209         final Map<String, String> workflowParameters = WorkflowPropertiesUtil
1210                 .getLatestWorkflowProperties(assetManager, mediaPackage.getIdentifier().toString());
1211         final Workflows workflows = new Workflows(assetManager, workflowService);
1212         workflows.applyWorkflowToLatestVersion(Collections.singletonList(mediaPackage.getIdentifier().toString()),
1213                 ConfiguredWorkflow.workflow(workflowService.getWorkflowDefinitionById(workflowId), workflowParameters));
1214       } catch (AssetManagerException e) {
1215         errorExit("Unable to start workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_ERROR, e);
1216       } catch (WorkflowDatabaseException e) {
1217         errorExit("Unable to load workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_ERROR, e);
1218       } catch (NotFoundException e) {
1219         errorExit("Unable to load workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_NOT_FOUND, e);
1220       }
1221     }
1222   }
1223 
1224   @Override
1225   public String getMetadata(String mediaPackageId) throws EditorServiceException {
1226     final Event event = getEvent(mediaPackageId);
1227     MediaPackage mediaPackage = getMediaPackage(event);
1228     MetadataList metadataList = new MetadataList();
1229     List<EventCatalogUIAdapter> catalogUIAdapters = index.getEventCatalogUIAdapters();
1230     catalogUIAdapters.remove(index.getCommonEventCatalogUIAdapter());
1231     for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1232       metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(mediaPackage));
1233     }
1234 
1235     DublinCoreMetadataCollection metadataCollection = null;
1236     try {
1237       metadataCollection = EventUtils.getEventMetadata(event,
1238               index.getCommonEventCatalogUIAdapter());
1239     } catch (Exception e) {
1240       errorExit("Unable to retrieve event metadata", mediaPackageId, ErrorStatus.UNKNOWN);
1241     }
1242     metadataList.add(index.getCommonEventCatalogUIAdapter(), metadataCollection);
1243 
1244     final String wfState = event.getWorkflowState();
1245     if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
1246       metadataList.setLocked(MetadataList.Locked.WORKFLOW_RUNNING);
1247     }
1248 
1249     return MetadataJson.listToJson(metadataList, true).toString();
1250   }
1251 }