1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
210
211
212
213
214
215 @Path("/admin-ng/event")
216 public abstract class AbstractEventEndpoint {
217
218
219
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
231 static final Logger logger = LoggerFactory.getLogger(AbstractEventEndpoint.class);
232
233
234
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
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
281 protected String serverUrl = "http://localhost:8080";
282
283
284 protected String serviceUrl = null;
285
286
287 protected String defaultWorkflowDefinionId = null;
288
289
290 private String systemUserName = "opencast_system_account";
291
292
293
294
295
296
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
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
511
512
513
514
515
516
517
518
519
520
521
522
523 Event event = optEvent.get();
524
525
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
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
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
766
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
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
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
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
1240
1241
1242 private void removeSeriesWithNullTitlesFromFieldCollection(MetadataList ml) {
1243
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
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
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
1297
1298
1299 EventCatalogUIAdapter eventCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
1300 DublinCoreMetadataCollection metadataCollection = eventCatalogUiAdapter.getRawFields(getCollectionQueryDisable());
1301 EventUtils.setEventMetadataValues(event, metadataCollection);
1302 metadataList.add(eventCatalogUiAdapter, metadataCollection);
1303
1304
1305 removeSeriesWithNullTitlesFromFieldCollection(metadataList);
1306
1307
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
1317
1318
1319
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
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
1367 List<DublinCoreMetadataCollection> collectedMetadata = new ArrayList();
1368 for (String eventId: ids) {
1369 Optional<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1370
1371 if (optEvent.isEmpty()) {
1372 eventsNotFound.add(eventId);
1373 continue;
1374 }
1375
1376 Event event = optEvent.get();
1377
1378
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
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
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
1404 DublinCoreMetadataCollection mergedMetadata;
1405 if (collectedMetadata.size() == 1) {
1406 mergedMetadata = collectedMetadata.get(0);
1407 }
1408 else {
1409
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
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
1458 final Map<String, Optional<Event>> events = groupInstructions.getEventIds().stream()
1459 .collect(Collectors.toMap(id -> id, id -> BulkUpdateUtil.getEvent(getIndexService(), getIndex(), id)));
1460
1461
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
1473 try {
1474 if (groupInstructions.getScheduling() != null) {
1475
1476 final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupInstructions.getScheduling());
1477 updateEventScheduling(scheduling.toJSONString(), event);
1478
1479 metadata = BulkUpdateUtil.toNonTechnicalMetadataJson(scheduling);
1480 }
1481 } catch (Exception exception) {
1482 schedulingUpdateFailures.put(event.getIdentifier(), exception.getMessage());
1483 }
1484
1485
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
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
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
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
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
1563 return;
1564 }
1565 if (Util.schedulingIntervalsOverlap(start, end, otherStart, otherEnd)) {
1566
1567 currentConflicts.add(convertEventToConflictingObject(
1568 DateTimeSupport.toUTC(otherStart.getTime()),
1569 DateTimeSupport.toUTC(otherEnd.getTime()),
1570 otherEvent.getA().getTitle()));
1571 }
1572 });
1573
1574
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
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
1683
1684 if (optEvent.isEmpty()) {
1685 eventsNotFound.add(eventId);
1686 continue;
1687 }
1688
1689
1690 try {
1691 getIndexService().updateAllEventMetadata(eventId, metadata, getIndex());
1692 eventsUpdated.add(eventId);
1693 } catch (IllegalArgumentException e) {
1694 eventsUpdateFailure.add(eventId);
1695 }
1696 }
1697
1698
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
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
2256
2257
2258
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
2291
2292
2293
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
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
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
2395 List<EventCatalogUIAdapter> extendedCatalogUIAdapters = getIndexService().getExtendedEventCatalogUIAdapters();
2396 for (EventCatalogUIAdapter extendedCatalogUIAdapter : extendedCatalogUIAdapters) {
2397 metadataList.add(extendedCatalogUIAdapter, extendedCatalogUIAdapter.getRawFields());
2398 }
2399
2400
2401
2402
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
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
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
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
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
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
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
3010
3011
3012
3013
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
3053
3054
3055
3056
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 }