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