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