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