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.adminui.endpoint;
23  
24  import static java.lang.String.format;
25  import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
26  import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
27  import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
28  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
29  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
30  import static javax.servlet.http.HttpServletResponse.SC_OK;
31  import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
32  import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
33  import static javax.ws.rs.core.Response.Status.NOT_FOUND;
34  import static org.apache.commons.lang3.StringUtils.trimToNull;
35  import static org.opencastproject.adminui.endpoint.EndpointUtil.transformAccessControList;
36  import static org.opencastproject.index.service.impl.util.EventUtils.internalChannelFilter;
37  import static org.opencastproject.index.service.util.JSONUtils.arrayToJsonArray;
38  import static org.opencastproject.index.service.util.JSONUtils.collectionToJsonArray;
39  import static org.opencastproject.index.service.util.JSONUtils.mapToJsonObject;
40  import static org.opencastproject.index.service.util.JSONUtils.safeString;
41  import static org.opencastproject.index.service.util.RestUtils.conflictJson;
42  import static org.opencastproject.index.service.util.RestUtils.notFound;
43  import static org.opencastproject.index.service.util.RestUtils.notFoundJson;
44  import static org.opencastproject.index.service.util.RestUtils.okJson;
45  import static org.opencastproject.index.service.util.RestUtils.okJsonList;
46  import static org.opencastproject.index.service.util.RestUtils.serverErrorJson;
47  import static org.opencastproject.util.DateTimeSupport.toUTC;
48  import static org.opencastproject.util.RestUtil.R.badRequest;
49  import static org.opencastproject.util.RestUtil.R.conflict;
50  import static org.opencastproject.util.RestUtil.R.forbidden;
51  import static org.opencastproject.util.RestUtil.R.noContent;
52  import static org.opencastproject.util.RestUtil.R.notFound;
53  import static org.opencastproject.util.RestUtil.R.ok;
54  import static org.opencastproject.util.RestUtil.R.serverError;
55  import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
56  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
57  import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
58  
59  import org.opencastproject.adminui.exception.JobEndpointException;
60  import org.opencastproject.adminui.impl.AdminUIConfiguration;
61  import org.opencastproject.adminui.tobira.TobiraException;
62  import org.opencastproject.adminui.tobira.TobiraService;
63  import org.opencastproject.adminui.util.BulkUpdateUtil;
64  import org.opencastproject.assetmanager.api.AssetManager;
65  import org.opencastproject.authorization.xacml.manager.api.AclService;
66  import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
67  import org.opencastproject.authorization.xacml.manager.util.AccessInformationUtil;
68  import org.opencastproject.capture.CaptureParameters;
69  import org.opencastproject.capture.admin.api.Agent;
70  import org.opencastproject.capture.admin.api.CaptureAgentStateService;
71  import org.opencastproject.elasticsearch.api.SearchIndexException;
72  import org.opencastproject.elasticsearch.api.SearchResult;
73  import org.opencastproject.elasticsearch.api.SearchResultItem;
74  import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
75  import org.opencastproject.elasticsearch.index.objects.event.Event;
76  import org.opencastproject.elasticsearch.index.objects.event.EventIndexSchema;
77  import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
78  import org.opencastproject.event.comment.EventComment;
79  import org.opencastproject.event.comment.EventCommentException;
80  import org.opencastproject.event.comment.EventCommentReply;
81  import org.opencastproject.event.comment.EventCommentService;
82  import org.opencastproject.index.service.api.IndexService;
83  import org.opencastproject.index.service.api.IndexService.Source;
84  import org.opencastproject.index.service.exception.IndexServiceException;
85  import org.opencastproject.index.service.exception.UnsupportedAssetException;
86  import org.opencastproject.index.service.impl.util.EventUtils;
87  import org.opencastproject.index.service.resources.list.provider.EventsListProvider.Comments;
88  import org.opencastproject.index.service.resources.list.provider.EventsListProvider.IsPublished;
89  import org.opencastproject.index.service.resources.list.query.EventListQuery;
90  import org.opencastproject.index.service.resources.list.query.SeriesListQuery;
91  import org.opencastproject.index.service.util.JSONUtils;
92  import org.opencastproject.index.service.util.RestUtils;
93  import org.opencastproject.list.api.ResourceListQuery;
94  import org.opencastproject.mediapackage.Attachment;
95  import org.opencastproject.mediapackage.AudioStream;
96  import org.opencastproject.mediapackage.Catalog;
97  import org.opencastproject.mediapackage.MediaPackage;
98  import org.opencastproject.mediapackage.MediaPackageElement;
99  import org.opencastproject.mediapackage.MediaPackageException;
100 import org.opencastproject.mediapackage.Publication;
101 import org.opencastproject.mediapackage.Track;
102 import org.opencastproject.mediapackage.VideoStream;
103 import org.opencastproject.mediapackage.track.AudioStreamImpl;
104 import org.opencastproject.mediapackage.track.SubtitleStreamImpl;
105 import org.opencastproject.mediapackage.track.VideoStreamImpl;
106 import org.opencastproject.metadata.dublincore.DublinCore;
107 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
108 import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
109 import org.opencastproject.metadata.dublincore.MetadataField;
110 import org.opencastproject.metadata.dublincore.MetadataJson;
111 import org.opencastproject.metadata.dublincore.MetadataList;
112 import org.opencastproject.metadata.dublincore.MetadataList.Locked;
113 import org.opencastproject.rest.BulkOperationResult;
114 import org.opencastproject.rest.RestConstants;
115 import org.opencastproject.scheduler.api.Recording;
116 import org.opencastproject.scheduler.api.SchedulerException;
117 import org.opencastproject.scheduler.api.SchedulerService;
118 import org.opencastproject.scheduler.api.TechnicalMetadata;
119 import org.opencastproject.scheduler.api.Util;
120 import org.opencastproject.security.api.AccessControlList;
121 import org.opencastproject.security.api.AccessControlParser;
122 import org.opencastproject.security.api.AclScope;
123 import org.opencastproject.security.api.AuthorizationService;
124 import org.opencastproject.security.api.Organization;
125 import org.opencastproject.security.api.Permissions;
126 import org.opencastproject.security.api.SecurityService;
127 import org.opencastproject.security.api.UnauthorizedException;
128 import org.opencastproject.security.api.User;
129 import org.opencastproject.security.api.UserDirectoryService;
130 import org.opencastproject.security.urlsigning.exception.UrlSigningException;
131 import org.opencastproject.security.urlsigning.service.UrlSigningService;
132 import org.opencastproject.security.util.SecurityUtil;
133 import org.opencastproject.systems.OpencastConstants;
134 import org.opencastproject.util.DateTimeSupport;
135 import org.opencastproject.util.Jsons.Val;
136 import org.opencastproject.util.NotFoundException;
137 import org.opencastproject.util.RestUtil;
138 import org.opencastproject.util.UrlSupport;
139 import org.opencastproject.util.data.Tuple;
140 import org.opencastproject.util.data.Tuple3;
141 import org.opencastproject.util.doc.rest.RestParameter;
142 import org.opencastproject.util.doc.rest.RestQuery;
143 import org.opencastproject.util.doc.rest.RestResponse;
144 import org.opencastproject.util.requests.SortCriterion;
145 import org.opencastproject.workflow.api.RetryStrategy;
146 import org.opencastproject.workflow.api.WorkflowDatabaseException;
147 import org.opencastproject.workflow.api.WorkflowDefinition;
148 import org.opencastproject.workflow.api.WorkflowInstance;
149 import org.opencastproject.workflow.api.WorkflowOperationInstance;
150 import org.opencastproject.workflow.api.WorkflowService;
151 import org.opencastproject.workflow.api.WorkflowStateException;
152 import org.opencastproject.workflow.api.WorkflowUtil;
153 
154 import com.google.gson.JsonArray;
155 import com.google.gson.JsonNull;
156 import com.google.gson.JsonObject;
157 import com.google.gson.JsonPrimitive;
158 
159 import net.fortuna.ical4j.model.property.RRule;
160 
161 import org.apache.commons.lang3.BooleanUtils;
162 import org.apache.commons.lang3.StringUtils;
163 import org.codehaus.jettison.json.JSONException;
164 import org.json.simple.JSONArray;
165 import org.json.simple.JSONObject;
166 import org.json.simple.parser.JSONParser;
167 import org.osgi.service.component.ComponentContext;
168 import org.osgi.service.component.annotations.Activate;
169 import org.slf4j.Logger;
170 import org.slf4j.LoggerFactory;
171 
172 import java.net.URI;
173 import java.text.ParseException;
174 import java.time.Instant;
175 import java.util.ArrayList;
176 import java.util.Collections;
177 import java.util.Date;
178 import java.util.HashMap;
179 import java.util.HashSet;
180 import java.util.List;
181 import java.util.Map;
182 import java.util.Map.Entry;
183 import java.util.Objects;
184 import java.util.Optional;
185 import java.util.Set;
186 import java.util.TimeZone;
187 import java.util.function.Function;
188 import java.util.stream.Collectors;
189 
190 import javax.servlet.http.HttpServletRequest;
191 import javax.servlet.http.HttpServletResponse;
192 import javax.ws.rs.Consumes;
193 import javax.ws.rs.DELETE;
194 import javax.ws.rs.FormParam;
195 import javax.ws.rs.GET;
196 import javax.ws.rs.POST;
197 import javax.ws.rs.PUT;
198 import javax.ws.rs.Path;
199 import javax.ws.rs.PathParam;
200 import javax.ws.rs.Produces;
201 import javax.ws.rs.QueryParam;
202 import javax.ws.rs.WebApplicationException;
203 import javax.ws.rs.core.Context;
204 import javax.ws.rs.core.MediaType;
205 import javax.ws.rs.core.Response;
206 import javax.ws.rs.core.Response.Status;
207 
208 /**
209  * The event endpoint acts as a facade for WorkflowService and Archive providing a unified query interface and result
210  * set.
211  * <p>
212  * This first implementation uses the {@link org.opencastproject.assetmanager.api.AssetManager}. In a later iteration
213  * the endpoint may abstract over the concrete archive.
214  */
215 @Path("/admin-ng/event")
216 public abstract class AbstractEventEndpoint {
217 
218   /**
219    * Scheduling JSON keys
220    */
221   public static final String SCHEDULING_AGENT_ID_KEY = "agentId";
222   public static final String SCHEDULING_START_KEY = "start";
223   public static final String SCHEDULING_END_KEY = "end";
224   private static final String SCHEDULING_AGENT_CONFIGURATION_KEY = "agentConfiguration";
225   public static final String SCHEDULING_PREVIOUS_AGENTID = "previousAgentId";
226   public static final String SCHEDULING_PREVIOUS_PREVIOUSENTRIES = "previousEntries";
227 
228   private static final String WORKFLOW_ACTION_STOP = "STOP";
229 
230   /** The logging facility */
231   static final Logger logger = LoggerFactory.getLogger(AbstractEventEndpoint.class);
232 
233   /** The configuration key that defines the default workflow definition */
234   //TODO Move to a constants file instead of declaring it at the top of multiple files?
235   protected static final String WORKFLOW_DEFINITION_DEFAULT = "org.opencastproject.workflow.default.definition";
236 
237   private static final String WORKFLOW_STATUS_TRANSLATION_PREFIX = "EVENTS.EVENTS.DETAILS.WORKFLOWS.OPERATION_STATUS.";
238 
239   /** The default time before a piece of signed content expires. 2 Hours. */
240   protected static final long DEFAULT_URL_SIGNING_EXPIRE_DURATION = 2 * 60 * 60;
241 
242   public abstract AssetManager getAssetManager();
243 
244   public abstract WorkflowService getWorkflowService();
245 
246   public abstract ElasticsearchIndex getIndex();
247 
248   public abstract JobEndpoint getJobService();
249 
250   public abstract SeriesEndpoint getSeriesEndpoint();
251 
252   public abstract AclService getAclService();
253 
254   public abstract EventCommentService getEventCommentService();
255 
256   public abstract SecurityService getSecurityService();
257 
258   public abstract IndexService getIndexService();
259 
260   public abstract AuthorizationService getAuthorizationService();
261 
262   public abstract SchedulerService getSchedulerService();
263 
264   public abstract CaptureAgentStateService getCaptureAgentStateService();
265 
266   public abstract AdminUIConfiguration getAdminUIConfiguration();
267 
268   public abstract long getUrlSigningExpireDuration();
269 
270   public abstract UrlSigningService getUrlSigningService();
271 
272   public abstract Boolean signWithClientIP();
273 
274   public abstract Boolean getOnlySeriesWithWriteAccessEventModal();
275 
276   public abstract Boolean getOnlyEventsWithWriteAccessEventsTab();
277 
278   public abstract UserDirectoryService getUserDirectoryService();
279 
280   /** Default server URL */
281   protected String serverUrl = "http://localhost:8080";
282 
283   /** Service url */
284   protected String serviceUrl = null;
285 
286   /** The default workflow identifier, if one is configured */
287   protected String defaultWorkflowDefinionId = null;
288 
289   /** The system user name (default set here for unit tests) */
290   private String systemUserName = "opencast_system_account";
291 
292   /**
293    * Activates REST service.
294    *
295    * @param cc
296    *          ComponentContext
297    */
298   @Activate
299   public void activate(ComponentContext cc) {
300     if (cc != null) {
301       String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
302       if (StringUtils.isNotBlank(ccServerUrl))
303         this.serverUrl = ccServerUrl;
304 
305       this.serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
306 
307       String ccDefaultWorkflowDefinionId = StringUtils.trimToNull(cc.getBundleContext().getProperty(WORKFLOW_DEFINITION_DEFAULT));
308 
309       if (StringUtils.isNotBlank(ccDefaultWorkflowDefinionId))
310         this.defaultWorkflowDefinionId = ccDefaultWorkflowDefinionId;
311 
312       systemUserName = SecurityUtil.getSystemUserName(cc);
313     }
314   }
315 
316   /* As the list of event ids can grow large, we use a POST request to avoid problems with too large query strings */
317   @POST
318   @Path("workflowProperties")
319   @Produces(MediaType.APPLICATION_JSON)
320   @RestQuery(name = "workflowProperties", description = "Returns workflow properties for the specified events",
321              returnDescription = "The workflow properties for every event as JSON", restParameters = {
322                 @RestParameter(name = "eventIds", description = "A JSON array of ids of the events", isRequired = true, type = RestParameter.Type.STRING)},
323              responses = {
324                 @RestResponse(description = "Returns the workflow properties for the events as JSON", responseCode = HttpServletResponse.SC_OK),
325                 @RestResponse(description = "The list of ids could not be parsed into a json list.", responseCode = HttpServletResponse.SC_BAD_REQUEST)
326               })
327   public Response getEventWorkflowProperties(@FormParam("eventIds") String eventIds) throws UnauthorizedException {
328     if (StringUtils.isBlank(eventIds)) {
329       return Response.status(Response.Status.BAD_REQUEST).build();
330     }
331 
332     JSONParser parser = new JSONParser();
333     List<String> ids;
334     try {
335       ids = (List<String>) parser.parse(eventIds);
336     } catch (org.json.simple.parser.ParseException e) {
337       logger.error("Unable to parse '{}'", eventIds, e);
338       return Response.status(Response.Status.BAD_REQUEST).build();
339     } catch (ClassCastException e) {
340       logger.error("Unable to cast '{}'", eventIds, e);
341       return Response.status(Response.Status.BAD_REQUEST).build();
342     }
343 
344     final Map<String, Map<String, String>> eventWithProperties = getIndexService().getEventWorkflowProperties(ids);
345     JsonObject jsonEvents = new JsonObject();
346 
347     for (Entry<String, Map<String, String>> event : eventWithProperties.entrySet()) {
348       JsonObject jsonProperties = new JsonObject();
349 
350       for (Entry<String, String> property : event.getValue().entrySet()) {
351         jsonProperties.add(property.getKey(), new JsonPrimitive(property.getValue()));
352       }
353 
354       jsonEvents.add(event.getKey(), jsonProperties);
355     }
356 
357     return okJson(jsonEvents);
358   }
359 
360 
361   @GET
362   @Path("catalogAdapters")
363   @Produces(MediaType.APPLICATION_JSON)
364   @RestQuery(name = "getcataloguiadapters", description = "Returns the available catalog UI adapters as JSON", returnDescription = "The catalog UI adapters as JSON", responses = {
365           @RestResponse(description = "Returns the available catalog UI adapters as JSON", responseCode = HttpServletResponse.SC_OK) })
366   public Response getCatalogAdapters() {
367     JsonArray jsonAdapters = new JsonArray();
368     for (EventCatalogUIAdapter adapter : getIndexService().getEventCatalogUIAdapters()) {
369       JsonObject obj = new JsonObject();
370       obj.addProperty("flavor", adapter.getFlavor().toString());
371       obj.addProperty("title", adapter.getUITitle());
372       jsonAdapters.add(obj);
373     }
374 
375     return okJson(jsonAdapters);
376   }
377 
378   @GET
379   @Path("{eventId}")
380   @Produces(MediaType.APPLICATION_JSON)
381   @RestQuery(name = "getevent", description = "Returns the event by the given id as JSON", returnDescription = "The event as JSON", pathParameters = {
382           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
383                   @RestResponse(description = "Returns the event as JSON", responseCode = HttpServletResponse.SC_OK),
384                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
385   public Response getEventResponse(@PathParam("eventId") String id) throws Exception {
386     Optional<Event> eventOpt = getIndexService().getEvent(id, getIndex());
387     if (eventOpt.isPresent()) {
388       Event event = eventOpt.get();
389       event.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
390       JsonObject json = eventToJSON(event, Optional.empty());
391       return okJson(json);
392     }
393     return notFound("Cannot find an event with id '%s'.", id);
394   }
395 
396   @DELETE
397   @Path("{eventId}")
398   @Produces(MediaType.APPLICATION_JSON)
399   @RestQuery(name = "deleteevent", description = "Delete a single event.", returnDescription = "Ok if the event has been deleted.", pathParameters = {
400           @RestParameter(name = "eventId", isRequired = true, description = "The id of the event to delete.", type = STRING), }, responses = {
401                   @RestResponse(responseCode = SC_OK, description = "The event has been deleted."),
402                   @RestResponse(responseCode = SC_ACCEPTED, description = "The event will be retracted and deleted afterwards."),
403                   @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "If the current user is not authorized to perform this action") })
404   public Response deleteEvent(@PathParam("eventId") String id) throws UnauthorizedException, SearchIndexException {
405     final Optional<Event> event = checkAgentAccessForEvent(id);
406     if (event.isEmpty()) {
407       return RestUtil.R.notFound(id);
408     }
409     final IndexService.EventRemovalResult result;
410     try {
411       result = getIndexService().removeEvent(event.get(), getAdminUIConfiguration().getRetractWorkflowId());
412     } catch (WorkflowDatabaseException e) {
413       logger.error("Workflow database is not reachable. This may be a temporary problem.");
414       return RestUtil.R.serverError();
415     } catch (NotFoundException e) {
416       logger.error("Configured retract workflow not found. Check your configuration.");
417       return RestUtil.R.serverError();
418     }
419     switch (result) {
420       case SUCCESS:
421         return Response.ok().build();
422       case RETRACTING:
423         return Response.accepted().build();
424       case GENERAL_FAILURE:
425         return Response.serverError().build();
426       case NOT_FOUND:
427         return RestUtil.R.notFound(id);
428       default:
429         throw new RuntimeException("Unknown EventRemovalResult type: " + result.name());
430     }
431   }
432 
433   @POST
434   @Path("deleteEvents")
435   @Produces(MediaType.APPLICATION_JSON)
436   @RestQuery(name = "deleteevents", description = "Deletes a json list of events by their given ids e.g. [\"1dbe7255-e17d-4279-811d-a5c7ced689bf\", \"04fae22b-0717-4f59-8b72-5f824f76d529\"]", returnDescription = "Returns a JSON object containing a list of event ids that were deleted, not found or if there was a server error.", responses = {
437           @RestResponse(description = "Events have been deleted", responseCode = HttpServletResponse.SC_OK),
438           @RestResponse(description = "The list of ids could not be parsed into a json list.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
439           @RestResponse(description = "If the current user is not authorized to perform this action", responseCode = HttpServletResponse.SC_UNAUTHORIZED) })
440   public Response deleteEvents(String eventIdsContent) throws UnauthorizedException, SearchIndexException {
441     if (StringUtils.isBlank(eventIdsContent)) {
442       return Response.status(Response.Status.BAD_REQUEST).build();
443     }
444 
445     JSONParser parser = new JSONParser();
446     JSONArray eventIdsJsonArray;
447     try {
448       eventIdsJsonArray = (JSONArray) parser.parse(eventIdsContent);
449     } catch (org.json.simple.parser.ParseException e) {
450       logger.error("Unable to parse '{}'", eventIdsContent, e);
451       return Response.status(Response.Status.BAD_REQUEST).build();
452     } catch (ClassCastException e) {
453       logger.error("Unable to cast '{}'", eventIdsContent, e);
454       return Response.status(Response.Status.BAD_REQUEST).build();
455     }
456 
457     BulkOperationResult result = new BulkOperationResult();
458 
459     for (Object eventIdObject : eventIdsJsonArray) {
460       final String eventId = eventIdObject.toString();
461       try {
462         final Optional<Event> event = checkAgentAccessForEvent(eventId);
463         if (event.isPresent()) {
464           final IndexService.EventRemovalResult currentResult = getIndexService().removeEvent(event.get(),
465                   getAdminUIConfiguration().getRetractWorkflowId());
466           switch (currentResult) {
467             case SUCCESS:
468               result.addOk(eventId);
469               break;
470             case RETRACTING:
471               result.addAccepted(eventId);
472               break;
473             case GENERAL_FAILURE:
474               result.addServerError(eventId);
475               break;
476             case NOT_FOUND:
477               result.addNotFound(eventId);
478               break;
479             default:
480               throw new RuntimeException("Unknown EventRemovalResult type: " + currentResult.name());
481           }
482         } else {
483           result.addNotFound(eventId);
484         }
485       } catch (UnauthorizedException e) {
486         result.addUnauthorized(eventId);
487       } catch (WorkflowDatabaseException e) {
488         logger.error("Workflow database is not reachable. This may be a temporary problem.");
489         return RestUtil.R.serverError();
490       } catch (NotFoundException e) {
491         logger.error("Configured retract workflow not found. Check your configuration.");
492         return RestUtil.R.serverError();
493       }
494     }
495     return Response.ok(result.toJson()).build();
496   }
497 
498   @GET
499   @Path("{eventId}/publications.json")
500   @Produces(MediaType.APPLICATION_JSON)
501   @RestQuery(name = "geteventpublications", description = "Returns all the data related to the publications tab in the event details modal as JSON", returnDescription = "All the data related to the event publications tab as JSON", pathParameters = {
502           @RestParameter(name = "eventId", description = "The event id (mediapackage id).", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
503                   @RestResponse(description = "Returns all the data related to the event publications tab as JSON", responseCode = HttpServletResponse.SC_OK),
504                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
505   public Response getEventPublicationsTab(@PathParam("eventId") String id) throws Exception {
506     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
507     if (optEvent.isEmpty())
508       return notFound("Cannot find an event with id '%s'.", id);
509 
510     // Quick actions have been temporally removed from the publications tab
511     // ---------------------------------------------------------------
512     // List<JValue> actions = new ArrayList<JValue>();
513     // List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions();
514     // for (WorkflowDefinition wflDef : workflowsDefinitions) {
515     // if (wflDef.containsTag(WORKFLOWDEF_TAG)) {
516     //
517     // actions.add(obj(f("id", v(wflDef.getId())), f("title", v(Opt.nul(wflDef.getTitle()).or(""))),
518     // f("description", v(Opt.nul(wflDef.getDescription()).or(""))),
519     // f("configuration_panel", v(Opt.nul(wflDef.getConfigurationPanel()).or("")))));
520     // }
521     // }
522 
523     Event event = optEvent.get();
524 
525     // Convert event publications to JSON array
526     List<JsonObject> pubJSONList = eventPublicationsToJson(event);
527     JsonArray publicationsJsonArray = new JsonArray();
528     for (JsonObject pubJson : pubJSONList) {
529       publicationsJsonArray.add(pubJson);
530     }
531 
532     JsonObject result = new JsonObject();
533     result.add("publications", publicationsJsonArray);
534 
535     // Add start-date and end-date as strings, or blank if null
536     String startDate = event.getRecordingStartDate() != null
537         ? event.getRecordingStartDate().toString()
538         : "";
539     String endDate = event.getRecordingEndDate() != null
540         ? event.getRecordingEndDate().toString()
541         : "";
542     result.addProperty("start-date", startDate);
543     result.addProperty("end-date", endDate);
544 
545     return okJson(result);
546   }
547 
548   private List<JsonObject> eventPublicationsToJson(Event event) {
549     List<JsonObject> pubJSON = new ArrayList<>();
550 
551     for (Publication publication : event.getPublications()) {
552       if (internalChannelFilter.test(publication)) {
553         pubJSON.add(publicationToJson.apply(publication));
554       }
555     }
556 
557     return pubJSON;
558   }
559 
560   private List<JsonObject> eventCommentsToJson(List<EventComment> comments) {
561     List<JsonObject> commentArr = new ArrayList<>();
562 
563     for (EventComment c : comments) {
564       JsonObject author = new JsonObject();
565       author.addProperty("name", c.getAuthor().getName());
566       if (c.getAuthor().getEmail() != null) {
567         author.addProperty("email", c.getAuthor().getEmail());
568       } else {
569         author.add("email", null);
570       }
571       author.addProperty("username", c.getAuthor().getUsername());
572 
573       JsonArray replies = new JsonArray();
574       List<JsonObject> replyJsonList = eventCommentRepliesToJson(c.getReplies());
575       for (JsonObject replyJson : replyJsonList) {
576         replies.add(replyJson);
577       }
578 
579       JsonObject commentJson = new JsonObject();
580       commentJson.addProperty("reason", c.getReason());
581       commentJson.addProperty("resolvedStatus", c.isResolvedStatus());
582       commentJson.addProperty("modificationDate", c.getModificationDate().toInstant().toString());
583       commentJson.add("replies", replies);
584       commentJson.add("author", author);
585       commentJson.addProperty("id", c.getId().get());
586       commentJson.addProperty("text", c.getText());
587       commentJson.addProperty("creationDate", c.getCreationDate().toInstant().toString());
588 
589       commentArr.add(commentJson);
590     }
591 
592     return commentArr;
593   }
594 
595   private List<JsonObject> eventCommentRepliesToJson(List<EventCommentReply> replies) {
596     List<JsonObject> repliesArr = new ArrayList<>();
597 
598     for (EventCommentReply r : replies) {
599       JsonObject author = new JsonObject();
600       author.addProperty("name", r.getAuthor().getName());
601       if (r.getAuthor().getEmail() != null) {
602         author.addProperty("email", r.getAuthor().getEmail());
603       } else {
604         author.add("email", null);
605       }
606       author.addProperty("username", r.getAuthor().getUsername());
607 
608       JsonObject replyJson = new JsonObject();
609       replyJson.addProperty("id", r.getId().get());
610       replyJson.addProperty("text", r.getText());
611       replyJson.addProperty("creationDate", r.getCreationDate().toInstant().toString());
612       replyJson.addProperty("modificationDate", r.getModificationDate().toInstant().toString());
613       replyJson.add("author", author);
614 
615       repliesArr.add(replyJson);
616     }
617 
618     return repliesArr;
619   }
620 
621   @GET
622   @Path("{eventId}/scheduling.json")
623   @Produces(MediaType.APPLICATION_JSON)
624   @RestQuery(name = "getEventSchedulingMetadata", description = "Returns all of the scheduling metadata for an event", returnDescription = "All the technical metadata related to scheduling as JSON", pathParameters = {
625           @RestParameter(name = "eventId", description = "The event id (mediapackage id).", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
626                   @RestResponse(description = "Returns all the data related to the event scheduling tab as JSON", responseCode = HttpServletResponse.SC_OK),
627                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
628   public Response getEventScheduling(@PathParam("eventId") String eventId)
629           throws NotFoundException, UnauthorizedException, SearchIndexException {
630     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
631     if (optEvent.isEmpty())
632       return notFound("Cannot find an event with id '%s'.", eventId);
633 
634     try {
635       TechnicalMetadata technicalMetadata = getSchedulerService().getTechnicalMetadata(eventId);
636       return okJson(technicalMetadataToJson(technicalMetadata));
637     } catch (SchedulerException e) {
638       logger.error("Unable to get technical metadata for event with id {}", eventId);
639       throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
640     }
641   }
642 
643   @POST
644   @Path("scheduling.json")
645   @Produces(MediaType.APPLICATION_JSON)
646   @RestQuery(name = "getEventsScheduling", description = "Returns all of the scheduling metadata for a list of events", returnDescription = "All the technical metadata related to scheduling as JSON", restParameters = {
647     @RestParameter(name = "eventIds", description = "An array of event IDs (mediapackage id)", isRequired = true, type = RestParameter.Type.STRING),
648     @RestParameter(name = "ignoreNonScheduled", description = "Whether events that are not really scheduled events should be ignored or produce an error", isRequired = true, type = RestParameter.Type.BOOLEAN) }, responses = {
649     @RestResponse(description = "Returns all the data related to the event scheduling tab as JSON", responseCode = HttpServletResponse.SC_OK),
650     @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
651   public Response getEventsScheduling(@FormParam("eventIds") final List<String> eventIds, @FormParam("ignoreNonScheduled") final boolean ignoreNonScheduled) {
652     JsonArray fields = new JsonArray();
653 
654     for (final String eventId : eventIds) {
655       try {
656         fields.add(technicalMetadataToJson(getSchedulerService().getTechnicalMetadata(eventId)));
657       } catch (final NotFoundException e) {
658         if (!ignoreNonScheduled) {
659           logger.warn("Unable to find id {}", eventId, e);
660           return notFound("Cannot find an event with id '%s'.", eventId);
661         }
662       } catch (final UnauthorizedException e) {
663         logger.warn("Unauthorized access to event ID {}", eventId, e);
664         return Response.status(Status.BAD_REQUEST).build();
665       } catch (final SchedulerException e) {
666         logger.warn("Scheduler exception accessing event ID {}", eventId, e);
667         return Response.status(Status.BAD_REQUEST).build();
668       }
669     }
670     return okJson(fields);
671   }
672 
673   @PUT
674   @Path("{eventId}/scheduling")
675   @RestQuery(name = "updateEventScheduling", description = "Updates the scheduling information of an event", returnDescription = "The method doesn't return any content", pathParameters = {
676           @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, restParameters = {
677                   @RestParameter(name = "scheduling", isRequired = true, description = "The updated scheduling (JSON object)", type = RestParameter.Type.TEXT) }, responses = {
678                           @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required params were missing in the request."),
679                           @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."),
680                           @RestResponse(responseCode = SC_NO_CONTENT, description = "The method doesn't return any content") })
681   public Response updateEventScheduling(@PathParam("eventId") String eventId,
682           @FormParam("scheduling") String scheduling)
683           throws NotFoundException, UnauthorizedException, SearchIndexException, IndexServiceException {
684     if (StringUtils.isBlank(scheduling))
685       return RestUtil.R.badRequest("Missing parameters");
686 
687     try {
688       final Event event = getEventOrThrowNotFoundException(eventId);
689       updateEventScheduling(scheduling, event);
690       return Response.noContent().build();
691     } catch (JSONException e) {
692       return RestUtil.R.badRequest("The scheduling object is not valid");
693     } catch (ParseException e) {
694       return RestUtil.R.badRequest("The UTC dates in the scheduling object is not valid");
695     } catch (SchedulerException e) {
696       logger.error("Unable to update scheduling technical metadata of event {}", eventId, e);
697       throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
698     } catch (IllegalStateException e) {
699       return RestUtil.R.badRequest(e.getMessage());
700     }
701   }
702 
703   private void updateEventScheduling(String scheduling, Event event) throws NotFoundException, UnauthorizedException,
704     SchedulerException, JSONException, ParseException, SearchIndexException, IndexServiceException {
705     final TechnicalMetadata technicalMetadata = getSchedulerService().getTechnicalMetadata(event.getIdentifier());
706     final org.codehaus.jettison.json.JSONObject schedulingJson = new org.codehaus.jettison.json.JSONObject(
707             scheduling);
708     Optional<String> agentId = Optional.empty();
709     if (schedulingJson.has(SCHEDULING_AGENT_ID_KEY)) {
710       agentId = Optional.of(schedulingJson.getString(SCHEDULING_AGENT_ID_KEY));
711       logger.trace("Updating agent id of event '{}' from '{}' to '{}'",
712               event.getIdentifier(), technicalMetadata.getAgentId(), agentId);
713     }
714 
715     Optional<String> previousAgentId = Optional.empty();
716     if (schedulingJson.has(SCHEDULING_PREVIOUS_AGENTID)) {
717       previousAgentId = Optional.of(schedulingJson.getString(SCHEDULING_PREVIOUS_AGENTID));
718     }
719 
720     Optional<String> previousAgentInputs = Optional.empty();
721     Optional<String> agentInputs = Optional.empty();
722     if (agentId.isPresent() && previousAgentId.isPresent()) {
723       Agent previousAgent = getCaptureAgentStateService().getAgent(previousAgentId.get());
724       Agent agent = getCaptureAgentStateService().getAgent(agentId.get());
725 
726       previousAgentInputs = Optional.ofNullable(previousAgent.getCapabilities().getProperty(CaptureParameters.CAPTURE_DEVICE_NAMES));
727       agentInputs = Optional.ofNullable(agent.getCapabilities().getProperty(CaptureParameters.CAPTURE_DEVICE_NAMES));
728     }
729 
730     // Check if we are allowed to re-schedule on this agent
731     checkAgentAccessForAgent(technicalMetadata.getAgentId());
732     if (agentId.isPresent()) {
733       checkAgentAccessForAgent(agentId.get());
734     }
735 
736     Optional<Date> start = Optional.empty();
737     if (schedulingJson.has(SCHEDULING_START_KEY)) {
738       start = Optional.of(new Date(DateTimeSupport.fromUTC(schedulingJson.getString(SCHEDULING_START_KEY))));
739       logger.trace("Updating start time of event '{}' id from '{}' to '{}'",
740         event.getIdentifier(), DateTimeSupport.toUTC(technicalMetadata.getStartDate().getTime()),
741                       DateTimeSupport.toUTC(start.get().getTime()));
742     }
743 
744     Optional<Date> end = Optional.empty();
745     if (schedulingJson.has(SCHEDULING_END_KEY)) {
746       end = Optional.of(new Date(DateTimeSupport.fromUTC(schedulingJson.getString(SCHEDULING_END_KEY))));
747       logger.trace("Updating end time of event '{}' id from '{}' to '{}'",
748         event.getIdentifier(), DateTimeSupport.toUTC(technicalMetadata.getEndDate().getTime()),
749                       DateTimeSupport.toUTC(end.get().getTime()));
750     }
751 
752     Optional<Map<String, String>> agentConfiguration = Optional.empty();
753     if (schedulingJson.has(SCHEDULING_AGENT_CONFIGURATION_KEY)) {
754       agentConfiguration = Optional.of(JSONUtils.toMap(schedulingJson.getJSONObject(SCHEDULING_AGENT_CONFIGURATION_KEY)));
755       logger.trace("Updating agent configuration of event '{}' id from '{}' to '{}'",
756         event.getIdentifier(), technicalMetadata.getCaptureAgentConfiguration(), agentConfiguration);
757     }
758 
759     Optional<Map<String, String>> previousAgentInputMethods = Optional.empty();
760     if (schedulingJson.has(SCHEDULING_PREVIOUS_PREVIOUSENTRIES)) {
761       previousAgentInputMethods = Optional.of(
762               JSONUtils.toMap(schedulingJson.getJSONObject(SCHEDULING_PREVIOUS_PREVIOUSENTRIES)));
763     }
764 
765     // If we had previously selected an agent, and both the old and new agent have the same set of input channels,
766     // copy which input channels are active to the new agent
767     if (previousAgentInputs.isPresent() && previousAgentInputs.isPresent() && agentInputs.isPresent()) {
768       Map<String, String> map = previousAgentInputMethods.get();
769       String mapAsString = map.keySet().stream()
770               .collect(Collectors.joining(","));
771       String previousInputs = mapAsString;
772 
773       if (previousAgentInputs.equals(agentInputs)) {
774         final Map<String, String> configMap = new HashMap<>(agentConfiguration.get());
775         configMap.put(CaptureParameters.CAPTURE_DEVICE_NAMES, previousInputs);
776         agentConfiguration = Optional.of(configMap);
777       }
778     }
779 
780     if ((start.isPresent() || end.isPresent())
781             && end.orElse(technicalMetadata.getEndDate()).before(start.orElse(technicalMetadata.getStartDate()))) {
782       throw new IllegalStateException("The end date is before the start date");
783     }
784 
785     if (!start.isEmpty() || !end.isEmpty() || !agentId.isEmpty() || !agentConfiguration.isEmpty()) {
786       getSchedulerService()
787         .updateEvent(event.getIdentifier(), start, end, agentId, Optional.empty(), Optional.empty(), Optional.empty(), agentConfiguration);
788     }
789   }
790 
791   private Event getEventOrThrowNotFoundException(final String eventId) throws NotFoundException, SearchIndexException {
792     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
793     if (optEvent.isPresent()) {
794       return optEvent.get();
795     } else {
796       throw new NotFoundException(format("Cannot find an event with id '%s'.", eventId));
797     }
798   }
799 
800   @GET
801   @Path("{eventId}/comments")
802   @Produces(MediaType.APPLICATION_JSON)
803   @RestQuery(name = "geteventcomments", description = "Returns all the data related to the comments tab in the event details modal as JSON", returnDescription = "All the data related to the event comments tab as JSON", pathParameters = {
804           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
805                   @RestResponse(description = "Returns all the data related to the event comments tab as JSON", responseCode = HttpServletResponse.SC_OK),
806                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
807   public Response getEventComments(@PathParam("eventId") String eventId) throws Exception {
808     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
809     if (optEvent.isEmpty())
810       return notFound("Cannot find an event with id '%s'.", eventId);
811 
812     try {
813       List<EventComment> comments = getEventCommentService().getComments(eventId);
814       List<Val> commentArr = new ArrayList<>();
815       for (EventComment c : comments) {
816         commentArr.add(c.toJson());
817       }
818       return Response.ok(org.opencastproject.util.Jsons.arr(commentArr).toJson(), MediaType.APPLICATION_JSON_TYPE)
819               .build();
820     } catch (EventCommentException e) {
821       logger.error("Unable to get comments from event {}", eventId, e);
822       throw new WebApplicationException(e);
823     }
824   }
825 
826   @GET
827   @Path("{eventId}/hasActiveTransaction")
828   @Produces(MediaType.TEXT_PLAIN)
829   @RestQuery(name = "hasactivetransaction", description = "Returns whether there is currently a transaction in progress for the given event", returnDescription = "Whether there is currently a transaction in progress for the given event", pathParameters = {
830           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
831                   @RestResponse(description = "Returns whether there is currently a transaction in progress for the given event", responseCode = HttpServletResponse.SC_OK),
832                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
833   public Response hasActiveTransaction(@PathParam("eventId") String eventId) throws Exception {
834     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
835     if (optEvent.isEmpty())
836       return notFound("Cannot find an event with id '%s'.", eventId);
837 
838     JSONObject json = new JSONObject();
839 
840     if (WorkflowUtil.isActive(optEvent.get().getWorkflowState())) {
841       json.put("active", true);
842     } else {
843       json.put("active", false);
844     }
845 
846     return Response.ok(json.toJSONString()).build();
847   }
848 
849   @GET
850   @Produces(MediaType.APPLICATION_JSON)
851   @Path("{eventId}/comment/{commentId}")
852   @RestQuery(name = "geteventcomment", description = "Returns the comment with the given identifier", returnDescription = "Returns the comment as JSON", pathParameters = {
853           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
854           @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, responses = {
855                   @RestResponse(responseCode = SC_OK, description = "The comment as JSON."),
856                   @RestResponse(responseCode = SC_NOT_FOUND, description = "No event or comment with this identifier was found.") })
857   public Response getEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId)
858           throws NotFoundException, Exception {
859     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
860     if (optEvent.isEmpty())
861       return notFound("Cannot find an event with id '%s'.", eventId);
862 
863     try {
864       EventComment comment = getEventCommentService().getComment(commentId);
865       return Response.ok(comment.toJson().toJson()).build();
866     } catch (NotFoundException e) {
867       throw e;
868     } catch (Exception e) {
869       logger.error("Could not retrieve comment {}", commentId, e);
870       throw new WebApplicationException(e);
871     }
872   }
873 
874   @PUT
875   @Path("{eventId}/comment/{commentId}")
876   @RestQuery(name = "updateeventcomment", description = "Updates an event comment", returnDescription = "The updated comment as JSON.", pathParameters = {
877           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
878           @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, restParameters = {
879                   @RestParameter(name = "text", isRequired = false, description = "The comment text", type = TEXT),
880                   @RestParameter(name = "reason", isRequired = false, description = "The comment reason", type = STRING),
881                   @RestParameter(name = "resolved", isRequired = false, description = "The comment resolved status", type = RestParameter.Type.BOOLEAN) }, responses = {
882                           @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to update has not been found."),
883                           @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
884   public Response updateEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId,
885           @FormParam("text") String text, @FormParam("reason") String reason, @FormParam("resolved") Boolean resolved)
886                   throws Exception {
887     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
888     if (optEvent.isEmpty())
889       return notFound("Cannot find an event with id '%s'.", eventId);
890 
891     try {
892       EventComment dto = getEventCommentService().getComment(commentId);
893 
894       if (StringUtils.isNotBlank(text)) {
895         text = text.trim();
896       } else {
897         text = dto.getText();
898       }
899 
900       if (StringUtils.isNotBlank(reason)) {
901         reason = reason.trim();
902       } else {
903         reason = dto.getReason();
904       }
905 
906       if (resolved == null)
907         resolved = dto.isResolvedStatus();
908 
909       EventComment updatedComment = EventComment.create(dto.getId(), eventId,
910               getSecurityService().getOrganization().getId(), text, dto.getAuthor(), reason, resolved,
911               dto.getCreationDate(), new Date(), dto.getReplies());
912 
913       updatedComment = getEventCommentService().updateComment(updatedComment);
914       List<EventComment> comments = getEventCommentService().getComments(eventId);
915       getIndexService().updateCommentCatalog(optEvent.get(), comments);
916       return Response.ok(updatedComment.toJson().toJson()).build();
917     } catch (NotFoundException e) {
918       throw e;
919     } catch (Exception e) {
920       logger.error("Unable to update the comments catalog on event {}", eventId, e);
921       throw new WebApplicationException(e);
922     }
923   }
924 
925   @POST
926   @Path("{eventId}/access")
927   @RestQuery(name = "applyAclToEvent", description = "Immediate application of an ACL to an event", returnDescription = "Status code", pathParameters = {
928           @RestParameter(name = "eventId", isRequired = true, description = "The event ID", type = STRING) }, restParameters = {
929                   @RestParameter(name = "acl", isRequired = true, description = "The ACL to apply", type = STRING) }, responses = {
930                           @RestResponse(responseCode = SC_OK, description = "The ACL has been successfully applied"),
931                           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the given ACL"),
932                           @RestResponse(responseCode = SC_NOT_FOUND, description = "The the event has not been found"),
933                           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "Not authorized to perform this action"),
934                           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Internal error") })
935   public Response applyAclToEvent(@PathParam("eventId") String eventId, @FormParam("acl") String acl)
936           throws NotFoundException, UnauthorizedException, SearchIndexException, IndexServiceException {
937     final AccessControlList accessControlList;
938     try {
939       accessControlList = AccessControlParser.parseAcl(acl);
940     } catch (Exception e) {
941       logger.warn("Unable to parse ACL '{}'", acl);
942       return badRequest();
943     }
944 
945     try {
946       final Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
947       if (optEvent.isEmpty()) {
948         logger.warn("Unable to find the event '{}'", eventId);
949         return notFound();
950       }
951 
952       Source eventSource = getIndexService().getEventSource(optEvent.get());
953       if (eventSource == Source.ARCHIVE) {
954         Optional<MediaPackage> mediaPackage = getAssetManager().getMediaPackage(eventId);
955         Optional<AccessControlList> aclOpt = Optional.ofNullable(accessControlList);
956         // the episode service is the source of authority for the retrieval of media packages
957         if (mediaPackage.isPresent()) {
958           MediaPackage episodeSvcMp = mediaPackage.get();
959           aclOpt.ifPresentOrElse(
960               aclPresent -> {
961                 try {
962                   MediaPackage mp = getAuthorizationService()
963                       .setAcl(episodeSvcMp, AclScope.Episode, aclPresent)
964                       .getA();
965                   getAssetManager().takeSnapshot(mp);
966                 } catch (MediaPackageException e) {
967                   logger.error("Error getting ACL from media package", e);
968                 }
969               },
970               () -> {
971                 MediaPackage mp = getAuthorizationService().removeAcl(episodeSvcMp, AclScope.Episode);
972                 getAssetManager().takeSnapshot(mp);
973               }
974           );
975           return ok();
976         }
977         logger.warn("Unable to find the event '{}'", eventId);
978         return notFound();
979       } else if (eventSource == Source.WORKFLOW) {
980         logger.warn("An ACL cannot be edited while an event is part of a current workflow because it might"
981                 + " lead to inconsistent ACLs i.e. changed after distribution so that the old ACL is still "
982                 + "being used by the distribution channel.");
983         JSONObject json = new JSONObject();
984         json.put("Error", "Unable to edit an ACL for a current workflow.");
985         return conflict(json.toJSONString());
986       } else {
987         MediaPackage mediaPackage = getIndexService().getEventMediapackage(optEvent.get());
988         mediaPackage = getAuthorizationService().setAcl(mediaPackage, AclScope.Episode, accessControlList).getA();
989         // We could check agent access here if we want to forbid updating ACLs for users without access.
990         getSchedulerService().updateEvent(eventId, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
991                 Optional.of(mediaPackage), Optional.empty(), Optional.empty());
992         return ok();
993       }
994     } catch (MediaPackageException e) {
995       if (e.getCause() instanceof UnauthorizedException) {
996         return forbidden();
997       }
998       logger.error("Error applying acl '{}' to event '{}'", accessControlList, eventId, e);
999       return serverError();
1000     } catch (SchedulerException e) {
1001       logger.error("Error applying ACL to scheduled event {}", eventId, e);
1002       return serverError();
1003     }
1004   }
1005 
1006   @POST
1007   @Path("{eventId}/comment")
1008   @Produces(MediaType.APPLICATION_JSON)
1009   @RestQuery(name = "createeventcomment", description = "Creates a comment related to the event given by the identifier", returnDescription = "The comment related to the event as JSON", pathParameters = {
1010           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
1011                   @RestParameter(name = "text", isRequired = true, description = "The comment text", type = TEXT),
1012                   @RestParameter(name = "resolved", isRequired = false, description = "The comment resolved status", type = RestParameter.Type.BOOLEAN),
1013                   @RestParameter(name = "reason", isRequired = false, description = "The comment reason", type = STRING) }, responses = {
1014                           @RestResponse(description = "The comment has been created.", responseCode = HttpServletResponse.SC_CREATED),
1015                           @RestResponse(description = "If no text ist set.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1016                           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1017   public Response createEventComment(@PathParam("eventId") String eventId, @FormParam("text") String text,
1018           @FormParam("reason") String reason, @FormParam("resolved") Boolean resolved) throws Exception {
1019     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1020     if (optEvent.isEmpty())
1021       return notFound("Cannot find an event with id '%s'.", eventId);
1022 
1023     if (StringUtils.isBlank(text))
1024       return Response.status(Status.BAD_REQUEST).build();
1025 
1026     User author = getSecurityService().getUser();
1027     try {
1028       EventComment createdComment = EventComment.create(Optional.<Long> empty(), eventId,
1029               getSecurityService().getOrganization().getId(), text, author, reason, BooleanUtils.toBoolean(reason));
1030       createdComment = getEventCommentService().updateComment(createdComment);
1031       List<EventComment> comments = getEventCommentService().getComments(eventId);
1032       getIndexService().updateCommentCatalog(optEvent.get(), comments);
1033       return Response.created(getCommentUrl(eventId, createdComment.getId().get()))
1034               .entity(createdComment.toJson().toJson()).build();
1035     } catch (Exception e) {
1036       logger.error("Unable to create a comment on the event {}", eventId, e);
1037       throw new WebApplicationException(e);
1038     }
1039   }
1040 
1041   @POST
1042   @Path("{eventId}/comment/{commentId}")
1043   @RestQuery(name = "resolveeventcomment", description = "Resolves an event comment", returnDescription = "The resolved comment.", pathParameters = {
1044           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1045           @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, responses = {
1046                   @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to resolve has not been found."),
1047                   @RestResponse(responseCode = SC_OK, description = "The resolved comment as JSON.") })
1048   public Response resolveEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId)
1049           throws Exception {
1050     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1051     if (optEvent.isEmpty())
1052       return notFound("Cannot find an event with id '%s'.", eventId);
1053 
1054     try {
1055       EventComment dto = getEventCommentService().getComment(commentId);
1056       EventComment updatedComment = EventComment.create(dto.getId(), dto.getEventId(), dto.getOrganization(),
1057               dto.getText(), dto.getAuthor(), dto.getReason(), true, dto.getCreationDate(), new Date(),
1058               dto.getReplies());
1059 
1060       updatedComment = getEventCommentService().updateComment(updatedComment);
1061       List<EventComment> comments = getEventCommentService().getComments(eventId);
1062       getIndexService().updateCommentCatalog(optEvent.get(), comments);
1063       return Response.ok(updatedComment.toJson().toJson()).build();
1064     } catch (NotFoundException e) {
1065       throw e;
1066     } catch (Exception e) {
1067       logger.error("Could not resolve comment {}", commentId, e);
1068       throw new WebApplicationException(e);
1069     }
1070   }
1071 
1072   @DELETE
1073   @Path("{eventId}/comment/{commentId}")
1074   @Produces(MediaType.APPLICATION_JSON)
1075   @RestQuery(name = "deleteeventcomment", description = "Deletes a event related comment by its identifier", returnDescription = "No content", pathParameters = {
1076           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1077           @RestParameter(name = "commentId", description = "The comment id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1078                   @RestResponse(description = "The event related comment has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
1079                   @RestResponse(description = "No event or comment with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1080   public Response deleteEventComment(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId)
1081           throws Exception {
1082     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1083     if (optEvent.isEmpty())
1084       return notFound("Cannot find an event with id '%s'.", eventId);
1085 
1086     try {
1087       getEventCommentService().deleteComment(commentId);
1088       List<EventComment> comments = getEventCommentService().getComments(eventId);
1089       getIndexService().updateCommentCatalog(optEvent.get(), comments);
1090       return Response.noContent().build();
1091     } catch (NotFoundException e) {
1092       throw e;
1093     } catch (Exception e) {
1094       logger.error("Unable to delete comment {} on event {}", commentId, eventId, e);
1095       throw new WebApplicationException(e);
1096     }
1097   }
1098 
1099   @DELETE
1100   @Path("{eventId}/comment/{commentId}/{replyId}")
1101   @RestQuery(name = "deleteeventreply", description = "Delete an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = {
1102           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1103           @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING),
1104           @RestParameter(name = "replyId", isRequired = true, description = "The comment reply identifier", type = STRING) }, responses = {
1105                   @RestResponse(responseCode = SC_NOT_FOUND, description = "No event comment or reply with this identifier was found."),
1106                   @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
1107   public Response deleteEventCommentReply(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId,
1108           @PathParam("replyId") long replyId) throws Exception {
1109     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1110     if (optEvent.isEmpty())
1111       return notFound("Cannot find an event with id '%s'.", eventId);
1112 
1113     EventComment comment = null;
1114     EventCommentReply reply = null;
1115     try {
1116       comment = getEventCommentService().getComment(commentId);
1117       for (EventCommentReply r : comment.getReplies()) {
1118         if (r.getId().isEmpty() || replyId != r.getId().get().longValue())
1119           continue;
1120         reply = r;
1121         break;
1122       }
1123 
1124       if (reply == null)
1125         throw new NotFoundException("Reply with id " + replyId + " not found!");
1126 
1127       comment.removeReply(reply);
1128 
1129       EventComment updatedComment = getEventCommentService().updateComment(comment);
1130       List<EventComment> comments = getEventCommentService().getComments(eventId);
1131       getIndexService().updateCommentCatalog(optEvent.get(), comments);
1132       return Response.ok(updatedComment.toJson().toJson()).build();
1133     } catch (NotFoundException e) {
1134       throw e;
1135     } catch (Exception e) {
1136       logger.warn("Could not remove event comment reply {} from comment {}", replyId, commentId, e);
1137       throw new WebApplicationException(e);
1138     }
1139   }
1140 
1141   @PUT
1142   @Path("{eventId}/comment/{commentId}/{replyId}")
1143   @RestQuery(name = "updateeventcommentreply", description = "Updates an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = {
1144           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1145           @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING),
1146           @RestParameter(name = "replyId", isRequired = true, description = "The comment reply identifier", type = STRING) }, restParameters = {
1147                   @RestParameter(name = "text", isRequired = true, description = "The comment reply text", type = TEXT) }, responses = {
1148                           @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to extend with a reply or the reply has not been found."),
1149                           @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "If no text is set."),
1150                           @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
1151   public Response updateEventCommentReply(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId,
1152           @PathParam("replyId") long replyId, @FormParam("text") String text) throws Exception {
1153     if (StringUtils.isBlank(text))
1154       return Response.status(Status.BAD_REQUEST).build();
1155 
1156     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1157     if (optEvent.isEmpty())
1158       return notFound("Cannot find an event with id '%s'.", eventId);
1159 
1160     EventComment comment = null;
1161     EventCommentReply reply = null;
1162     try {
1163       comment = getEventCommentService().getComment(commentId);
1164       for (EventCommentReply r : comment.getReplies()) {
1165         if (r.getId().isEmpty() || replyId != r.getId().get().longValue())
1166           continue;
1167         reply = r;
1168         break;
1169       }
1170 
1171       if (reply == null)
1172         throw new NotFoundException("Reply with id " + replyId + " not found!");
1173 
1174       EventCommentReply updatedReply = EventCommentReply.create(reply.getId(), text.trim(), reply.getAuthor(),
1175               reply.getCreationDate(), new Date());
1176       comment.removeReply(reply);
1177       comment.addReply(updatedReply);
1178 
1179       EventComment updatedComment = getEventCommentService().updateComment(comment);
1180       List<EventComment> comments = getEventCommentService().getComments(eventId);
1181       getIndexService().updateCommentCatalog(optEvent.get(), comments);
1182       return Response.ok(updatedComment.toJson().toJson()).build();
1183     } catch (NotFoundException e) {
1184       throw e;
1185     } catch (Exception e) {
1186       logger.warn("Could not update event comment reply {} from comment {}", replyId, commentId, e);
1187       throw new WebApplicationException(e);
1188     }
1189   }
1190 
1191   @POST
1192   @Path("{eventId}/comment/{commentId}/reply")
1193   @RestQuery(name = "createeventcommentreply", description = "Creates an event comment reply", returnDescription = "The updated comment as JSON.", pathParameters = {
1194           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1195           @RestParameter(name = "commentId", isRequired = true, description = "The comment identifier", type = STRING) }, restParameters = {
1196                   @RestParameter(name = "text", isRequired = true, description = "The comment reply text", type = TEXT),
1197                   @RestParameter(name = "resolved", isRequired = false, description = "Flag defining if this reply solve or not the comment.", type = BOOLEAN) }, responses = {
1198                           @RestResponse(responseCode = SC_NOT_FOUND, description = "The event or comment to extend with a reply has not been found."),
1199                           @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "If no text is set."),
1200                           @RestResponse(responseCode = SC_OK, description = "The updated comment as JSON.") })
1201   public Response createEventCommentReply(@PathParam("eventId") String eventId, @PathParam("commentId") long commentId,
1202           @FormParam("text") String text, @FormParam("resolved") Boolean resolved) throws Exception {
1203     if (StringUtils.isBlank(text))
1204       return Response.status(Status.BAD_REQUEST).build();
1205 
1206     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1207     if (optEvent.isEmpty())
1208       return notFound("Cannot find an event with id '%s'.", eventId);
1209 
1210     EventComment comment = null;
1211     try {
1212       comment = getEventCommentService().getComment(commentId);
1213       EventComment updatedComment;
1214 
1215       if (resolved != null && resolved) {
1216         // If the resolve flag is set to true, change to comment to resolved
1217         updatedComment = EventComment.create(comment.getId(), comment.getEventId(), comment.getOrganization(),
1218                 comment.getText(), comment.getAuthor(), comment.getReason(), true, comment.getCreationDate(),
1219                 new Date(), comment.getReplies());
1220       } else {
1221         updatedComment = comment;
1222       }
1223 
1224       User author = getSecurityService().getUser();
1225       EventCommentReply reply = EventCommentReply.create(Optional.<Long> empty(), text, author);
1226       updatedComment.addReply(reply);
1227 
1228       updatedComment = getEventCommentService().updateComment(updatedComment);
1229       List<EventComment> comments = getEventCommentService().getComments(eventId);
1230       getIndexService().updateCommentCatalog(optEvent.get(), comments);
1231       return Response.ok(updatedComment.toJson().toJson()).build();
1232     } catch (Exception e) {
1233       logger.warn("Could not create event comment reply on comment {}", comment, e);
1234       throw new WebApplicationException(e);
1235     }
1236   }
1237 
1238   /**
1239    * Removes emtpy series titles from the collection of the isPartOf Field
1240    * @param ml the list to modify
1241    */
1242   private void removeSeriesWithNullTitlesFromFieldCollection(MetadataList ml) {
1243     // get Series MetadataField from MetadataList
1244     MetadataField seriesField = Optional.ofNullable(ml.getMetadataList().get("dublincore/episode"))
1245             .flatMap(titledMetadataCollection -> Optional.ofNullable(titledMetadataCollection.getCollection()))
1246             .flatMap(dcMetadataCollection -> Optional.ofNullable(dcMetadataCollection.getOutputFields()))
1247             .flatMap(metadataFields -> Optional.ofNullable(metadataFields.get("isPartOf")))
1248             .orElse(null);
1249     if (seriesField == null || seriesField.getCollection() == null) {
1250       return;
1251     }
1252 
1253     // Remove null keys
1254     Map<String, String> seriesCollection = seriesField.getCollection();
1255     seriesCollection.remove(null);
1256     seriesField.setCollection(seriesCollection);
1257 
1258     return;
1259   }
1260 
1261   @GET
1262   @Path("{eventId}/metadata.json")
1263   @Produces(MediaType.APPLICATION_JSON)
1264   @RestQuery(name = "geteventmetadata", description = "Returns all the data related to the metadata tab in the event details modal as JSON", returnDescription = "All the data related to the event metadata tab as JSON", pathParameters = {
1265           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1266                   @RestResponse(description = "Returns all the data related to the event metadata tab as JSON", responseCode = HttpServletResponse.SC_OK),
1267                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1268   public Response getEventMetadata(@PathParam("eventId") String eventId) throws Exception {
1269     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1270     if (optEvent.isEmpty())
1271       return notFound("Cannot find an event with id '%s'.", eventId);
1272     Event event = optEvent.get();
1273     MetadataList metadataList = new MetadataList();
1274 
1275     // Load extended metadata
1276     List<EventCatalogUIAdapter> extendedCatalogUIAdapters = getIndexService().getExtendedEventCatalogUIAdapters();
1277     if (!extendedCatalogUIAdapters.isEmpty()) {
1278       MediaPackage mediaPackage;
1279       try {
1280         mediaPackage = getIndexService().getEventMediapackage(event);
1281       } catch (IndexServiceException e) {
1282         if (e.getCause() instanceof NotFoundException) {
1283           return notFound("Cannot find data for event %s", eventId);
1284         } else if (e.getCause() instanceof UnauthorizedException) {
1285           return Response.status(Status.FORBIDDEN).entity("Not authorized to access " + eventId).build();
1286         }
1287         logger.error("Internal error when trying to access metadata for " + eventId, e);
1288         return serverError();
1289       }
1290 
1291       for (EventCatalogUIAdapter extendedCatalogUIAdapter : extendedCatalogUIAdapters) {
1292         metadataList.add(extendedCatalogUIAdapter, extendedCatalogUIAdapter.getFields(mediaPackage));
1293       }
1294     }
1295 
1296     // Load common metadata
1297     // We do this after extended metadata because we want to overwrite any extended metadata adapters with the same
1298     // flavor instead of the other way around.
1299     EventCatalogUIAdapter eventCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
1300     DublinCoreMetadataCollection metadataCollection = eventCatalogUiAdapter.getRawFields(getCollectionQueryDisable());
1301     EventUtils.setEventMetadataValues(event, metadataCollection);
1302     metadataList.add(eventCatalogUiAdapter, metadataCollection);
1303 
1304     // remove series with empty titles from the collection of the isPartOf field as these can't be converted to json
1305     removeSeriesWithNullTitlesFromFieldCollection(metadataList);
1306 
1307     // lock metadata?
1308     final String wfState = event.getWorkflowState();
1309     if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState)))
1310       metadataList.setLocked(Locked.WORKFLOW_RUNNING);
1311 
1312     return okJson(MetadataJson.listToJson(metadataList, true));
1313   }
1314 
1315   /**
1316    * Create a special query that disables filling the collection of a series, for performance reasons.
1317    * The collection can still be fetched via the listprovider endpoint.
1318    *
1319    * @return a map with resource list queries belonging to metadata fields
1320    */
1321   private Map getCollectionQueryDisable() {
1322     HashMap<String, ResourceListQuery> collectionQueryOverrides = new HashMap();
1323     SeriesListQuery seriesListQuery = new SeriesListQuery();
1324     seriesListQuery.setLimit(0);
1325     collectionQueryOverrides.put(DublinCore.PROPERTY_IS_PART_OF.getLocalName(), seriesListQuery);
1326     return collectionQueryOverrides;
1327   }
1328 
1329   @POST  // use POST instead of GET because of a possibly long list of ids
1330   @Path("events/metadata.json")
1331   @Produces(MediaType.APPLICATION_JSON)
1332   @RestQuery(name = "geteventsmetadata",
1333              description = "Returns all the data related to the edit events metadata modal as JSON",
1334              returnDescription = "All the data related to the edit events metadata modal as JSON",
1335              restParameters = {
1336                @RestParameter(name = "eventIds", description = "The event ids", isRequired = true,
1337                               type = RestParameter.Type.STRING)
1338              }, responses = {
1339                @RestResponse(description = "Returns all the data related to the edit events metadata modal as JSON",
1340                              responseCode = HttpServletResponse.SC_OK),
1341                @RestResponse(description = "No events to update, either not found or with running workflow, "
1342                                          + "details in response body.",
1343                              responseCode = HttpServletResponse.SC_NOT_FOUND)
1344              })
1345   public Response getEventsMetadata(@FormParam("eventIds") String eventIds) throws Exception {
1346     if (StringUtils.isBlank(eventIds)) {
1347       return badRequest("Event ids can't be empty");
1348     }
1349 
1350     JSONParser parser = new JSONParser();
1351     List<String> ids;
1352     try {
1353       ids = (List<String>) parser.parse(eventIds);
1354     } catch (org.json.simple.parser.ParseException e) {
1355       logger.error("Unable to parse '{}'", eventIds, e);
1356       return badRequest("Unable to parse event ids");
1357     } catch (ClassCastException e) {
1358       logger.error("Unable to cast '{}'", eventIds, e);
1359       return badRequest("Unable to parse event ids");
1360     }
1361 
1362     Set<String> eventsNotFound = new HashSet();
1363     Set<String> eventsWithRunningWorkflow = new HashSet();
1364     Set<String> eventsMerged = new HashSet();
1365 
1366     // collect the metadata of all events
1367     List<DublinCoreMetadataCollection> collectedMetadata = new ArrayList();
1368     for (String eventId: ids) {
1369       Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1370       // not found?
1371       if (optEvent.isEmpty()) {
1372         eventsNotFound.add(eventId);
1373         continue;
1374       }
1375 
1376       Event event = optEvent.get();
1377 
1378       // check if there's a running workflow
1379       final String wfState = event.getWorkflowState();
1380       if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
1381         eventsWithRunningWorkflow.add(eventId);
1382         continue;
1383       }
1384 
1385       // collect metadata
1386       EventCatalogUIAdapter eventCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
1387       DublinCoreMetadataCollection metadataCollection = eventCatalogUiAdapter.getRawFields(
1388             getCollectionQueryDisable());
1389       EventUtils.setEventMetadataValues(event, metadataCollection);
1390       collectedMetadata.add(metadataCollection);
1391 
1392       eventsMerged.add(eventId);
1393     }
1394 
1395     // no events found?
1396     if (collectedMetadata.isEmpty()) {
1397       JsonObject response = new JsonObject();
1398       response.add("notFound", collectionToJsonArray(eventsNotFound));
1399       response.add("runningWorkflow", collectionToJsonArray(eventsWithRunningWorkflow));
1400       return Response.status(Status.NOT_FOUND).type(MediaType.APPLICATION_JSON_TYPE).build();
1401     }
1402 
1403     // merge metadata of events
1404     DublinCoreMetadataCollection mergedMetadata;
1405     if (collectedMetadata.size() == 1) {
1406       mergedMetadata = collectedMetadata.get(0);
1407     }
1408     else {
1409       //use first metadata collection as base
1410       mergedMetadata = new DublinCoreMetadataCollection(collectedMetadata.get(0));
1411       collectedMetadata.remove(0);
1412 
1413       for (MetadataField field : mergedMetadata.getFields()) {
1414         for (DublinCoreMetadataCollection otherMetadataCollection : collectedMetadata) {
1415           MetadataField matchingField = otherMetadataCollection.getOutputFields().get(field.getOutputID());
1416 
1417           // check if fields have the same value
1418           if (!Objects.equals(field.getValue(), matchingField.getValue())) {
1419             field.setDifferentValues();
1420             break;
1421           }
1422         }
1423       }
1424     }
1425 
1426     JsonObject result = new JsonObject();
1427     result.add("metadata", MetadataJson.collectionToJson(mergedMetadata, true));
1428     result.add("notFound", collectionToJsonArray(eventsNotFound));
1429     result.add("runningWorkflow", collectionToJsonArray(eventsWithRunningWorkflow));
1430     result.add("merged", collectionToJsonArray(eventsMerged));
1431 
1432     return okJson(result);
1433   }
1434 
1435   @PUT
1436   @Path("bulk/update")
1437   @RestQuery(name = "bulkupdate", description = "Update all of the given events at once", restParameters = {
1438     @RestParameter(name = "update", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of groups with events and fields to update.")}, responses = {
1439     @RestResponse(description = "All events have been updated successfully.", responseCode = HttpServletResponse.SC_OK),
1440     @RestResponse(description = "Could not parse update instructions.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1441     @RestResponse(description = "Field updating metadata or scheduling information. Some events may have been updated. Details are available in the response body.", responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR),
1442     @RestResponse(description = "The events in the response body were not found. No events were updated.", responseCode = HttpServletResponse.SC_NOT_FOUND)},
1443     returnDescription = "In case of success, no content is returned. In case of errors while updating the metadata or scheduling information, the errors are returned. In case events were not found, their ids are returned")
1444   public Response bulkUpdate(@FormParam("update") String updateJson) {
1445 
1446     final BulkUpdateUtil.BulkUpdateInstructions instructions;
1447     try {
1448       instructions = new BulkUpdateUtil.BulkUpdateInstructions(updateJson);
1449     } catch (IllegalArgumentException e) {
1450       return badRequest("Cannot parse bulk update instructions");
1451     }
1452 
1453     final Map<String, String> metadataUpdateFailures = new HashMap<>();
1454     final Map<String, String> schedulingUpdateFailures = new HashMap<>();
1455 
1456     for (final BulkUpdateUtil.BulkUpdateInstructionGroup groupInstructions : instructions.getGroups()) {
1457       // Get all the events to edit
1458       final Map<String, Optional<Event>> events = groupInstructions.getEventIds().stream()
1459         .collect(Collectors.toMap(id -> id, id -> BulkUpdateUtil.getEvent(getIndexService(), getIndex(), id)));
1460 
1461       // Check for invalid (non-existing) event ids
1462       final Set<String> notFoundIds = events.entrySet().stream().filter(e -> !e.getValue().isPresent()).map(Entry::getKey).collect(Collectors.toSet());
1463       if (!notFoundIds.isEmpty()) {
1464         return notFoundJson(collectionToJsonArray(notFoundIds));
1465       }
1466 
1467 
1468       events.values().forEach(e -> e.ifPresent(event -> {
1469 
1470         JSONObject metadata = null;
1471 
1472         // Update the scheduling information
1473         try {
1474           if (groupInstructions.getScheduling() != null) {
1475             // Since we only have the start/end time, we have to add the correct date(s) for this event.
1476             final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupInstructions.getScheduling());
1477             updateEventScheduling(scheduling.toJSONString(), event);
1478             // We have to update the non-technical metadata as well to keep them in sync with the technical ones.
1479             metadata = BulkUpdateUtil.toNonTechnicalMetadataJson(scheduling);
1480           }
1481         } catch (Exception exception) {
1482           schedulingUpdateFailures.put(event.getIdentifier(), exception.getMessage());
1483         }
1484 
1485         // Update the event metadata
1486         try {
1487           if (groupInstructions.getMetadata() != null || metadata != null) {
1488             metadata = BulkUpdateUtil.mergeMetadataFields(metadata, groupInstructions.getMetadata());
1489             getIndexService().updateAllEventMetadata(event.getIdentifier(), JSONArray.toJSONString(Collections.singletonList(metadata)), getIndex());
1490           }
1491         } catch (Exception exception) {
1492           metadataUpdateFailures.put(event.getIdentifier(), exception.getMessage());
1493         }
1494       }));
1495     }
1496 
1497     // Check if there were any errors updating the metadata or scheduling information
1498     if (!metadataUpdateFailures.isEmpty() || !schedulingUpdateFailures.isEmpty()) {
1499       JsonObject json = new JsonObject();
1500       json.add("metadataFailures", mapToJsonObject(metadataUpdateFailures));
1501       json.add("schedulingFailures", mapToJsonObject(schedulingUpdateFailures));
1502       return serverErrorJson(json);
1503     }
1504     return ok();
1505   }
1506 
1507   @POST
1508   @Path("bulk/conflicts")
1509   @RestQuery(name = "getBulkConflicts", description = "Checks if the current bulk update scheduling settings are in a conflict with another event", returnDescription = "Returns NO CONTENT if no event are in conflict within specified period or list of conflicting recordings in JSON", restParameters = {
1510     @RestParameter(name = "update", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of events and fields to update.")}, responses = {
1511     @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
1512     @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The events in the response body were not found. No events were updated."),
1513     @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"),
1514     @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters")})
1515   public Response getBulkConflicts(@FormParam("update") final String updateJson) throws NotFoundException {
1516     final BulkUpdateUtil.BulkUpdateInstructions instructions;
1517     try {
1518       instructions = new BulkUpdateUtil.BulkUpdateInstructions(updateJson);
1519     } catch (IllegalArgumentException e) {
1520       return badRequest("Cannot parse bulk update instructions");
1521     }
1522 
1523     final Map<String, List<JsonObject>> conflicts = new HashMap<>();
1524     final List<Tuple3<String, Optional<Event>, JSONObject>> eventsWithSchedulingOpt = instructions.getGroups().stream()
1525         .flatMap(group -> group.getEventIds().stream().map(eventId -> Tuple3
1526             .tuple3(eventId, BulkUpdateUtil.getEvent(getIndexService(), getIndex(), eventId), group.getScheduling())))
1527         .collect(Collectors.toList());
1528     // Check for invalid (non-existing) event ids
1529     final Set<String> notFoundIds = eventsWithSchedulingOpt.stream().filter(e -> !e.getB().isPresent())
1530         .map(Tuple3::getA).collect(Collectors.toSet());
1531     if (!notFoundIds.isEmpty()) {
1532       return notFoundJson(collectionToJsonArray(notFoundIds));
1533     }
1534     final List<Tuple<Event, JSONObject>> eventsWithScheduling = eventsWithSchedulingOpt.stream()
1535         .map(e -> Tuple.tuple(e.getB().get(), e.getC())).collect(Collectors.toList());
1536     final Set<String> changedIds = eventsWithScheduling.stream().map(e -> e.getA().getIdentifier())
1537         .collect(Collectors.toSet());
1538     for (final Tuple<Event, JSONObject> eventWithGroup : eventsWithScheduling) {
1539       final Event event = eventWithGroup.getA();
1540       final JSONObject groupScheduling = eventWithGroup.getB();
1541       try {
1542         if (groupScheduling != null) {
1543           // Since we only have the start/end time, we have to add the correct date(s) for this event.
1544           final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupScheduling);
1545           final Date start = Date.from(Instant.parse((String) scheduling.get(SCHEDULING_START_KEY)));
1546           final Date end = Date.from(Instant.parse((String) scheduling.get(SCHEDULING_END_KEY)));
1547           final String agentId = Optional.ofNullable((String) scheduling.get(SCHEDULING_AGENT_ID_KEY))
1548               .orElse(event.getAgentId());
1549 
1550           final List<JsonObject> currentConflicts = new ArrayList<>();
1551 
1552           // Check for conflicts between the events themselves
1553           eventsWithScheduling.stream()
1554               .filter(otherEvent -> !otherEvent.getA().getIdentifier().equals(event.getIdentifier()))
1555               .forEach(otherEvent -> {
1556                 final JSONObject otherScheduling = BulkUpdateUtil.addSchedulingDates(otherEvent.getA(), otherEvent.getB());
1557                 final Date otherStart = Date.from(Instant.parse((String) otherScheduling.get(SCHEDULING_START_KEY)));
1558                 final Date otherEnd = Date.from(Instant.parse((String) otherScheduling.get(SCHEDULING_END_KEY)));
1559                 final String otherAgentId = Optional.ofNullable((String) otherScheduling.get(SCHEDULING_AGENT_ID_KEY))
1560                     .orElse(otherEvent.getA().getAgentId());
1561                 if (!otherAgentId.equals(agentId)) {
1562                   // different agent -> no conflict
1563                   return;
1564                 }
1565                 if (Util.schedulingIntervalsOverlap(start, end, otherStart, otherEnd)) {
1566                   // conflict
1567                   currentConflicts.add(convertEventToConflictingObject(
1568                       DateTimeSupport.toUTC(otherStart.getTime()),
1569                       DateTimeSupport.toUTC(otherEnd.getTime()),
1570                       otherEvent.getA().getTitle()));
1571                 }
1572               });
1573 
1574           // Check for conflicts with other events from the database
1575           final List<MediaPackage> conflicting = getSchedulerService().findConflictingEvents(agentId, start, end)
1576               .stream()
1577               .filter(mp -> !changedIds.contains(mp.getIdentifier().toString()))
1578               .collect(Collectors.toList());
1579           if (!conflicting.isEmpty()) {
1580             currentConflicts.addAll(convertToConflictObjects(event.getIdentifier(), conflicting));
1581           }
1582           conflicts.put(event.getIdentifier(), currentConflicts);
1583         }
1584       } catch (final SchedulerException | UnauthorizedException | SearchIndexException exception) {
1585         throw new RuntimeException(exception);
1586       }
1587     }
1588 
1589     if (!conflicts.isEmpty()) {
1590       JsonArray responseJson = new JsonArray();
1591 
1592       conflicts.forEach((eventId, conflictingEvents) -> {
1593         if (!conflictingEvents.isEmpty()) {
1594           JsonObject obj = new JsonObject();
1595           obj.addProperty("eventId", eventId);
1596 
1597           JsonArray conflictsArray = new JsonArray();
1598           for (JsonObject conflict : conflictingEvents) {
1599             conflictsArray.add(conflict);
1600           }
1601 
1602           obj.add("conflicts", conflictsArray);
1603           responseJson.add(obj);
1604         }
1605       });
1606 
1607       if (responseJson.size() > 0) {
1608         return conflictJson(responseJson);
1609       }
1610     }
1611 
1612     return noContent();
1613   }
1614 
1615   @PUT
1616   @Path("{eventId}/metadata")
1617   @RestQuery(name = "updateeventmetadata", description = "Update the passed metadata for the event with the given Id", pathParameters = {
1618           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
1619                   @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of metadata to update") }, responses = {
1620                           @RestResponse(description = "The metadata have been updated.", responseCode = HttpServletResponse.SC_OK),
1621                           @RestResponse(description = "Could not parse metadata.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1622                           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "No content is returned.")
1623   public Response updateEventMetadata(@PathParam("eventId") String id, @FormParam("metadata") String metadataJSON)
1624           throws Exception {
1625     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1626     if (optEvent.isEmpty())
1627       return notFound("Cannot find an event with id '%s'.", id);
1628 
1629     try {
1630       MetadataList metadataList = getIndexService().updateAllEventMetadata(id, metadataJSON, getIndex());
1631       return okJson(MetadataJson.listToJson(metadataList, true));
1632     } catch (IllegalArgumentException e) {
1633       return badRequest(String.format("Event %s metadata can't be updated.: %s", id, e.getMessage()));
1634     }
1635   }
1636 
1637   @PUT
1638   @Path("events/metadata")
1639   @RestQuery(name = "updateeventsmetadata",
1640     description = "Update the passed metadata for the events with the given ids",
1641     restParameters = {
1642       @RestParameter(name = "eventIds", isRequired = true, type = RestParameter.Type.STRING,
1643         description = "The ids of the events to update"),
1644       @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT,
1645         description = "The metadata fields to update"),
1646     }, responses = {
1647     @RestResponse(description = "All events have been updated successfully.",
1648       responseCode = HttpServletResponse.SC_NO_CONTENT),
1649     @RestResponse(description = "One or multiple errors occured while updating event metadata. "
1650       + "Some events may have been updated successfully. "
1651       + "Details are available in the response body.",
1652       responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)},
1653     returnDescription = "In case of complete success, no content is returned. Otherwise, the response content "
1654       + "contains the ids of events that couldn't be found and the ids and errors of events where the update failed "
1655       + "as well as the ids of the events that were updated successfully.")
1656   public Response updateEventsMetadata(@FormParam("eventIds") String eventIds, @FormParam("metadata") String metadata)
1657     throws Exception {
1658 
1659     if (StringUtils.isBlank(eventIds)) {
1660       return badRequest("Event ids can't be empty");
1661     }
1662 
1663     JSONParser parser = new JSONParser();
1664     List<String> ids;
1665     try {
1666       ids = (List<String>) parser.parse(eventIds);
1667     } catch (org.json.simple.parser.ParseException e) {
1668       logger.error("Unable to parse '{}'", eventIds, e);
1669       return badRequest("Unable to parse event ids");
1670     } catch (ClassCastException e) {
1671       logger.error("Unable to cast '{}'", eventIds, e);
1672       return badRequest("Unable to parse event ids");
1673     }
1674 
1675     // try to update each event
1676     Set<String> eventsNotFound = new HashSet<>();
1677     Set<String> eventsUpdated = new HashSet<>();
1678     Set<String> eventsUpdateFailure = new HashSet();
1679 
1680     for (String eventId : ids) {
1681       Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1682       // not found?
1683 
1684       if (optEvent.isEmpty()) {
1685         eventsNotFound.add(eventId);
1686         continue;
1687       }
1688 
1689       // update
1690       try {
1691         getIndexService().updateAllEventMetadata(eventId, metadata, getIndex());
1692         eventsUpdated.add(eventId);
1693       } catch (IllegalArgumentException e) {
1694         eventsUpdateFailure.add(eventId);
1695       }
1696     }
1697 
1698     // errors occurred?
1699     if (!eventsNotFound.isEmpty() || !eventsUpdateFailure.isEmpty()) {
1700       JsonObject errorJson = new JsonObject();
1701 
1702       errorJson.add("updateFailures", collectionToJsonArray(eventsUpdateFailure));
1703       errorJson.add("notFound", collectionToJsonArray(eventsNotFound));
1704       errorJson.add("updated", collectionToJsonArray(eventsUpdated));
1705 
1706       return serverErrorJson(errorJson);
1707     }
1708 
1709     return noContent();
1710   }
1711 
1712   @GET
1713   @Path("{eventId}/asset/assets.json")
1714   @Produces(MediaType.APPLICATION_JSON)
1715   @RestQuery(name = "getAssetList", description = "Returns the number of assets from each types as JSON", returnDescription = "The number of assets from each types as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1716           @RestResponse(description = "Returns the number of assets from each types as JSON", responseCode = HttpServletResponse.SC_OK),
1717           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1718   public Response getAssetList(@PathParam("eventId") String id) throws Exception {
1719     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1720     if (optEvent.isEmpty())
1721       return notFound("Cannot find an event with id '%s'.", id);
1722     MediaPackage mp;
1723     try {
1724       mp = getIndexService().getEventMediapackage(optEvent.get());
1725     } catch (IndexServiceException e) {
1726       if (e.getCause() instanceof NotFoundException) {
1727         return notFound("Cannot find data for event %s", id);
1728       } else if (e.getCause() instanceof UnauthorizedException) {
1729         return Response.status(Status.FORBIDDEN).entity("Not authorized to access " + id).build();
1730       }
1731       logger.error("Internal error when trying to access metadata for " + id, e);
1732       return serverError();
1733     }
1734     int attachments = mp.getAttachments().length;
1735     int catalogs = mp.getCatalogs().length;
1736     int media = mp.getTracks().length;
1737     int publications = mp.getPublications().length;
1738 
1739     JsonObject result = new JsonObject();
1740     result.addProperty("attachments", attachments);
1741     result.addProperty("catalogs", catalogs);
1742     result.addProperty("media", media);
1743     result.addProperty("publications", publications);
1744 
1745     return okJson(result);
1746   }
1747 
1748   @GET
1749   @Path("{eventId}/asset/attachment/attachments.json")
1750   @Produces(MediaType.APPLICATION_JSON)
1751   @RestQuery(name = "getAttachmentsList", description = "Returns a list of attachments from the given event as JSON", returnDescription = "The list of attachments from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1752           @RestResponse(description = "Returns a list of attachments from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1753           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1754   public Response getAttachmentsList(@PathParam("eventId") String id) throws Exception {
1755     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1756     if (optEvent.isEmpty())
1757       return notFound("Cannot find an event with id '%s'.", id);
1758     MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1759     return okJson(getEventMediaPackageElements(mp.getAttachments()));
1760   }
1761 
1762   @GET
1763   @Path("{eventId}/asset/attachment/{id}.json")
1764   @Produces(MediaType.APPLICATION_JSON)
1765   @RestQuery(name = "getAttachment", description = "Returns the details of an attachment from the given event and attachment id as JSON", returnDescription = "The details of an attachment from the given event and attachment id as JSON", pathParameters = {
1766           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1767           @RestParameter(name = "id", description = "The attachment id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1768           @RestResponse(description = "Returns the details of an attachment from the given event and attachment id as JSON", responseCode = HttpServletResponse.SC_OK),
1769           @RestResponse(description = "No event or attachment with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1770   public Response getAttachment(@PathParam("eventId") String eventId, @PathParam("id") String id)
1771           throws NotFoundException, SearchIndexException, IndexServiceException {
1772     MediaPackage mp = getMediaPackageByEventId(eventId);
1773 
1774     Attachment attachment = mp.getAttachment(id);
1775     if (attachment == null)
1776       return notFound("Cannot find an attachment with id '%s'.", id);
1777     return okJson(attachmentToJSON(attachment));
1778   }
1779 
1780   @GET
1781   @Path("{eventId}/asset/catalog/catalogs.json")
1782   @Produces(MediaType.APPLICATION_JSON)
1783   @RestQuery(name = "getCatalogList", description = "Returns a list of catalogs from the given event as JSON", returnDescription = "The list of catalogs from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1784           @RestResponse(description = "Returns a list of catalogs from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1785           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1786   public Response getCatalogList(@PathParam("eventId") String id) throws Exception {
1787     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1788     if (optEvent.isEmpty())
1789       return notFound("Cannot find an event with id '%s'.", id);
1790     MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1791     return okJson(getEventMediaPackageElements(mp.getCatalogs()));
1792   }
1793 
1794   @GET
1795   @Path("{eventId}/asset/catalog/{id}.json")
1796   @Produces(MediaType.APPLICATION_JSON)
1797   @RestQuery(name = "getCatalog", description = "Returns the details of a catalog from the given event and catalog id as JSON", returnDescription = "The details of a catalog from the given event and catalog id as JSON", pathParameters = {
1798           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1799           @RestParameter(name = "id", description = "The catalog id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1800           @RestResponse(description = "Returns the details of a catalog from the given event and catalog id as JSON", responseCode = HttpServletResponse.SC_OK),
1801           @RestResponse(description = "No event or catalog with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1802   public Response getCatalog(@PathParam("eventId") String eventId, @PathParam("id") String id)
1803           throws NotFoundException, SearchIndexException, IndexServiceException {
1804     MediaPackage mp = getMediaPackageByEventId(eventId);
1805 
1806     Catalog catalog = mp.getCatalog(id);
1807     if (catalog == null)
1808       return notFound("Cannot find a catalog with id '%s'.", id);
1809     return okJson(catalogToJSON(catalog));
1810   }
1811 
1812   @GET
1813   @Path("{eventId}/asset/media/media.json")
1814   @Produces(MediaType.APPLICATION_JSON)
1815   @RestQuery(name = "getMediaList", description = "Returns a list of media from the given event as JSON", returnDescription = "The list of media from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1816           @RestResponse(description = "Returns a list of media from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1817           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1818   public Response getMediaList(@PathParam("eventId") String id) throws Exception {
1819     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1820     if (optEvent.isEmpty())
1821       return notFound("Cannot find an event with id '%s'.", id);
1822     MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1823     return okJson(getEventMediaPackageElements(mp.getTracks()));
1824   }
1825 
1826   @GET
1827   @Path("{eventId}/asset/media/{id}.json")
1828   @Produces(MediaType.APPLICATION_JSON)
1829   @RestQuery(name = "getMedia", description = "Returns the details of a media from the given event and media id as JSON", returnDescription = "The details of a media from the given event and media id as JSON", pathParameters = {
1830           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1831           @RestParameter(name = "id", description = "The media id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1832                   @RestResponse(description = "Returns the media of a catalog from the given event and media id as JSON", responseCode = HttpServletResponse.SC_OK),
1833                   @RestResponse(description = "No event or media with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1834   public Response getMedia(@PathParam("eventId") String eventId, @PathParam("id") String id)
1835           throws NotFoundException, SearchIndexException, IndexServiceException {
1836     MediaPackage mp = getMediaPackageByEventId(eventId);
1837 
1838     Track track = mp.getTrack(id);
1839     if (track == null)
1840       return notFound("Cannot find media with id '%s'.", id);
1841     return okJson(trackToJSON(track));
1842   }
1843 
1844   @GET
1845   @Path("{eventId}/asset/publication/publications.json")
1846   @Produces(MediaType.APPLICATION_JSON)
1847   @RestQuery(name = "getPublicationList", description = "Returns a list of publications from the given event as JSON", returnDescription = "The list of publications from the given event as JSON", pathParameters = { @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1848           @RestResponse(description = "Returns a list of publications from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1849           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1850   public Response getPublicationList(@PathParam("eventId") String id) throws Exception {
1851     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1852     if (optEvent.isEmpty())
1853       return notFound("Cannot find an event with id '%s'.", id);
1854     MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1855     return okJson(getEventPublications(mp.getPublications()));
1856   }
1857 
1858   @GET
1859   @Path("{eventId}/asset/publication/{id}.json")
1860   @Produces(MediaType.APPLICATION_JSON)
1861   @RestQuery(name = "getPublication", description = "Returns the details of a publication from the given event and publication id as JSON", returnDescription = "The details of a publication from the given event and publication id as JSON", pathParameters = {
1862           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1863           @RestParameter(name = "id", description = "The publication id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1864           @RestResponse(description = "Returns the publication of a catalog from the given event and publication id as JSON", responseCode = HttpServletResponse.SC_OK),
1865           @RestResponse(description = "No event or publication with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1866   public Response getPublication(@PathParam("eventId") String eventId, @PathParam("id") String id)
1867           throws NotFoundException, SearchIndexException, IndexServiceException {
1868     MediaPackage mp = getMediaPackageByEventId(eventId);
1869 
1870     Publication publication = null;
1871     for (Publication p : mp.getPublications()) {
1872       if (id.equals(p.getIdentifier())) {
1873         publication = p;
1874         break;
1875       }
1876     }
1877 
1878     if (publication == null)
1879       return notFound("Cannot find publication with id '%s'.", id);
1880     return okJson(publicationToJSON(publication));
1881   }
1882 
1883   @GET
1884   @Path("{eventId}/tobira/pages")
1885   @RestQuery(
1886           name = "getEventHostPages",
1887           description = "Returns the pages of a connected Tobira instance that contain the given event",
1888           returnDescription = "The Tobira pages that contain the given event",
1889           pathParameters = {
1890                   @RestParameter(
1891                           name = "eventId",
1892                           isRequired = true,
1893                           description = "The event identifier",
1894                           type = STRING
1895                   ),
1896           },
1897           responses = {
1898                   @RestResponse(
1899                           responseCode = SC_OK,
1900                           description = "The Tobira pages containing the given event"
1901                   ),
1902                   @RestResponse(
1903                           responseCode = SC_NOT_FOUND,
1904                           description = "Tobira doesn't know about the given event"
1905                   ),
1906                   @RestResponse(
1907                           responseCode = SC_SERVICE_UNAVAILABLE,
1908                           description = "Tobira is not configured (correctly)"
1909                   ),
1910           }
1911   )
1912   public Response getEventHostPages(@PathParam("eventId") String eventId) {
1913     var tobira = TobiraService.getTobira(getSecurityService().getOrganization().getId());
1914     if (!tobira.ready()) {
1915       return Response.status(Status.SERVICE_UNAVAILABLE)
1916               .entity("Tobira is not configured (correctly)")
1917               .build();
1918     }
1919 
1920     try {
1921       var eventData = tobira.getEventHostPages(eventId);
1922       if (eventData == null) {
1923         throw new WebApplicationException(NOT_FOUND);
1924       }
1925       eventData.put("baseURL", tobira.getOrigin());
1926       return Response.ok(eventData.toJSONString()).build();
1927     } catch (TobiraException e) {
1928       throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
1929     }
1930   }
1931 
1932   @GET
1933   @Path("{eventId}/workflows.json")
1934   @Produces(MediaType.APPLICATION_JSON)
1935   @RestQuery(name = "geteventworkflows", description = "Returns all the data related to the workflows tab in the event details modal as JSON", returnDescription = "All the data related to the event workflows tab as JSON", pathParameters = {
1936           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1937                   @RestResponse(description = "Returns all the data related to the event workflows tab as JSON", responseCode = HttpServletResponse.SC_OK),
1938                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1939   public Response getEventWorkflows(@PathParam("eventId") String id)
1940           throws UnauthorizedException, SearchIndexException, JobEndpointException {
1941     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
1942     if (optEvent.isEmpty())
1943       return notFound("Cannot find an event with id '%s'.", id);
1944 
1945     try {
1946       if (optEvent.get().getEventStatus().equals("EVENTS.EVENTS.STATUS.SCHEDULED")) {
1947         Map<String, String> workflowConfig = getSchedulerService().getWorkflowConfig(id);
1948         JsonObject configJson = new JsonObject();
1949         for (Map.Entry<String, String> entry : workflowConfig.entrySet()) {
1950           configJson.addProperty(entry.getKey(), safeString(entry.getValue()));
1951         }
1952 
1953         Map<String, String> agentConfiguration = getSchedulerService().getCaptureAgentConfiguration(id);
1954         JsonObject responseJson = new JsonObject();
1955         responseJson.addProperty("workflowId", agentConfiguration.getOrDefault(CaptureParameters.INGEST_WORKFLOW_DEFINITION, ""));
1956         responseJson.add("configuration", configJson);
1957 
1958         return okJson(responseJson);
1959       } else {
1960         List<WorkflowInstance> workflowInstances = getWorkflowService().getWorkflowInstancesByMediaPackage(id);
1961         JsonArray jsonArray = new JsonArray();
1962 
1963         for (WorkflowInstance instance : workflowInstances) {
1964           JsonObject instanceJson = new JsonObject();
1965           instanceJson.addProperty("id", instance.getId());
1966           instanceJson.addProperty("title", safeString(instance.getTitle()));
1967           instanceJson.addProperty("status", WORKFLOW_STATUS_TRANSLATION_PREFIX + instance.getState().toString());
1968 
1969           Date created = instance.getDateCreated();
1970           instanceJson.addProperty("submitted", created != null ? DateTimeSupport.toUTC(created.getTime()) : "");
1971 
1972           String submitter = instance.getCreatorName();
1973           instanceJson.addProperty("submitter", safeString(submitter));
1974 
1975           User user = submitter == null ? null : getUserDirectoryService().loadUser(submitter);
1976           String submitterName = null;
1977           String submitterEmail = null;
1978           if (user != null) {
1979             submitterName = user.getName();
1980             submitterEmail = user.getEmail();
1981           }
1982           instanceJson.addProperty("submitterName", safeString(submitterName));
1983           instanceJson.addProperty("submitterEmail", safeString(submitterEmail));
1984 
1985           jsonArray.add(instanceJson);
1986         }
1987 
1988         JsonObject result = new JsonObject();
1989         result.add("results", jsonArray);
1990         result.addProperty("count", workflowInstances.size());
1991 
1992         return okJson(result);
1993       }
1994     } catch (NotFoundException e) {
1995       return notFound("Cannot find workflows for event %s", id);
1996     } catch (SchedulerException e) {
1997       logger.error("Unable to get workflow data for event with id {}", id);
1998       throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
1999     } catch (WorkflowDatabaseException e) {
2000       throw new JobEndpointException(String.format("Not able to get the list of job from the database: %s", e),
2001               e.getCause());
2002     }
2003   }
2004 
2005   @PUT
2006   @Path("{eventId}/workflows")
2007   @RestQuery(name = "updateEventWorkflow", description = "Update the workflow configuration for the scheduled event with the given id", pathParameters = {
2008           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
2009                   @RestParameter(name = "configuration", isRequired = true, description = "The workflow configuration as JSON", type = RestParameter.Type.TEXT) }, responses = {
2010                           @RestResponse(description = "Request executed succesfully", responseCode = HttpServletResponse.SC_NO_CONTENT),
2011                           @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "The method does not retrun any content.")
2012   public Response updateEventWorkflow(@PathParam("eventId") String id, @FormParam("configuration") String configuration)
2013           throws SearchIndexException, UnauthorizedException {
2014     Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
2015     if (optEvent.isEmpty())
2016       return notFound("Cannot find an event with id '%s'.", id);
2017 
2018     if (optEvent.get().isScheduledEvent() && !optEvent.get().hasRecordingStarted()) {
2019       try {
2020 
2021         JSONObject configJSON;
2022         try {
2023           configJSON = (JSONObject) new JSONParser().parse(configuration);
2024         } catch (Exception e) {
2025           logger.warn("Unable to parse the workflow configuration {}", configuration);
2026           return badRequest();
2027         }
2028 
2029         Optional<Map<String, String>> caMetadataOpt = Optional.empty();
2030         Optional<Map<String, String>> workflowConfigOpt = Optional.empty();
2031 
2032         String workflowId = (String) configJSON.get("id");
2033         Map<String, String> caMetadata = new HashMap<>(getSchedulerService().getCaptureAgentConfiguration(id));
2034         if (!workflowId.equals(caMetadata.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION))) {
2035           caMetadata.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowId);
2036           caMetadataOpt = Optional.of(caMetadata);
2037         }
2038 
2039         Map<String, String> workflowConfig = new HashMap<>((JSONObject) configJSON.get("configuration"));
2040         Map<String, String> oldWorkflowConfig = new HashMap<>(getSchedulerService().getWorkflowConfig(id));
2041         if (!oldWorkflowConfig.equals(workflowConfig))
2042           workflowConfigOpt = Optional.of(workflowConfig);
2043 
2044         if (caMetadataOpt.isEmpty() && workflowConfigOpt.isEmpty())
2045           return Response.noContent().build();
2046 
2047         checkAgentAccessForAgent(optEvent.get().getAgentId());
2048 
2049         getSchedulerService().updateEvent(id, Optional.empty(), Optional.empty(), Optional.empty(),
2050             Optional.empty(), Optional.empty(), workflowConfigOpt, caMetadataOpt);
2051         return Response.noContent().build();
2052       } catch (NotFoundException e) {
2053         return notFound("Cannot find event %s in scheduler service", id);
2054       } catch (SchedulerException e) {
2055         logger.error("Unable to update scheduling workflow data for event with id {}", id);
2056         throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
2057       }
2058     } else {
2059       return badRequest(String.format("Event %s workflow can not be updated as the recording already started.", id));
2060     }
2061   }
2062 
2063   @GET
2064   @Path("{eventId}/workflows/{workflowId}")
2065   @Produces(MediaType.APPLICATION_JSON)
2066   @RestQuery(name = "geteventworkflow", description = "Returns all the data related to the single workflow tab in the event details modal as JSON", returnDescription = "All the data related to the event singe workflow tab as JSON", pathParameters = {
2067           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2068           @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2069                   @RestResponse(description = "Returns all the data related to the event single workflow tab as JSON", responseCode = HttpServletResponse.SC_OK),
2070                   @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2071                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2072   public Response getEventWorkflow(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId)
2073       throws SearchIndexException {
2074     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2075     if (optEvent.isEmpty()) {
2076       return notFound("Cannot find an event with id '%s'.", eventId);
2077     }
2078 
2079     long workflowInstanceId;
2080     try {
2081       workflowId = StringUtils.remove(workflowId, ".json");
2082       workflowInstanceId = Long.parseLong(workflowId);
2083     } catch (Exception e) {
2084       logger.warn("Unable to parse workflow id {}", workflowId);
2085       return RestUtil.R.badRequest();
2086     }
2087 
2088     try {
2089       WorkflowInstance instance = getWorkflowService().getWorkflowById(workflowInstanceId);
2090       // Retrieve submission date with the workflow instance main job
2091       Date created = instance.getDateCreated();
2092       Date completed = instance.getDateCompleted();
2093       if (completed == null)
2094         completed = new Date();
2095 
2096       long executionTime = completed.getTime() - created.getTime();
2097 
2098       JsonObject configurationObj = new JsonObject();
2099       for (Entry<String, String> entry : instance.getConfigurations().entrySet()) {
2100         configurationObj.addProperty(entry.getKey(), safeString(entry.getValue()));
2101       }
2102 
2103       JsonObject json = new JsonObject();
2104       json.addProperty("status", WORKFLOW_STATUS_TRANSLATION_PREFIX + instance.getState());
2105       json.addProperty("description", safeString(instance.getDescription()));
2106       json.addProperty("executionTime", executionTime);
2107       json.addProperty("wiid", instance.getId());
2108       json.addProperty("title", safeString(instance.getTitle()));
2109       json.addProperty("wdid", safeString(instance.getTemplate()));
2110       if (!configurationObj.isEmpty()) {
2111         json.add("configuration", configurationObj);
2112       }
2113       json.addProperty("submittedAt", DateTimeSupport.toUTC(created.getTime()));
2114       json.addProperty("creator", safeString(instance.getCreatorName()));
2115 
2116       return okJson(json);
2117 
2118     } catch (NotFoundException e) {
2119       return notFound("Cannot find workflow  %s", workflowId);
2120     } catch (WorkflowDatabaseException e) {
2121       logger.error("Unable to get workflow {} of event {}", workflowId, eventId, e);
2122       return serverError();
2123     } catch (UnauthorizedException e) {
2124       return forbidden();
2125     }
2126   }
2127 
2128   @GET
2129   @Path("{eventId}/workflows/{workflowId}/operations.json")
2130   @Produces(MediaType.APPLICATION_JSON)
2131   @RestQuery(name = "geteventoperations", description = "Returns all the data related to the workflow/operations tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/opertations tab as JSON", pathParameters = {
2132           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2133           @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2134                   @RestResponse(description = "Returns all the data related to the event workflow/operations tab as JSON", responseCode = HttpServletResponse.SC_OK),
2135                   @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2136                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2137   public Response getEventOperations(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId)
2138       throws SearchIndexException {
2139     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2140     if (optEvent.isEmpty()) {
2141       return notFound("Cannot find an event with id '%s'.", eventId);
2142     }
2143 
2144     long workflowInstanceId;
2145     try {
2146       workflowInstanceId = Long.parseLong(workflowId);
2147     } catch (Exception e) {
2148       logger.warn("Unable to parse workflow id {}", workflowId);
2149       return RestUtil.R.badRequest();
2150     }
2151 
2152     try {
2153       WorkflowInstance instance = getWorkflowService().getWorkflowById(workflowInstanceId);
2154       List<WorkflowOperationInstance> operations = instance.getOperations();
2155       JsonArray operationsJsonArray = new JsonArray();
2156 
2157       for (WorkflowOperationInstance wflOp : operations) {
2158         JsonObject operationJson = new JsonObject();
2159         operationJson.addProperty("status", WORKFLOW_STATUS_TRANSLATION_PREFIX + wflOp.getState());
2160         operationJson.addProperty("title", safeString(wflOp.getTemplate()));
2161         operationJson.addProperty("description", safeString(wflOp.getDescription()));
2162         operationJson.addProperty("id", wflOp.getId());
2163         if (!wflOp.getConfigurationKeys().isEmpty()) {
2164           operationJson.add("configuration", collectionToJsonArray(wflOp.getConfigurationKeys()));
2165         }
2166         operationsJsonArray.add(operationJson);
2167       }
2168 
2169       return okJson(operationsJsonArray);
2170     } catch (NotFoundException e) {
2171       return notFound("Cannot find workflow %s", workflowId);
2172     } catch (WorkflowDatabaseException e) {
2173       logger.error("Unable to get workflow operations of event {} and workflow {}", eventId, workflowId, e);
2174       return serverError();
2175     } catch (UnauthorizedException e) {
2176       return forbidden();
2177     }
2178   }
2179 
2180   @GET
2181   @Path("{eventId}/workflows/{workflowId}/operations/{operationPosition}")
2182   @Produces(MediaType.APPLICATION_JSON)
2183   @RestQuery(name = "geteventoperation", description = "Returns all the data related to the workflow/operation tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/opertation tab as JSON", pathParameters = {
2184           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2185           @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING),
2186           @RestParameter(name = "operationPosition", description = "The operation position", isRequired = true, type = RestParameter.Type.INTEGER) }, responses = {
2187                   @RestResponse(description = "Returns all the data related to the event workflow/operation tab as JSON", responseCode = HttpServletResponse.SC_OK),
2188                   @RestResponse(description = "Unable to parse workflowId or operationPosition", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2189                   @RestResponse(description = "No operation with these identifiers was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2190   public Response getEventOperation(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
2191       @PathParam("operationPosition") Integer operationPosition) throws SearchIndexException {
2192     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2193     if (optEvent.isEmpty()) {
2194       return notFound("Cannot find an event with id '%s'.", eventId);
2195     }
2196 
2197     long workflowInstanceId;
2198     try {
2199       workflowInstanceId = Long.parseLong(workflowId);
2200     } catch (Exception e) {
2201       logger.warn("Unable to parse workflow id {}", workflowId);
2202       return RestUtil.R.badRequest();
2203     }
2204 
2205     WorkflowInstance instance;
2206     try {
2207       instance = getWorkflowService().getWorkflowById(workflowInstanceId);
2208     } catch (NotFoundException e) {
2209       return notFound("Cannot find workflow %s", workflowId);
2210     } catch (WorkflowDatabaseException e) {
2211       logger.error("Unable to get workflow operation of event {} and workflow {} at position {}", eventId, workflowId,
2212           operationPosition, e);
2213       return serverError();
2214     } catch (UnauthorizedException e) {
2215       return forbidden();
2216     }
2217 
2218     List<WorkflowOperationInstance> operations = instance.getOperations();
2219 
2220     if (operationPosition < operations.size()) {
2221       WorkflowOperationInstance wflOp = operations.get(operationPosition);
2222       JsonObject json = new JsonObject();
2223 
2224       json.addProperty("retry_strategy", wflOp.getRetryStrategy() != null ? wflOp.getRetryStrategy().toString() : "");
2225       json.addProperty("execution_host", safeString(wflOp.getExecutionHost()));
2226       json.addProperty("failed_attempts", wflOp.getFailedAttempts());
2227       json.addProperty("max_attempts", wflOp.getMaxAttempts());
2228       json.addProperty("exception_handler_workflow", safeString(wflOp.getExceptionHandlingWorkflow()));
2229       json.addProperty("fail_on_error", wflOp.isFailOnError());
2230       json.addProperty("description", safeString(wflOp.getDescription()));
2231       json.addProperty("state", WORKFLOW_STATUS_TRANSLATION_PREFIX + wflOp.getState());
2232       json.addProperty("job", wflOp.getId());
2233       json.addProperty("name", safeString(wflOp.getTemplate()));
2234       json.addProperty("time_in_queue", wflOp.getTimeInQueue() != null ? wflOp.getTimeInQueue() : 0);
2235       json.addProperty("started", wflOp.getDateStarted() != null ? toUTC(wflOp.getDateStarted().getTime()) : "");
2236       json.addProperty("completed", wflOp.getDateCompleted() != null ? toUTC(wflOp.getDateCompleted().getTime()) : "");
2237 
2238       return okJson(json);
2239     }
2240 
2241     return notFound("Cannot find workflow operation of workflow %s at position %s", workflowId, operationPosition);
2242   }
2243 
2244   @GET
2245   @Path("{eventId}/workflows/{workflowId}/errors.json")
2246   @Produces(MediaType.APPLICATION_JSON)
2247   @RestQuery(name = "geteventerrors", description = "Returns all the data related to the workflow/errors tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/errors tab as JSON", pathParameters = {
2248           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2249           @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2250                   @RestResponse(description = "Returns all the data related to the event workflow/errors tab as JSON", responseCode = HttpServletResponse.SC_OK),
2251                   @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2252                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2253   public Response getEventErrors(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
2254           @Context HttpServletRequest req) throws JobEndpointException, SearchIndexException {
2255     // the call to #getEvent should make sure that the calling user has access rights to the workflow
2256     // FIXME since there is no dependency between the event and the workflow (the fetched event is
2257     // simply ignored) an attacker can get access by using an event he owns and a workflow ID of
2258     // someone else.
2259     Optional<Event> eventOpt = getIndexService().getEvent(eventId, getIndex());
2260     if (eventOpt.isPresent()) {
2261       final long workflowIdLong;
2262       try {
2263         workflowIdLong = Long.parseLong(workflowId);
2264       } catch (Exception e) {
2265         logger.warn("Unable to parse workflow id {}", workflowId);
2266         return RestUtil.R.badRequest();
2267       }
2268       try {
2269         return okJson(getJobService().getIncidentsAsJSON(workflowIdLong, req.getLocale(), true));
2270       } catch (NotFoundException e) {
2271         return notFound("Cannot find the incident for the workflow %s", workflowId);
2272       }
2273     }
2274     return notFound("Cannot find an event with id '%s'.", eventId);
2275   }
2276 
2277   @GET
2278   @Path("{eventId}/workflows/{workflowId}/errors/{errorId}.json")
2279   @Produces(MediaType.APPLICATION_JSON)
2280   @RestQuery(name = "geteventerror", description = "Returns all the data related to the workflow/error tab in the event details modal as JSON", returnDescription = "All the data related to the event workflow/error tab as JSON", pathParameters = {
2281           @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2282           @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING),
2283           @RestParameter(name = "errorId", description = "The error id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2284                   @RestResponse(description = "Returns all the data related to the event workflow/error tab as JSON", responseCode = HttpServletResponse.SC_OK),
2285                   @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2286                   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2287   public Response getEventError(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
2288           @PathParam("errorId") String errorId, @Context HttpServletRequest req)
2289                   throws JobEndpointException, SearchIndexException {
2290     // the call to #getEvent should make sure that the calling user has access rights to the workflow
2291     // FIXME since there is no dependency between the event and the workflow (the fetched event is
2292     // simply ignored) an attacker can get access by using an event he owns and a workflow ID of
2293     // someone else.
2294     Optional<Event> eventOpt = getIndexService().getEvent(eventId, getIndex());
2295     if (eventOpt.isPresent()) {
2296       final long errorIdLong;
2297       try {
2298         errorIdLong = Long.parseLong(errorId);
2299       } catch (Exception e) {
2300         logger.warn("Unable to parse error id {}", errorId);
2301         return RestUtil.R.badRequest();
2302       }
2303       try {
2304         return okJson(getJobService().getIncidentAsJSON(errorIdLong, req.getLocale()));
2305       } catch (NotFoundException e) {
2306         return notFound("Cannot find the incident %s", errorId);
2307       }
2308     }
2309     return notFound("Cannot find an event with id '%s'.", eventId);
2310   }
2311 
2312   @GET
2313   @Path("{eventId}/access.json")
2314   @SuppressWarnings("unchecked")
2315   @Produces(MediaType.APPLICATION_JSON)
2316   @RestQuery(name = "getEventAccessInformation", description = "Get the access information of an event", returnDescription = "The access information", pathParameters = {
2317           @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, responses = {
2318                   @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the request."),
2319                   @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."),
2320                   @RestResponse(responseCode = SC_OK, description = "The access information ") })
2321   public Response getEventAccessInformation(@PathParam("eventId") String eventId) throws Exception {
2322     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2323     if (optEvent.isEmpty())
2324       return notFound("Cannot find an event with id '%s'.", eventId);
2325 
2326     // Add all available ACLs to the response
2327     JSONArray systemAclsJson = new JSONArray();
2328     List<ManagedAcl> acls = getAclService().getAcls();
2329     for (ManagedAcl acl : acls) {
2330       systemAclsJson.add(AccessInformationUtil.serializeManagedAcl(acl));
2331     }
2332 
2333     AccessControlList activeAcl = new AccessControlList();
2334     try {
2335       if (optEvent.get().getAccessPolicy() != null)
2336         activeAcl = AccessControlParser.parseAcl(optEvent.get().getAccessPolicy());
2337     } catch (Exception e) {
2338       logger.error("Unable to parse access policy", e);
2339     }
2340     Optional<ManagedAcl> currentAcl = AccessInformationUtil.matchAclsLenient(acls, activeAcl,
2341             getAdminUIConfiguration().getMatchManagedAclRolePrefixes());
2342 
2343     JSONObject episodeAccessJson = new JSONObject();
2344     episodeAccessJson.put("current_acl", currentAcl.isPresent() ? currentAcl.get().getId() : 0L);
2345     episodeAccessJson.put("acl", transformAccessControList(activeAcl, getUserDirectoryService()));
2346     episodeAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(activeAcl));
2347     if (StringUtils.isNotBlank(optEvent.get().getWorkflowState())
2348             && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(optEvent.get().getWorkflowState())))
2349       episodeAccessJson.put("locked", true);
2350 
2351     JSONObject jsonReturnObj = new JSONObject();
2352     jsonReturnObj.put("episode_access", episodeAccessJson);
2353     jsonReturnObj.put("system_acls", systemAclsJson);
2354 
2355     return Response.ok(jsonReturnObj.toString()).build();
2356   }
2357 
2358   // MH-12085 Add manually uploaded assets, multipart file upload has to be a POST
2359   @POST
2360   @Path("{eventId}/assets")
2361   @Consumes(MediaType.MULTIPART_FORM_DATA)
2362   @RestQuery(name = "updateAssets", description = "Update or create an asset for the eventId by the given metadata as JSON and files in the body",
2363   pathParameters = {
2364   @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) },
2365   restParameters = {
2366   @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of asset metadata") },
2367   responses = {
2368   @RestResponse(description = "The asset has been added.", responseCode = HttpServletResponse.SC_OK),
2369   @RestResponse(description = "Could not add asset, problem with the metadata or files.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2370   @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) },
2371   returnDescription = "The workflow identifier")
2372   public Response updateAssets(@PathParam("eventId") final String eventId,
2373           @Context HttpServletRequest request)  throws Exception {
2374     try {
2375       MediaPackage mp = getMediaPackageByEventId(eventId);
2376       String result = getIndexService().updateEventAssets(mp, request);
2377       return Response.status(Status.CREATED).entity(result).build();
2378     }  catch (NotFoundException e) {
2379       return notFound("Cannot find an event with id '%s'.", eventId);
2380     } catch (IllegalArgumentException | UnsupportedAssetException e) {
2381       return RestUtil.R.badRequest(e.getMessage());
2382     } catch (Exception e) {
2383       return RestUtil.R.serverError();
2384     }
2385   }
2386 
2387   @GET
2388   @Path("new/metadata")
2389   @RestQuery(name = "getNewMetadata", description = "Returns all the data related to the metadata tab in the new event modal as JSON", returnDescription = "All the data related to the event metadata tab as JSON", responses = {
2390           @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event metadata tab as JSON") })
2391   public Response getNewMetadata() {
2392     MetadataList metadataList = new MetadataList();
2393 
2394     // Extended metadata
2395     List<EventCatalogUIAdapter> extendedCatalogUIAdapters = getIndexService().getExtendedEventCatalogUIAdapters();
2396     for (EventCatalogUIAdapter extendedCatalogUIAdapter : extendedCatalogUIAdapters) {
2397       metadataList.add(extendedCatalogUIAdapter, extendedCatalogUIAdapter.getRawFields());
2398     }
2399 
2400     // Common metadata
2401     // We do this after extended metadata because we want to overwrite any extended metadata adapters with the same
2402     // flavor instead of the other way around.
2403     EventCatalogUIAdapter commonCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
2404     DublinCoreMetadataCollection commonMetadata = commonCatalogUiAdapter.getRawFields(getCollectionQueryDisable());
2405 
2406     if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_CREATED.getLocalName()))
2407       commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName()));
2408     if (commonMetadata.getOutputFields().containsKey("duration"))
2409       commonMetadata.removeField(commonMetadata.getOutputFields().get("duration"));
2410     if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_IDENTIFIER.getLocalName()))
2411       commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_IDENTIFIER.getLocalName()));
2412     if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_SOURCE.getLocalName()))
2413       commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_SOURCE.getLocalName()));
2414     if (commonMetadata.getOutputFields().containsKey("startDate"))
2415       commonMetadata.removeField(commonMetadata.getOutputFields().get("startDate"));
2416     if (commonMetadata.getOutputFields().containsKey("startTime"))
2417       commonMetadata.removeField(commonMetadata.getOutputFields().get("startTime"));
2418     if (commonMetadata.getOutputFields().containsKey("location"))
2419       commonMetadata.removeField(commonMetadata.getOutputFields().get("location"));
2420 
2421     // Set publisher to user
2422     if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_PUBLISHER.getLocalName())) {
2423       MetadataField publisher = commonMetadata.getOutputFields().get(DublinCore.PROPERTY_PUBLISHER.getLocalName());
2424       Map<String, String> users = new HashMap<>();
2425       if (publisher.getCollection() != null) {
2426         users = publisher.getCollection();
2427       }
2428       String loggedInUser = getSecurityService().getUser().getName();
2429       if (!users.containsKey(loggedInUser)) {
2430         users.put(loggedInUser, loggedInUser);
2431       }
2432       publisher.setValue(loggedInUser);
2433     }
2434 
2435     metadataList.add(commonCatalogUiAdapter, commonMetadata);
2436 
2437     // remove series with empty titles from the collection of the isPartOf field as these can't be converted to json
2438     removeSeriesWithNullTitlesFromFieldCollection(metadataList);
2439 
2440     return okJson(MetadataJson.listToJson(metadataList, true));
2441   }
2442 
2443   @GET
2444   @Path("new/processing")
2445   @RestQuery(name = "getNewProcessing", description = "Returns all the data related to the processing tab in the new event modal as JSON", returnDescription = "All the data related to the event processing tab as JSON", restParameters = {
2446           @RestParameter(name = "tags", isRequired = false, description = "A comma separated list of tags to filter the workflow definitions", type = RestParameter.Type.STRING) }, responses = {
2447                   @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event processing tab as JSON") })
2448   public Response getNewProcessing(@QueryParam("tags") String tagsString) {
2449     List<String> tags = RestUtil.splitCommaSeparatedParam(Optional.ofNullable(tagsString));
2450 
2451     JsonArray workflowsArray = new JsonArray();
2452     try {
2453       List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions();
2454       for (WorkflowDefinition wflDef : workflowsDefinitions) {
2455         if (wflDef.containsTag(tags)) {
2456           JsonObject wfJson = new JsonObject();
2457           wfJson.addProperty("id", wflDef.getId());
2458           wfJson.add("tags", arrayToJsonArray(wflDef.getTags()));
2459           wfJson.addProperty("title", safeString(wflDef.getTitle()));
2460           wfJson.addProperty("description", safeString(wflDef.getDescription()));
2461           wfJson.addProperty("displayOrder", wflDef.getDisplayOrder());
2462           wfJson.addProperty("configuration_panel", safeString(wflDef.getConfigurationPanel()));
2463           wfJson.addProperty("configuration_panel_json", safeString(wflDef.getConfigurationPanelJson()));
2464 
2465           workflowsArray.add(wfJson);
2466         }
2467       }
2468     } catch (WorkflowDatabaseException e) {
2469       logger.error("Unable to get available workflow definitions", e);
2470       return RestUtil.R.serverError();
2471     }
2472 
2473     JsonObject data = new JsonObject();
2474     data.add("workflows", workflowsArray);
2475     data.addProperty("default_workflow_id", defaultWorkflowDefinionId);
2476 
2477     return okJson(data);
2478   }
2479 
2480   @POST
2481   @Path("new/conflicts")
2482   @RestQuery(name = "checkNewConflicts", description = "Checks if the current scheduler parameters are in a conflict with another event", returnDescription = "Returns NO CONTENT if no event are in conflict within specified period or list of conflicting recordings in JSON", restParameters = {
2483           @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, responses = {
2484                   @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
2485                   @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"),
2486                   @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters") })
2487   public Response getNewConflicts(@FormParam("metadata") String metadata) throws NotFoundException {
2488     if (StringUtils.isBlank(metadata)) {
2489       logger.warn("Metadata is not specified");
2490       return Response.status(Status.BAD_REQUEST).build();
2491     }
2492 
2493     JSONParser parser = new JSONParser();
2494     JSONObject metadataJson;
2495     try {
2496       metadataJson = (JSONObject) parser.parse(metadata);
2497     } catch (Exception e) {
2498       logger.warn("Unable to parse metadata {}", metadata);
2499       return RestUtil.R.badRequest("Unable to parse metadata");
2500     }
2501 
2502     String device;
2503     String startDate;
2504     String endDate;
2505     try {
2506       device = (String) metadataJson.get("device");
2507       startDate = (String) metadataJson.get("start");
2508       endDate = (String) metadataJson.get("end");
2509     } catch (Exception e) {
2510       logger.warn("Unable to parse metadata {}", metadata);
2511       return RestUtil.R.badRequest("Unable to parse metadata");
2512     }
2513 
2514     if (StringUtils.isBlank(device) || StringUtils.isBlank(startDate) || StringUtils.isBlank(endDate)) {
2515       logger.warn("Either device, start date or end date were not specified");
2516       return Response.status(Status.BAD_REQUEST).build();
2517     }
2518 
2519     Date start;
2520     try {
2521       start = new Date(DateTimeSupport.fromUTC(startDate));
2522     } catch (Exception e) {
2523       logger.warn("Unable to parse start date {}", startDate);
2524       return RestUtil.R.badRequest("Unable to parse start date");
2525     }
2526 
2527     Date end;
2528     try {
2529       end = new Date(DateTimeSupport.fromUTC(endDate));
2530     } catch (Exception e) {
2531       logger.warn("Unable to parse end date {}", endDate);
2532       return RestUtil.R.badRequest("Unable to parse end date");
2533     }
2534 
2535     String rruleString = (String) metadataJson.get("rrule");
2536 
2537     RRule rrule = null;
2538     TimeZone timeZone = TimeZone.getDefault();
2539     String durationString = null;
2540     if (StringUtils.isNotEmpty(rruleString)) {
2541       try {
2542         rrule = new RRule(rruleString);
2543         rrule.validate();
2544       } catch (Exception e) {
2545         logger.warn("Unable to parse rrule {}: {}", rruleString, e.getMessage());
2546         return Response.status(Status.BAD_REQUEST).build();
2547       }
2548 
2549       durationString = (String) metadataJson.get("duration");
2550       if (StringUtils.isBlank(durationString)) {
2551         logger.warn("If checking recurrence, must include duration.");
2552         return Response.status(Status.BAD_REQUEST).build();
2553       }
2554 
2555       Agent agent = getCaptureAgentStateService().getAgent(device);
2556       String timezone = agent.getConfiguration().getProperty("capture.device.timezone");
2557       if (StringUtils.isBlank(timezone)) {
2558         timezone = TimeZone.getDefault().getID();
2559         logger.warn("No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.",
2560                 device, timezone);
2561       }
2562       timeZone = TimeZone.getTimeZone(timezone);
2563     }
2564 
2565     String eventId = (String) metadataJson.get("id");
2566 
2567     try {
2568       List<MediaPackage> events = null;
2569       if (StringUtils.isNotEmpty(rruleString)) {
2570         events = getSchedulerService().findConflictingEvents(device, rrule, start, end, Long.parseLong(durationString),
2571                 timeZone);
2572       } else {
2573         events = getSchedulerService().findConflictingEvents(device, start, end);
2574       }
2575       if (!events.isEmpty()) {
2576         final List<JsonObject> eventsJSON = convertToConflictObjects(eventId, events);
2577         if (!eventsJSON.isEmpty()) {
2578           JsonArray jsonArray = new JsonArray();
2579           for (JsonObject jsonObj : eventsJSON) {
2580             jsonArray.add(jsonObj);
2581           }
2582           return conflictJson(jsonArray);
2583         }
2584       }
2585       return Response.noContent().build();
2586     } catch (Exception e) {
2587       logger.error("Unable to find conflicting events for {}, {}, {}",
2588               device, startDate, endDate, e);
2589       return RestUtil.R.serverError();
2590     }
2591   }
2592 
2593   private List<JsonObject> convertToConflictObjects(final String eventId, final List<MediaPackage> events) throws SearchIndexException {
2594     final List<JsonObject> eventsJSON = new ArrayList<>();
2595     final Organization organization = getSecurityService().getOrganization();
2596     final User user = SecurityUtil.createSystemUser(systemUserName, organization);
2597 
2598     SecurityUtil.runAs(getSecurityService(), organization, user, () -> {
2599       try {
2600         for (final MediaPackage event : events) {
2601           final Optional<Event> eventOpt = getIndexService().getEvent(event.getIdentifier().toString(), getIndex());
2602           if (eventOpt.isPresent()) {
2603             final Event e = eventOpt.get();
2604             if (StringUtils.isNotEmpty(eventId) && eventId.equals(e.getIdentifier())) {
2605               continue;
2606             }
2607             eventsJSON.add(convertEventToConflictingObject(e.getTechnicalStartTime(), e.getTechnicalEndTime(), e.getTitle()));
2608           } else {
2609             logger.warn("Index out of sync! Conflicting event catalog {} not found on event index!",
2610               event.getIdentifier().toString());
2611           }
2612         }
2613       } catch (Exception e) {
2614          logger.error("Failed to get conflicting events", e);
2615       }
2616     });
2617 
2618     return eventsJSON;
2619   }
2620 
2621   private JsonObject convertEventToConflictingObject(final String start, final String end, final String title) {
2622     JsonObject json = new JsonObject();
2623     json.addProperty("start", start);
2624     json.addProperty("end", end);
2625     json.addProperty("title", title);
2626     return json;
2627   }
2628 
2629   @POST
2630   @Path("/new")
2631   @Consumes(MediaType.MULTIPART_FORM_DATA)
2632   @RestQuery(name = "createNewEvent", description = "Creates a new event by the given metadata as JSON and the files in the body", returnDescription = "The workflow identifier", restParameters = {
2633           @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, responses = {
2634                   @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event sucessfully added"),
2635                   @RestResponse(responseCode = SC_BAD_REQUEST, description = "If the metadata is not set or couldn't be parsed") })
2636   public Response createNewEvent(@Context HttpServletRequest request) {
2637     try {
2638       String result = getIndexService().createEvent(request);
2639       if (StringUtils.isEmpty(result)) {
2640         return RestUtil.R.badRequest("The date range provided did not include any events");
2641       }
2642       return Response.status(Status.CREATED).entity(result).build();
2643     } catch (IllegalArgumentException | UnsupportedAssetException e) {
2644       return RestUtil.R.badRequest(e.getMessage());
2645     } catch (Exception e) {
2646       return RestUtil.R.serverError();
2647     }
2648   }
2649 
2650   @GET
2651   @Path("events.json")
2652   @Produces(MediaType.APPLICATION_JSON)
2653   @RestQuery(name = "getevents", description = "Returns all the events as JSON", returnDescription = "All the events as JSON", restParameters = {
2654           @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
2655           @RestParameter(name = "sort", description = "The order instructions used to sort the query result. Must be in the form '<field name>:(ASC|DESC)'", isRequired = false, type = STRING),
2656           @RestParameter(name = "limit", description = "The maximum number of items to return per page.", isRequired = false, type = RestParameter.Type.INTEGER),
2657           @RestParameter(name = "offset", description = "The page number.", isRequired = false, type = RestParameter.Type.INTEGER),
2658           @RestParameter(name = "getComments", description = "If comments should be fetched", isRequired = false, type = RestParameter.Type.BOOLEAN) }, responses = {
2659                   @RestResponse(description = "Returns all events as JSON", responseCode = HttpServletResponse.SC_OK) })
2660   public Response getEvents(@QueryParam("id") String id, @QueryParam("commentReason") String reasonFilter,
2661           @QueryParam("commentResolution") String resolutionFilter, @QueryParam("filter") String filter,
2662           @QueryParam("sort") String sort, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit,
2663           @QueryParam("getComments") Boolean getComments) {
2664 
2665     Optional<Integer> optLimit = Optional.ofNullable(limit);
2666     Optional<Integer> optOffset = Optional.ofNullable(offset);
2667     Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
2668     Optional<Boolean> optGetComments = Optional.ofNullable(getComments);
2669     List<JsonObject> eventsList = new ArrayList<>();
2670     final Organization organization = getSecurityService().getOrganization();
2671     final User user = getSecurityService().getUser();
2672     if (organization == null || user == null) {
2673       return Response.status(SC_SERVICE_UNAVAILABLE).build();
2674     }
2675     EventSearchQuery query = new EventSearchQuery(organization.getId(), user);
2676 
2677     // If the limit is set to 0, this is not taken into account
2678     if (optLimit.isPresent() && limit == 0) {
2679       optLimit = Optional.empty();
2680     }
2681 
2682     Map<String, String> filters = RestUtils.parseFilter(filter);
2683     for (String name : filters.keySet()) {
2684       if (EventListQuery.FILTER_PRESENTERS_BIBLIOGRAPHIC_NAME.equals(name))
2685         query.withPresenter(filters.get(name));
2686       if (EventListQuery.FILTER_PRESENTERS_TECHNICAL_NAME.equals(name))
2687         query.withTechnicalPresenters(filters.get(name));
2688       if (EventListQuery.FILTER_CONTRIBUTORS_NAME.equals(name))
2689         query.withContributor(filters.get(name));
2690       if (EventListQuery.FILTER_LOCATION_NAME.equals(name))
2691         query.withLocation(filters.get(name));
2692       if (EventListQuery.FILTER_AGENT_NAME.equals(name))
2693         query.withAgentId(filters.get(name));
2694       if (EventListQuery.FILTER_TEXT_NAME.equals(name))
2695         query.withText(filters.get(name));
2696       if (EventListQuery.FILTER_SERIES_NAME.equals(name))
2697         query.withSeriesId(filters.get(name));
2698       if (EventListQuery.FILTER_STATUS_NAME.equals(name))
2699         query.withEventStatus(filters.get(name));
2700       if (EventListQuery.FILTER_PUBLISHER_NAME.equals(name))
2701         query.withPublisher(filters.get(name));
2702       if (EventListQuery.FILTER_COMMENTS_NAME.equals(name)) {
2703         switch (Comments.valueOf(filters.get(name))) {
2704           case NONE:
2705             query.withComments(false);
2706             break;
2707           case OPEN:
2708             query.withOpenComments(true);
2709             break;
2710           case RESOLVED:
2711             query.withComments(true);
2712             query.withOpenComments(false);
2713             break;
2714           default:
2715             logger.info("Unknown comment {}", filters.get(name));
2716             return Response.status(SC_BAD_REQUEST).build();
2717         }
2718       }
2719       if (EventListQuery.FILTER_IS_PUBLISHED_NAME.equals(name)) {
2720         if (filters.containsKey(name)) {
2721           switch (IsPublished.valueOf(filters.get(name))) {
2722             case YES:
2723               query.withIsPublished(true);
2724               break;
2725             case NO:
2726               query.withIsPublished(false);
2727               break;
2728             default:
2729               break;
2730           }
2731         } else {
2732           logger.info("Query for invalid published status: {}", filters.get(name));
2733           return Response.status(SC_BAD_REQUEST).build();
2734         }
2735       }
2736       if (EventListQuery.FILTER_STARTDATE_NAME.equals(name)) {
2737         try {
2738           Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(filters.get(name));
2739           query.withStartFrom(fromAndToCreationRange.getA());
2740           query.withStartTo(fromAndToCreationRange.getB());
2741         } catch (IllegalArgumentException e) {
2742           return RestUtil.R.badRequest(e.getMessage());
2743         }
2744       }
2745     }
2746 
2747     if (optSort.isPresent()) {
2748       ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
2749       for (SortCriterion criterion : sortCriteria) {
2750         switch (criterion.getFieldName()) {
2751           case EventIndexSchema.UID:
2752             query.sortByUID(criterion.getOrder());
2753             break;
2754           case EventIndexSchema.TITLE:
2755             query.sortByTitle(criterion.getOrder());
2756             break;
2757           case EventIndexSchema.PRESENTER:
2758             query.sortByPresenter(criterion.getOrder());
2759             break;
2760           case EventIndexSchema.TECHNICAL_START:
2761           case "technical_date":
2762             query.sortByTechnicalStartDate(criterion.getOrder());
2763             break;
2764           case EventIndexSchema.TECHNICAL_END:
2765             query.sortByTechnicalEndDate(criterion.getOrder());
2766             break;
2767           case EventIndexSchema.PUBLICATION:
2768             query.sortByPublicationIgnoringInternal(criterion.getOrder());
2769             break;
2770           case EventIndexSchema.START_DATE:
2771           case "date":
2772             query.sortByStartDate(criterion.getOrder());
2773             break;
2774           case EventIndexSchema.END_DATE:
2775             query.sortByEndDate(criterion.getOrder());
2776             break;
2777           case EventIndexSchema.SERIES_NAME:
2778             query.sortBySeriesName(criterion.getOrder());
2779             break;
2780           case EventIndexSchema.LOCATION:
2781             query.sortByLocation(criterion.getOrder());
2782             break;
2783           case EventIndexSchema.EVENT_STATUS:
2784             query.sortByEventStatus(criterion.getOrder());
2785             break;
2786           default:
2787             final String msg = String.format("Unknown sort criteria field %s", criterion.getFieldName());
2788             logger.debug(msg);
2789             return RestUtil.R.badRequest(msg);
2790         }
2791       }
2792     }
2793 
2794     // We search for write actions
2795     if (getOnlyEventsWithWriteAccessEventsTab()) {
2796       query.withoutActions();
2797       query.withAction(Permissions.Action.WRITE);
2798       query.withAction(Permissions.Action.READ);
2799     }
2800 
2801     if (optLimit.isPresent())
2802       query.withLimit(optLimit.get());
2803     if (optOffset.isPresent())
2804       query.withOffset(offset);
2805     // TODO: Add other filters to the query
2806 
2807     SearchResult<Event> results = null;
2808     try {
2809       results = getIndex().getByQuery(query);
2810     } catch (SearchIndexException e) {
2811       logger.error("The admin UI Search Index was not able to get the events list:", e);
2812       return RestUtil.R.serverError();
2813     }
2814 
2815     // If the results list if empty, we return already a response.
2816     if (results.getPageSize() == 0) {
2817       logger.debug("No events match the given filters.");
2818       return okJsonList(eventsList, Optional.ofNullable(offset).orElse(0), Optional.ofNullable(limit).orElse(0), 0);
2819     }
2820 
2821     for (SearchResultItem<Event> item : results.getItems()) {
2822       Event source = item.getSource();
2823       source.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
2824       List<EventComment> comments = null;
2825       if (optGetComments.isPresent() && optGetComments.get()) {
2826         try {
2827           comments = getEventCommentService().getComments(source.getIdentifier());
2828         } catch (EventCommentException e) {
2829           logger.error("Unable to get comments from event {}", source.getIdentifier(), e);
2830           throw new WebApplicationException(e);
2831         }
2832       }
2833       eventsList.add(eventToJSON(source, Optional.ofNullable(comments)));
2834     }
2835 
2836     return okJsonList(eventsList, Optional.ofNullable(offset).orElse(0), Optional.ofNullable(limit).orElse(0), results.getHitCount());
2837   }
2838 
2839   // --
2840 
2841   private MediaPackage getMediaPackageByEventId(String eventId)
2842           throws SearchIndexException, NotFoundException, IndexServiceException {
2843     Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2844     if (optEvent.isEmpty())
2845       throw new NotFoundException(format("Cannot find an event with id '%s'.", eventId));
2846     return getIndexService().getEventMediapackage(optEvent.get());
2847   }
2848 
2849   private URI getCommentUrl(String eventId, long commentId) {
2850     return UrlSupport.uri(serverUrl, eventId, "comment", Long.toString(commentId));
2851   }
2852 
2853 
2854   private JsonObject eventToJSON(Event event, Optional<List<EventComment>> comments) {
2855     JsonObject json = new JsonObject();
2856 
2857     json.addProperty("id", event.getIdentifier());
2858     json.addProperty("title", event.getTitle() != null ? event.getTitle() : "");
2859     json.addProperty("source", event.getSource() != null ? event.getSource() : "");
2860     json.add("presenters", collectionToJsonArray(event.getPresenters()));
2861 
2862     if (StringUtils.isNotBlank(event.getSeriesId())) {
2863       JsonObject seriesObj = new JsonObject();
2864       seriesObj.addProperty("id", event.getSeriesId() != null ? event.getSeriesId() : "");
2865       seriesObj.addProperty("title", event.getSeriesName() != null ? event.getSeriesName() : "");
2866       json.add("series", seriesObj);
2867     }
2868 
2869     json.addProperty("location", safeString(event.getLocation()));
2870     json.addProperty("start_date", safeString(event.getRecordingStartDate()));
2871     json.addProperty("end_date", safeString(event.getRecordingEndDate()));
2872     json.addProperty("managedAcl", safeString(event.getManagedAcl()));
2873     json.addProperty("workflow_state", safeString(event.getWorkflowState()));
2874     json.addProperty("event_status", event.getEventStatus());
2875     json.addProperty("displayable_status", event.getDisplayableStatus(getWorkflowService().getWorkflowStateMappings()));
2876     json.addProperty("source", getIndexService().getEventSource(event).toString());
2877     json.addProperty("has_comments", event.hasComments());
2878     json.addProperty("has_open_comments", event.hasOpenComments());
2879     json.addProperty("needs_cutting", event.needsCutting());
2880     json.addProperty("has_preview", event.hasPreview());
2881     json.addProperty("agent_id", safeString(event.getAgentId()));
2882     json.addProperty("technical_start", safeString(event.getTechnicalStartTime()));
2883     json.addProperty("technical_end", safeString(event.getTechnicalEndTime()));
2884     json.add("technical_presenters", collectionToJsonArray(event.getTechnicalPresenters()));
2885     json.add("publications", collectionToJsonArray(eventPublicationsToJson(event)));
2886     if (comments.isPresent()) {
2887       json.add("comments", collectionToJsonArray(eventCommentsToJson(comments.get())));
2888     }
2889 
2890     return json;
2891   }
2892 
2893 
2894 
2895   private void mergeJsonObjects(JsonObject target, JsonObject source) {
2896     for (String key : source.keySet()) {
2897       target.add(key, source.get(key));
2898     }
2899   }
2900 
2901   private JsonObject attachmentToJSON(Attachment attachment) {
2902     JsonObject json = new JsonObject();
2903     mergeJsonObjects(json, getEventMediaPackageElementFields(attachment));
2904     mergeJsonObjects(json, getCommonElementFields(attachment));
2905     return json;
2906   }
2907 
2908   private JsonObject catalogToJSON(Catalog catalog) {
2909     JsonObject json = new JsonObject();
2910     mergeJsonObjects(json, getEventMediaPackageElementFields(catalog));
2911     mergeJsonObjects(json, getCommonElementFields(catalog));
2912     return json;
2913   }
2914 
2915   private JsonObject trackToJSON(Track track) {
2916     JsonObject json = new JsonObject();
2917     mergeJsonObjects(json, getEventMediaPackageElementFields(track));
2918     mergeJsonObjects(json, getCommonElementFields(track));
2919     json.addProperty("duration", track.getDuration());
2920     json.addProperty("has_audio", track.hasAudio());
2921     json.addProperty("has_video", track.hasVideo());
2922     json.addProperty("has_subtitle", track.hasSubtitle());
2923     json.add("streams", streamsToJSON(track.getStreams()));
2924     return json;
2925   }
2926 
2927   private JsonObject streamsToJSON(org.opencastproject.mediapackage.Stream[] streams) {
2928     JsonArray audioArray = new JsonArray();
2929     JsonArray videoArray = new JsonArray();
2930     JsonArray subtitleArray = new JsonArray();
2931 
2932     for (org.opencastproject.mediapackage.Stream stream : streams) {
2933       if (stream instanceof AudioStreamImpl) {
2934         AudioStream audioStream = (AudioStream) stream;
2935         JsonObject audioJson = new JsonObject();
2936         audioJson.addProperty("id", safeString(audioStream.getIdentifier()));
2937         audioJson.addProperty("type", safeString(audioStream.getFormat()));
2938         audioJson.addProperty("channels", safeString(audioStream.getChannels()));
2939         audioJson.addProperty("bitrate", audioStream.getBitRate());
2940         audioJson.addProperty("bitdepth", safeString(audioStream.getBitDepth()));
2941         audioJson.addProperty("samplingrate", safeString(audioStream.getSamplingRate()));
2942         audioJson.addProperty("framecount", safeString(audioStream.getFrameCount()));
2943         audioJson.addProperty("peakleveldb", safeString(audioStream.getPkLevDb()));
2944         audioJson.addProperty("rmsleveldb", safeString(audioStream.getRmsLevDb()));
2945         audioJson.addProperty("rmspeakdb", safeString(audioStream.getRmsPkDb()));
2946         audioArray.add(audioJson);
2947 
2948       } else if (stream instanceof VideoStreamImpl) {
2949         VideoStream videoStream = (VideoStream) stream;
2950         JsonObject videoJson = new JsonObject();
2951         videoJson.addProperty("id", safeString(videoStream.getIdentifier()));
2952         videoJson.addProperty("type", safeString(videoStream.getFormat()));
2953         videoJson.addProperty("bitrate", videoStream.getBitRate());
2954         videoJson.addProperty("framerate", safeString(videoStream.getFrameRate()));
2955         videoJson.addProperty("resolution", safeString(videoStream.getFrameWidth() + "x" + videoStream.getFrameHeight()));
2956         videoJson.addProperty("framecount", safeString(videoStream.getFrameCount()));
2957         videoJson.addProperty("scantype", safeString(videoStream.getScanType()));
2958         videoJson.addProperty("scanorder", safeString(videoStream.getScanOrder()));
2959         videoArray.add(videoJson);
2960 
2961       } else if (stream instanceof SubtitleStreamImpl) {
2962         SubtitleStreamImpl subtitleStream = (SubtitleStreamImpl) stream;
2963         JsonObject subtitleJson = new JsonObject();
2964         subtitleJson.addProperty("id", safeString(subtitleStream.getIdentifier()));
2965         subtitleJson.addProperty("type", safeString(subtitleStream.getFormat()));
2966         subtitleArray.add(subtitleJson);
2967 
2968       } else {
2969         throw new IllegalArgumentException("Stream must be either audio, video, or subtitle");
2970       }
2971     }
2972 
2973     JsonObject result = new JsonObject();
2974     result.add("audio", audioArray);
2975     result.add("video", videoArray);
2976     result.add("subtitle", subtitleArray);
2977     return result;
2978   }
2979 
2980   private JsonObject publicationToJSON(Publication publication) {
2981     JsonObject json = new JsonObject();
2982 
2983     json.addProperty("id", safeString(publication.getIdentifier()));
2984     json.addProperty("channel", safeString(publication.getChannel()));
2985     json.addProperty("mimetype", safeString(publication.getMimeType()));
2986     json.add("tags", arrayToJsonArray(publication.getTags()));
2987     URI uri = signUrl(publication.getURI());
2988     json.addProperty("url", safeString(uri));
2989 
2990     JsonObject commonFields = getCommonElementFields(publication);
2991     for (String key : commonFields.keySet()) {
2992       json.add(key, commonFields.get(key));
2993     }
2994 
2995     return json;
2996   }
2997 
2998   private JsonObject getCommonElementFields(MediaPackageElement element) {
2999     JsonObject fields = new JsonObject();
3000 
3001     fields.addProperty("size", element.getSize());
3002     fields.addProperty("checksum", element.getChecksum() != null ? element.getChecksum().getValue() : "");
3003     fields.addProperty("reference", element.getReference() != null ? element.getReference().getIdentifier() : "");
3004 
3005     return fields;
3006   }
3007 
3008   /**
3009    * Render an array of {@link Publication}s into a list of JSON values.
3010    *
3011    * @param publications
3012    *          The elements to pull the data from to create the {@link JsonArray}
3013    * @return {@link JsonArray} that represent the {@link Publication}
3014    */
3015   private JsonArray getEventPublications(Publication[] publications) {
3016     JsonArray publicationJsonArray = new JsonArray();
3017 
3018     for (Publication publication : publications) {
3019       JsonObject pubJson = new JsonObject();
3020 
3021       pubJson.addProperty("id", safeString(publication.getIdentifier()));
3022       pubJson.addProperty("channel", safeString(publication.getChannel()));
3023       pubJson.addProperty("mimetype", safeString(publication.getMimeType()));
3024       pubJson.add("tags", arrayToJsonArray(publication.getTags()));
3025       pubJson.addProperty("url",  safeString(signUrl(publication.getURI())));
3026 
3027       publicationJsonArray.add(pubJson);
3028     }
3029 
3030     return publicationJsonArray;
3031   }
3032 
3033   private URI signUrl(URI url) {
3034     if (url == null) {
3035       return null;
3036     }
3037     if (getUrlSigningService().accepts(url.toString())) {
3038       try {
3039         String clientIP = null;
3040         if (signWithClientIP()) {
3041           clientIP = getSecurityService().getUserIP();
3042         }
3043         return URI.create(getUrlSigningService().sign(url.toString(), getUrlSigningExpireDuration(), null, clientIP));
3044       } catch (UrlSigningException e) {
3045         logger.warn("Unable to sign url '{}'", url, e);
3046       }
3047     }
3048     return url;
3049   }
3050 
3051   /**
3052    * Render an array of {@link MediaPackageElement}s into a list of JSON values.
3053    *
3054    * @param elements
3055    *          The elements to pull the data from to create the {@link JsonArray}
3056    * @return {@link JsonArray} that represent the {@link MediaPackageElement}
3057    */
3058   private JsonArray getEventMediaPackageElements(MediaPackageElement[] elements) {
3059     JsonArray elementJsonArray = new JsonArray();
3060     for (MediaPackageElement element : elements) {
3061       JsonObject elementJson = getEventMediaPackageElementFields(element);
3062       elementJsonArray.add(elementJson);
3063     }
3064     return elementJsonArray;
3065   }
3066 
3067   private JsonObject getEventMediaPackageElementFields(MediaPackageElement element) {
3068     JsonObject json = new JsonObject();
3069 
3070     json.addProperty("id", safeString(element.getIdentifier()));
3071     json.addProperty("type", safeString(element.getFlavor()));
3072     json.addProperty("mimetype", safeString(element.getMimeType()));
3073     json.add("tags", arrayToJsonArray(element.getTags()));
3074     json.addProperty("url", safeString(signUrl(element.getURI())));
3075 
3076     return json;
3077   }
3078 
3079   private final Function<Publication, JsonObject> publicationToJson = publication -> {
3080     String channelName = EventUtils.PUBLICATION_CHANNELS.get(publication.getChannel());
3081     if (channelName == null) {
3082       channelName = "EVENTS.EVENTS.DETAILS.PUBLICATIONS.CUSTOM";
3083     }
3084     String url = publication.getURI() == null ? "" : signUrl(publication.getURI()).toString();
3085 
3086     JsonObject json = new JsonObject();
3087     json.addProperty("id", publication.getChannel());
3088     json.addProperty("name", channelName);
3089     json.addProperty("url", url);
3090 
3091     return json;
3092   };
3093 
3094   private JsonObject technicalMetadataToJson(TechnicalMetadata technicalMetadata) {
3095     JsonObject json = new JsonObject();
3096 
3097     json.addProperty("agentId", technicalMetadata.getAgentId() != null ? technicalMetadata.getAgentId() : "");
3098     if (technicalMetadata.getCaptureAgentConfiguration() != null) {
3099       json.add("agentConfiguration", mapToJsonObject(technicalMetadata.getCaptureAgentConfiguration()));
3100     } else {
3101       json.add("agentConfiguration", JsonNull.INSTANCE);
3102     }
3103     if (technicalMetadata.getStartDate() != null) {
3104       String startUtc = DateTimeSupport.toUTC(technicalMetadata.getStartDate().getTime());
3105       json.addProperty("start", startUtc);
3106     } else {
3107       json.addProperty("start", "");
3108     }
3109     if (technicalMetadata.getEndDate() != null) {
3110       String endUtc = DateTimeSupport.toUTC(technicalMetadata.getEndDate().getTime());
3111       json.addProperty("end", endUtc);
3112     } else {
3113       json.addProperty("end", "");
3114     }
3115     String eventId = technicalMetadata.getEventId();
3116     json.addProperty("eventId", safeString(eventId));
3117     json.add("presenters", collectionToJsonArray(technicalMetadata.getPresenters()));
3118     Optional<Recording> optRecording = technicalMetadata.getRecording();
3119     if (optRecording.isPresent()) {
3120       json.add("recording", recordingToJson(optRecording.get()));
3121     }
3122 
3123     return json;
3124   }
3125 
3126   public static JsonObject recordingToJson(Recording recording) {
3127     JsonObject json = new JsonObject();
3128 
3129     json.addProperty("id", safeString(recording.getID()));
3130     json.addProperty("lastCheckInTime", recording.getLastCheckinTime() != null ? recording.getLastCheckinTime() : 0L);
3131     json.addProperty("lastCheckInTimeUTC", recording.getLastCheckinTime() != null ? toUTC(recording.getLastCheckinTime()) : "");
3132     json.addProperty("state", safeString(recording.getState()));
3133 
3134     return json;
3135   }
3136 
3137   @PUT
3138   @Path("{eventId}/workflows/{workflowId}/action/{action}")
3139   @RestQuery(name = "workflowAction", description = "Performs the given action for the given workflow.", returnDescription = "", pathParameters = {
3140           @RestParameter(name = "eventId", description = "The id of the media package", isRequired = true, type = RestParameter.Type.STRING),
3141           @RestParameter(name = "workflowId", description = "The id of the workflow", isRequired = true, type = RestParameter.Type.STRING),
3142           @RestParameter(name = "action", description = "The action to take: STOP, RETRY or NONE (abort processing)", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
3143                   @RestResponse(responseCode = SC_OK, description = "Workflow resumed."),
3144                   @RestResponse(responseCode = SC_NOT_FOUND, description = "Event or workflow instance not found."),
3145                   @RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid action entered."),
3146                   @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to perform the action. Maybe you need to authenticate."),
3147                   @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "An exception occurred.") })
3148   public Response workflowAction(@PathParam("eventId") String id, @PathParam("workflowId") long wfId,
3149           @PathParam("action") String action) {
3150     if (StringUtils.isEmpty(id) || StringUtils.isEmpty(action)) {
3151       return badRequest();
3152     }
3153 
3154     try {
3155       final Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
3156       if (optEvent.isEmpty()) {
3157         return notFound("Cannot find an event with id '%s'.", id);
3158       }
3159 
3160       final WorkflowInstance wfInstance = getWorkflowService().getWorkflowById(wfId);
3161       if (!wfInstance.getMediaPackage().getIdentifier().toString().equals(id)) {
3162         return badRequest(String.format("Workflow %s is not associated to event %s", wfId, id));
3163       }
3164 
3165       if (RetryStrategy.NONE.toString().equalsIgnoreCase(action)
3166         || RetryStrategy.RETRY.toString().equalsIgnoreCase(action)) {
3167         getWorkflowService().resume(wfId, Collections.singletonMap("retryStrategy", action));
3168         return ok();
3169       }
3170 
3171       if (WORKFLOW_ACTION_STOP.equalsIgnoreCase(action)) {
3172         getWorkflowService().stop(wfId);
3173         return ok();
3174       }
3175 
3176       return badRequest("Action not supported: " + action);
3177     } catch (NotFoundException e) {
3178       return notFound("Workflow not found: '%d'.", wfId);
3179     } catch (IllegalStateException e) {
3180       return badRequest(String.format("Action %s not allowed for current workflow state. EventId: %s", action, id));
3181     } catch (UnauthorizedException e) {
3182       return forbidden();
3183     } catch (Exception e) {
3184       return serverError();
3185     }
3186   }
3187 
3188   @DELETE
3189   @Path("{eventId}/workflows/{workflowId}")
3190   @RestQuery(name = "deleteWorkflow", description = "Deletes a workflow", returnDescription = "The method doesn't return any content", pathParameters = {
3191     @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING),
3192     @RestParameter(name = "workflowId", isRequired = true, description = "The workflow identifier", type = RestParameter.Type.INTEGER) }, responses = {
3193     @RestResponse(responseCode = SC_BAD_REQUEST, description = "When trying to delete the latest workflow of the event."),
3194     @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or the workflow has not been found."),
3195     @RestResponse(responseCode = SC_NO_CONTENT, description = "The method does not return any content") })
3196   public Response deleteWorkflow(@PathParam("eventId") String id, @PathParam("workflowId") long wfId)
3197     throws SearchIndexException {
3198     final Optional<Event> optEvent = getIndexService().getEvent(id, getIndex());
3199     try {
3200       if (optEvent.isEmpty()) {
3201         return notFound("Cannot find an event with id '%s'.", id);
3202       }
3203 
3204       final WorkflowInstance wfInstance = getWorkflowService().getWorkflowById(wfId);
3205       if (!wfInstance.getMediaPackage().getIdentifier().toString().equals(id)) {
3206         return badRequest(String.format("Workflow %s is not associated to event %s", wfId, id));
3207       }
3208 
3209       if (wfId == optEvent.get().getWorkflowId()) {
3210         return badRequest(String.format("Cannot delete current workflow %s from event %s."
3211           + " Only older workflows can be deleted.", wfId, id));
3212       }
3213 
3214       getWorkflowService().remove(wfId);
3215 
3216       return Response.noContent().build();
3217     } catch (WorkflowStateException e) {
3218       return badRequest("Deleting is not allowed for current workflow state. EventId: " + id);
3219     } catch (NotFoundException e) {
3220       return notFound("Workflow not found: '%d'.", wfId);
3221     } catch (UnauthorizedException e) {
3222       return forbidden();
3223     } catch (Exception e) {
3224       return serverError();
3225     }
3226   }
3227 
3228   private Optional<Event> checkAgentAccessForEvent(final String eventId) throws UnauthorizedException, SearchIndexException {
3229     final Optional<Event> event = getIndexService().getEvent(eventId, getIndex());
3230     if (event.isEmpty() || !event.get().getEventStatus().contains("SCHEDULE")) {
3231       return event;
3232     }
3233     SecurityUtil.checkAgentAccess(getSecurityService(), event.get().getAgentId());
3234     return event;
3235   }
3236 
3237   private void checkAgentAccessForAgent(final String agentId) throws UnauthorizedException {
3238     SecurityUtil.checkAgentAccess(getSecurityService(), agentId);
3239   }
3240 
3241 }