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