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