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