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.index.service.util.RestUtils.conflictJson;
44 import static org.opencastproject.index.service.util.RestUtils.notFound;
45 import static org.opencastproject.index.service.util.RestUtils.notFoundJson;
46 import static org.opencastproject.index.service.util.RestUtils.okJson;
47 import static org.opencastproject.index.service.util.RestUtils.okJsonList;
48 import static org.opencastproject.index.service.util.RestUtils.serverErrorJson;
49 import static org.opencastproject.util.DateTimeSupport.toUTC;
50 import static org.opencastproject.util.RestUtil.R.badRequest;
51 import static org.opencastproject.util.RestUtil.R.conflict;
52 import static org.opencastproject.util.RestUtil.R.forbidden;
53 import static org.opencastproject.util.RestUtil.R.noContent;
54 import static org.opencastproject.util.RestUtil.R.notFound;
55 import static org.opencastproject.util.RestUtil.R.ok;
56 import static org.opencastproject.util.RestUtil.R.serverError;
57 import static org.opencastproject.util.doc.rest.RestParameter.Type.BOOLEAN;
58 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
59 import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
60
61 import org.opencastproject.adminui.exception.JobEndpointException;
62 import org.opencastproject.adminui.impl.AdminUIConfiguration;
63 import org.opencastproject.adminui.tobira.TobiraException;
64 import org.opencastproject.adminui.tobira.TobiraService;
65 import org.opencastproject.adminui.util.BulkUpdateUtil;
66 import org.opencastproject.assetmanager.api.AssetManager;
67 import org.opencastproject.authorization.xacml.manager.api.AclService;
68 import org.opencastproject.authorization.xacml.manager.api.ManagedAcl;
69 import org.opencastproject.authorization.xacml.manager.util.AccessInformationUtil;
70 import org.opencastproject.capture.CaptureParameters;
71 import org.opencastproject.capture.admin.api.Agent;
72 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
73 import org.opencastproject.elasticsearch.api.SearchIndexException;
74 import org.opencastproject.elasticsearch.api.SearchResult;
75 import org.opencastproject.elasticsearch.api.SearchResultItem;
76 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
77 import org.opencastproject.elasticsearch.index.QueryPreprocessor;
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 Opt<String> agentId = Opt.none();
672 if (schedulingJson.has(SCHEDULING_AGENT_ID_KEY)) {
673 agentId = Opt.some(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.isSome() && 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.isSome()) {
696 checkAgentAccessForAgent(agentId.get());
697 }
698
699 Opt<Date> start = Opt.none();
700 if (schedulingJson.has(SCHEDULING_START_KEY)) {
701 start = Opt.some(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 Opt<Date> end = Opt.none();
708 if (schedulingJson.has(SCHEDULING_END_KEY)) {
709 end = Opt.some(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 Opt<Map<String, String>> agentConfiguration = Opt.none();
716 if (schedulingJson.has(SCHEDULING_AGENT_CONFIGURATION_KEY)) {
717 agentConfiguration = Opt.some(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 = Opt.some(configMap);
740 }
741 }
742
743 if ((start.isSome() || end.isSome())
744 && end.getOr(technicalMetadata.getEndDate()).before(start.getOr(technicalMetadata.getStartDate()))) {
745 throw new IllegalStateException("The end date is before the start date");
746 }
747
748 if (!start.isNone() || !end.isNone() || !agentId.isNone() || !agentConfiguration.isNone()) {
749 getSchedulerService()
750 .updateEvent(event.getIdentifier(), start, end, agentId, Opt.none(), Opt.none(), Opt.none(), 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, Opt.none(), Opt.none(), Opt.none(), Opt.none(),
960 Opt.some(mediaPackage), Opt.none(), Opt.none());
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(getCollectionQueryOverrides());
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 getCollectionQueryOverrides() {
1291 HashMap<String, ResourceListQuery> collectionQueryOverrides = new HashMap();
1292 if (getOnlySeriesWithWriteAccessEventModal()) {
1293 SeriesListQuery seriesListQuery = new SeriesListQuery();
1294 seriesListQuery.withReadPermission(true);
1295 seriesListQuery.withWritePermission(true);
1296 collectionQueryOverrides.put(DublinCore.PROPERTY_IS_PART_OF.getLocalName(), seriesListQuery);
1297 }
1298 return collectionQueryOverrides;
1299 }
1300
1301 @POST
1302 @Path("events/metadata.json")
1303 @Produces(MediaType.APPLICATION_JSON)
1304 @RestQuery(name = "geteventsmetadata",
1305 description = "Returns all the data related to the edit events metadata modal as JSON",
1306 returnDescription = "All the data related to the edit events metadata modal as JSON",
1307 restParameters = {
1308 @RestParameter(name = "eventIds", description = "The event ids", isRequired = true,
1309 type = RestParameter.Type.STRING)
1310 }, responses = {
1311 @RestResponse(description = "Returns all the data related to the edit events metadata modal as JSON",
1312 responseCode = HttpServletResponse.SC_OK),
1313 @RestResponse(description = "No events to update, either not found or with running workflow, "
1314 + "details in response body.",
1315 responseCode = HttpServletResponse.SC_NOT_FOUND)
1316 })
1317 public Response getEventsMetadata(@FormParam("eventIds") String eventIds) throws Exception {
1318 if (StringUtils.isBlank(eventIds)) {
1319 return badRequest("Event ids can't be empty");
1320 }
1321
1322 JSONParser parser = new JSONParser();
1323 List<String> ids;
1324 try {
1325 ids = (List<String>) parser.parse(eventIds);
1326 } catch (org.json.simple.parser.ParseException e) {
1327 logger.error("Unable to parse '{}'", eventIds, e);
1328 return badRequest("Unable to parse event ids");
1329 } catch (ClassCastException e) {
1330 logger.error("Unable to cast '{}'", eventIds, e);
1331 return badRequest("Unable to parse event ids");
1332 }
1333
1334 Set<String> eventsNotFound = new HashSet();
1335 Set<String> eventsWithRunningWorkflow = new HashSet();
1336 Set<String> eventsMerged = new HashSet();
1337
1338
1339 List<DublinCoreMetadataCollection> collectedMetadata = new ArrayList();
1340 for (String eventId: ids) {
1341 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1342
1343 if (optEvent.isNone()) {
1344 eventsNotFound.add(eventId);
1345 continue;
1346 }
1347
1348 Event event = optEvent.get();
1349
1350
1351 final String wfState = event.getWorkflowState();
1352 if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
1353 eventsWithRunningWorkflow.add(eventId);
1354 continue;
1355 }
1356
1357
1358 EventCatalogUIAdapter eventCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
1359 DublinCoreMetadataCollection metadataCollection = eventCatalogUiAdapter.getRawFields(
1360 getCollectionQueryOverrides());
1361 EventUtils.setEventMetadataValues(event, metadataCollection);
1362 collectedMetadata.add(metadataCollection);
1363
1364 eventsMerged.add(eventId);
1365 }
1366
1367
1368 if (collectedMetadata.isEmpty()) {
1369 return notFoundJson(obj(
1370 f("notFound", JSONUtils.setToJSON(eventsNotFound)),
1371 f("runningWorkflow", JSONUtils.setToJSON(eventsWithRunningWorkflow))));
1372 }
1373
1374
1375 DublinCoreMetadataCollection mergedMetadata;
1376 if (collectedMetadata.size() == 1) {
1377 mergedMetadata = collectedMetadata.get(0);
1378 }
1379 else {
1380
1381 mergedMetadata = new DublinCoreMetadataCollection(collectedMetadata.get(0));
1382 collectedMetadata.remove(0);
1383
1384 for (MetadataField field : mergedMetadata.getFields()) {
1385 for (DublinCoreMetadataCollection otherMetadataCollection : collectedMetadata) {
1386 MetadataField matchingField = otherMetadataCollection.getOutputFields().get(field.getOutputID());
1387
1388
1389 if (!Objects.equals(field.getValue(), matchingField.getValue())) {
1390 field.setDifferentValues();
1391 break;
1392 }
1393 }
1394 }
1395 }
1396
1397 return okJson(obj(
1398 f("metadata", MetadataJson.collectionToJson(mergedMetadata, true)),
1399 f("notFound", JSONUtils.setToJSON(eventsNotFound)),
1400 f("runningWorkflow", JSONUtils.setToJSON(eventsWithRunningWorkflow)),
1401 f("merged", JSONUtils.setToJSON(eventsMerged))
1402 ));
1403 }
1404
1405 @PUT
1406 @Path("bulk/update")
1407 @RestQuery(name = "bulkupdate", description = "Update all of the given events at once", restParameters = {
1408 @RestParameter(name = "update", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of groups with events and fields to update.")}, responses = {
1409 @RestResponse(description = "All events have been updated successfully.", responseCode = HttpServletResponse.SC_OK),
1410 @RestResponse(description = "Could not parse update instructions.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1411 @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),
1412 @RestResponse(description = "The events in the response body were not found. No events were updated.", responseCode = HttpServletResponse.SC_NOT_FOUND)},
1413 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")
1414 public Response bulkUpdate(@FormParam("update") String updateJson) {
1415
1416 final BulkUpdateUtil.BulkUpdateInstructions instructions;
1417 try {
1418 instructions = new BulkUpdateUtil.BulkUpdateInstructions(updateJson);
1419 } catch (IllegalArgumentException e) {
1420 return badRequest("Cannot parse bulk update instructions");
1421 }
1422
1423 final Map<String, String> metadataUpdateFailures = new HashMap<>();
1424 final Map<String, String> schedulingUpdateFailures = new HashMap<>();
1425
1426 for (final BulkUpdateUtil.BulkUpdateInstructionGroup groupInstructions : instructions.getGroups()) {
1427
1428 final Map<String, Optional<Event>> events = groupInstructions.getEventIds().stream()
1429 .collect(Collectors.toMap(id -> id, id -> BulkUpdateUtil.getEvent(getIndexService(), getIndex(), id)));
1430
1431
1432 final Set<String> notFoundIds = events.entrySet().stream().filter(e -> !e.getValue().isPresent()).map(Entry::getKey).collect(Collectors.toSet());
1433 if (!notFoundIds.isEmpty()) {
1434 return notFoundJson(JSONUtils.setToJSON(notFoundIds));
1435 }
1436
1437
1438 events.values().forEach(e -> e.ifPresent(event -> {
1439
1440 JSONObject metadata = null;
1441
1442
1443 try {
1444 if (groupInstructions.getScheduling() != null) {
1445
1446 final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupInstructions.getScheduling());
1447 updateEventScheduling(scheduling.toJSONString(), event);
1448
1449 metadata = BulkUpdateUtil.toNonTechnicalMetadataJson(scheduling);
1450 }
1451 } catch (Exception exception) {
1452 schedulingUpdateFailures.put(event.getIdentifier(), exception.getMessage());
1453 }
1454
1455
1456 try {
1457 if (groupInstructions.getMetadata() != null || metadata != null) {
1458 metadata = BulkUpdateUtil.mergeMetadataFields(metadata, groupInstructions.getMetadata());
1459 getIndexService().updateAllEventMetadata(event.getIdentifier(), JSONArray.toJSONString(Collections.singletonList(metadata)), getIndex());
1460 }
1461 } catch (Exception exception) {
1462 metadataUpdateFailures.put(event.getIdentifier(), exception.getMessage());
1463 }
1464 }));
1465 }
1466
1467
1468 if (!metadataUpdateFailures.isEmpty() || !schedulingUpdateFailures.isEmpty()) {
1469 return serverErrorJson(obj(
1470 f("metadataFailures", JSONUtils.mapToJSON(metadataUpdateFailures)),
1471 f("schedulingFailures", JSONUtils.mapToJSON(schedulingUpdateFailures))
1472 ));
1473 }
1474 return ok();
1475 }
1476
1477 @POST
1478 @Path("bulk/conflicts")
1479 @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 = {
1480 @RestParameter(name = "update", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of events and fields to update.")}, responses = {
1481 @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
1482 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "The events in the response body were not found. No events were updated."),
1483 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"),
1484 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters")})
1485 public Response getBulkConflicts(@FormParam("update") final String updateJson) throws NotFoundException {
1486 final BulkUpdateUtil.BulkUpdateInstructions instructions;
1487 try {
1488 instructions = new BulkUpdateUtil.BulkUpdateInstructions(updateJson);
1489 } catch (IllegalArgumentException e) {
1490 return badRequest("Cannot parse bulk update instructions");
1491 }
1492
1493 final Map<String, List<JValue>> conflicts = new HashMap<>();
1494 final List<Tuple3<String, Optional<Event>, JSONObject>> eventsWithSchedulingOpt = instructions.getGroups().stream()
1495 .flatMap(group -> group.getEventIds().stream().map(eventId -> Tuple3
1496 .tuple3(eventId, BulkUpdateUtil.getEvent(getIndexService(), getIndex(), eventId), group.getScheduling())))
1497 .collect(Collectors.toList());
1498
1499 final Set<String> notFoundIds = eventsWithSchedulingOpt.stream().filter(e -> !e.getB().isPresent())
1500 .map(Tuple3::getA).collect(Collectors.toSet());
1501 if (!notFoundIds.isEmpty()) {
1502 return notFoundJson(JSONUtils.setToJSON(notFoundIds));
1503 }
1504 final List<Tuple<Event, JSONObject>> eventsWithScheduling = eventsWithSchedulingOpt.stream()
1505 .map(e -> Tuple.tuple(e.getB().get(), e.getC())).collect(Collectors.toList());
1506 final Set<String> changedIds = eventsWithScheduling.stream().map(e -> e.getA().getIdentifier())
1507 .collect(Collectors.toSet());
1508 for (final Tuple<Event, JSONObject> eventWithGroup : eventsWithScheduling) {
1509 final Event event = eventWithGroup.getA();
1510 final JSONObject groupScheduling = eventWithGroup.getB();
1511 try {
1512 if (groupScheduling != null) {
1513
1514 final JSONObject scheduling = BulkUpdateUtil.addSchedulingDates(event, groupScheduling);
1515 final Date start = Date.from(Instant.parse((String) scheduling.get(SCHEDULING_START_KEY)));
1516 final Date end = Date.from(Instant.parse((String) scheduling.get(SCHEDULING_END_KEY)));
1517 final String agentId = Optional.ofNullable((String) scheduling.get(SCHEDULING_AGENT_ID_KEY))
1518 .orElse(event.getAgentId());
1519
1520 final List<JValue> currentConflicts = new ArrayList<>();
1521
1522
1523 eventsWithScheduling.stream()
1524 .filter(otherEvent -> !otherEvent.getA().getIdentifier().equals(event.getIdentifier()))
1525 .forEach(otherEvent -> {
1526 final JSONObject otherScheduling = BulkUpdateUtil.addSchedulingDates(otherEvent.getA(), otherEvent.getB());
1527 final Date otherStart = Date.from(Instant.parse((String) otherScheduling.get(SCHEDULING_START_KEY)));
1528 final Date otherEnd = Date.from(Instant.parse((String) otherScheduling.get(SCHEDULING_END_KEY)));
1529 final String otherAgentId = Optional.ofNullable((String) otherScheduling.get(SCHEDULING_AGENT_ID_KEY))
1530 .orElse(otherEvent.getA().getAgentId());
1531 if (!otherAgentId.equals(agentId)) {
1532
1533 return;
1534 }
1535 if (Util.schedulingIntervalsOverlap(start, end, otherStart, otherEnd)) {
1536
1537 currentConflicts.add(convertEventToConflictingObject(DateTimeSupport.toUTC(otherStart.getTime()),
1538 DateTimeSupport.toUTC(otherEnd.getTime()), otherEvent.getA().getTitle()));
1539 }
1540 });
1541
1542
1543 final List<MediaPackage> conflicting = getSchedulerService().findConflictingEvents(agentId, start, end)
1544 .stream()
1545 .filter(mp -> !changedIds.contains(mp.getIdentifier().toString()))
1546 .collect(Collectors.toList());
1547 if (!conflicting.isEmpty()) {
1548 currentConflicts.addAll(convertToConflictObjects(event.getIdentifier(), conflicting));
1549 }
1550 conflicts.put(event.getIdentifier(), currentConflicts);
1551 }
1552 } catch (final SchedulerException | UnauthorizedException | SearchIndexException exception) {
1553 throw new RuntimeException(exception);
1554 }
1555 }
1556
1557 if (!conflicts.isEmpty()) {
1558 final List<JValue> responseJson = new ArrayList<>();
1559 conflicts.forEach((eventId, conflictingEvents) -> {
1560 if (!conflictingEvents.isEmpty()) {
1561 responseJson.add(obj(f("eventId", eventId), f("conflicts", arr(conflictingEvents))));
1562 }
1563 });
1564 if (!responseJson.isEmpty()) {
1565 return conflictJson(arr(responseJson));
1566 }
1567 }
1568
1569 return noContent();
1570 }
1571
1572 @PUT
1573 @Path("{eventId}/metadata")
1574 @RestQuery(name = "updateeventmetadata", description = "Update the passed metadata for the event with the given Id", pathParameters = {
1575 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
1576 @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of metadata to update") }, responses = {
1577 @RestResponse(description = "The metadata have been updated.", responseCode = HttpServletResponse.SC_OK),
1578 @RestResponse(description = "Could not parse metadata.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1579 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "No content is returned.")
1580 public Response updateEventMetadata(@PathParam("eventId") String id, @FormParam("metadata") String metadataJSON)
1581 throws Exception {
1582 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1583 if (optEvent.isNone())
1584 return notFound("Cannot find an event with id '%s'.", id);
1585
1586 try {
1587 MetadataList metadataList = getIndexService().updateAllEventMetadata(id, metadataJSON, getIndex());
1588 return okJson(MetadataJson.listToJson(metadataList, true));
1589 } catch (IllegalArgumentException e) {
1590 return badRequest(String.format("Event %s metadata can't be updated.: %s", id, e.getMessage()));
1591 }
1592 }
1593
1594 @PUT
1595 @Path("events/metadata")
1596 @RestQuery(name = "updateeventsmetadata",
1597 description = "Update the passed metadata for the events with the given ids",
1598 restParameters = {
1599 @RestParameter(name = "eventIds", isRequired = true, type = RestParameter.Type.STRING,
1600 description = "The ids of the events to update"),
1601 @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT,
1602 description = "The metadata fields to update"),
1603 }, responses = {
1604 @RestResponse(description = "All events have been updated successfully.",
1605 responseCode = HttpServletResponse.SC_NO_CONTENT),
1606 @RestResponse(description = "One or multiple errors occured while updating event metadata. "
1607 + "Some events may have been updated successfully. "
1608 + "Details are available in the response body.",
1609 responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR)},
1610 returnDescription = "In case of complete success, no content is returned. Otherwise, the response content "
1611 + "contains the ids of events that couldn't be found and the ids and errors of events where the update failed "
1612 + "as well as the ids of the events that were updated successfully.")
1613 public Response updateEventsMetadata(@FormParam("eventIds") String eventIds, @FormParam("metadata") String metadata)
1614 throws Exception {
1615
1616 if (StringUtils.isBlank(eventIds)) {
1617 return badRequest("Event ids can't be empty");
1618 }
1619
1620 JSONParser parser = new JSONParser();
1621 List<String> ids;
1622 try {
1623 ids = (List<String>) parser.parse(eventIds);
1624 } catch (org.json.simple.parser.ParseException e) {
1625 logger.error("Unable to parse '{}'", eventIds, e);
1626 return badRequest("Unable to parse event ids");
1627 } catch (ClassCastException e) {
1628 logger.error("Unable to cast '{}'", eventIds, e);
1629 return badRequest("Unable to parse event ids");
1630 }
1631
1632
1633 Set<String> eventsNotFound = new HashSet<>();
1634 Set<String> eventsUpdated = new HashSet<>();
1635 Set<String> eventsUpdateFailure = new HashSet();
1636
1637 for (String eventId : ids) {
1638 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
1639
1640
1641 if (optEvent.isNone()) {
1642 eventsNotFound.add(eventId);
1643 continue;
1644 }
1645
1646
1647 try {
1648 getIndexService().updateAllEventMetadata(eventId, metadata, getIndex());
1649 eventsUpdated.add(eventId);
1650 } catch (IllegalArgumentException e) {
1651 eventsUpdateFailure.add(eventId);
1652 }
1653 }
1654
1655
1656 if (!eventsNotFound.isEmpty() || !eventsUpdateFailure.isEmpty()) {
1657 return serverErrorJson(obj(
1658 f("updateFailures", JSONUtils.setToJSON(eventsUpdateFailure)),
1659 f("notFound", JSONUtils.setToJSON(eventsNotFound)),
1660 f("updated", JSONUtils.setToJSON(eventsUpdated))
1661 ));
1662 }
1663
1664 return noContent();
1665 }
1666
1667 @GET
1668 @Path("{eventId}/asset/assets.json")
1669 @Produces(MediaType.APPLICATION_JSON)
1670 @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 = {
1671 @RestResponse(description = "Returns the number of assets from each types as JSON", responseCode = HttpServletResponse.SC_OK),
1672 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1673 public Response getAssetList(@PathParam("eventId") String id) throws Exception {
1674 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1675 if (optEvent.isNone())
1676 return notFound("Cannot find an event with id '%s'.", id);
1677 MediaPackage mp;
1678 try {
1679 mp = getIndexService().getEventMediapackage(optEvent.get());
1680 } catch (IndexServiceException e) {
1681 if (e.getCause() instanceof NotFoundException) {
1682 return notFound("Cannot find data for event %s", id);
1683 } else if (e.getCause() instanceof UnauthorizedException) {
1684 return Response.status(Status.FORBIDDEN).entity("Not authorized to access " + id).build();
1685 }
1686 logger.error("Internal error when trying to access metadata for " + id, e);
1687 return serverError();
1688 }
1689 int attachments = mp.getAttachments().length;
1690 int catalogs = mp.getCatalogs().length;
1691 int media = mp.getTracks().length;
1692 int publications = mp.getPublications().length;
1693 return okJson(obj(f("attachments", v(attachments)), f("catalogs", v(catalogs)), f("media", v(media)),
1694 f("publications", v(publications))));
1695 }
1696
1697 @GET
1698 @Path("{eventId}/asset/attachment/attachments.json")
1699 @Produces(MediaType.APPLICATION_JSON)
1700 @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 = {
1701 @RestResponse(description = "Returns a list of attachments from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1702 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1703 public Response getAttachmentsList(@PathParam("eventId") String id) throws Exception {
1704 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1705 if (optEvent.isNone())
1706 return notFound("Cannot find an event with id '%s'.", id);
1707 MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1708 return okJson(arr(getEventMediaPackageElements(mp.getAttachments())));
1709 }
1710
1711 @GET
1712 @Path("{eventId}/asset/attachment/{id}.json")
1713 @Produces(MediaType.APPLICATION_JSON)
1714 @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 = {
1715 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1716 @RestParameter(name = "id", description = "The attachment id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1717 @RestResponse(description = "Returns the details of an attachment from the given event and attachment id as JSON", responseCode = HttpServletResponse.SC_OK),
1718 @RestResponse(description = "No event or attachment with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1719 public Response getAttachment(@PathParam("eventId") String eventId, @PathParam("id") String id)
1720 throws NotFoundException, SearchIndexException, IndexServiceException {
1721 MediaPackage mp = getMediaPackageByEventId(eventId);
1722
1723 Attachment attachment = mp.getAttachment(id);
1724 if (attachment == null)
1725 return notFound("Cannot find an attachment with id '%s'.", id);
1726 return okJson(attachmentToJSON(attachment));
1727 }
1728
1729 @GET
1730 @Path("{eventId}/asset/catalog/catalogs.json")
1731 @Produces(MediaType.APPLICATION_JSON)
1732 @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 = {
1733 @RestResponse(description = "Returns a list of catalogs from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1734 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1735 public Response getCatalogList(@PathParam("eventId") String id) throws Exception {
1736 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1737 if (optEvent.isNone())
1738 return notFound("Cannot find an event with id '%s'.", id);
1739 MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1740 return okJson(arr(getEventMediaPackageElements(mp.getCatalogs())));
1741 }
1742
1743 @GET
1744 @Path("{eventId}/asset/catalog/{id}.json")
1745 @Produces(MediaType.APPLICATION_JSON)
1746 @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 = {
1747 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1748 @RestParameter(name = "id", description = "The catalog id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1749 @RestResponse(description = "Returns the details of a catalog from the given event and catalog id as JSON", responseCode = HttpServletResponse.SC_OK),
1750 @RestResponse(description = "No event or catalog with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1751 public Response getCatalog(@PathParam("eventId") String eventId, @PathParam("id") String id)
1752 throws NotFoundException, SearchIndexException, IndexServiceException {
1753 MediaPackage mp = getMediaPackageByEventId(eventId);
1754
1755 Catalog catalog = mp.getCatalog(id);
1756 if (catalog == null)
1757 return notFound("Cannot find a catalog with id '%s'.", id);
1758 return okJson(catalogToJSON(catalog));
1759 }
1760
1761 @GET
1762 @Path("{eventId}/asset/media/media.json")
1763 @Produces(MediaType.APPLICATION_JSON)
1764 @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 = {
1765 @RestResponse(description = "Returns a list of media from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1766 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1767 public Response getMediaList(@PathParam("eventId") String id) throws Exception {
1768 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1769 if (optEvent.isNone())
1770 return notFound("Cannot find an event with id '%s'.", id);
1771 MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1772 return okJson(arr(getEventMediaPackageElements(mp.getTracks())));
1773 }
1774
1775 @GET
1776 @Path("{eventId}/asset/media/{id}.json")
1777 @Produces(MediaType.APPLICATION_JSON)
1778 @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 = {
1779 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1780 @RestParameter(name = "id", description = "The media id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1781 @RestResponse(description = "Returns the media of a catalog from the given event and media id as JSON", responseCode = HttpServletResponse.SC_OK),
1782 @RestResponse(description = "No event or media with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1783 public Response getMedia(@PathParam("eventId") String eventId, @PathParam("id") String id)
1784 throws NotFoundException, SearchIndexException, IndexServiceException {
1785 MediaPackage mp = getMediaPackageByEventId(eventId);
1786
1787 Track track = mp.getTrack(id);
1788 if (track == null)
1789 return notFound("Cannot find media with id '%s'.", id);
1790 return okJson(trackToJSON(track));
1791 }
1792
1793 @GET
1794 @Path("{eventId}/asset/publication/publications.json")
1795 @Produces(MediaType.APPLICATION_JSON)
1796 @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 = {
1797 @RestResponse(description = "Returns a list of publications from the given event as JSON", responseCode = HttpServletResponse.SC_OK),
1798 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1799 public Response getPublicationList(@PathParam("eventId") String id) throws Exception {
1800 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1801 if (optEvent.isNone())
1802 return notFound("Cannot find an event with id '%s'.", id);
1803 MediaPackage mp = getIndexService().getEventMediapackage(optEvent.get());
1804 return okJson(arr(getEventPublications(mp.getPublications())));
1805 }
1806
1807 @GET
1808 @Path("{eventId}/asset/publication/{id}.json")
1809 @Produces(MediaType.APPLICATION_JSON)
1810 @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 = {
1811 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
1812 @RestParameter(name = "id", description = "The publication id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1813 @RestResponse(description = "Returns the publication of a catalog from the given event and publication id as JSON", responseCode = HttpServletResponse.SC_OK),
1814 @RestResponse(description = "No event or publication with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1815 public Response getPublication(@PathParam("eventId") String eventId, @PathParam("id") String id)
1816 throws NotFoundException, SearchIndexException, IndexServiceException {
1817 MediaPackage mp = getMediaPackageByEventId(eventId);
1818
1819 Publication publication = null;
1820 for (Publication p : mp.getPublications()) {
1821 if (id.equals(p.getIdentifier())) {
1822 publication = p;
1823 break;
1824 }
1825 }
1826
1827 if (publication == null)
1828 return notFound("Cannot find publication with id '%s'.", id);
1829 return okJson(publicationToJSON(publication));
1830 }
1831
1832 @GET
1833 @Path("{eventId}/tobira/pages")
1834 @RestQuery(
1835 name = "getEventHostPages",
1836 description = "Returns the pages of a connected Tobira instance that contain the given event",
1837 returnDescription = "The Tobira pages that contain the given event",
1838 pathParameters = {
1839 @RestParameter(
1840 name = "eventId",
1841 isRequired = true,
1842 description = "The event identifier",
1843 type = STRING
1844 ),
1845 },
1846 responses = {
1847 @RestResponse(
1848 responseCode = SC_OK,
1849 description = "The Tobira pages containing the given event"
1850 ),
1851 @RestResponse(
1852 responseCode = SC_NOT_FOUND,
1853 description = "Tobira doesn't know about the given event"
1854 ),
1855 @RestResponse(
1856 responseCode = SC_SERVICE_UNAVAILABLE,
1857 description = "Tobira is not configured (correctly)"
1858 ),
1859 }
1860 )
1861 public Response getEventHostPages(@PathParam("eventId") String eventId) {
1862 var tobira = TobiraService.getTobira(getSecurityService().getOrganization().getId());
1863 if (!tobira.ready()) {
1864 return Response.status(Status.SERVICE_UNAVAILABLE)
1865 .entity("Tobira is not configured (correctly)")
1866 .build();
1867 }
1868
1869 try {
1870 var eventData = tobira.getEventHostPages(eventId);
1871 if (eventData == null) {
1872 throw new WebApplicationException(NOT_FOUND);
1873 }
1874 eventData.put("baseURL", tobira.getOrigin());
1875 return Response.ok(eventData.toJSONString()).build();
1876 } catch (TobiraException e) {
1877 throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
1878 }
1879 }
1880
1881 @GET
1882 @Path("{eventId}/workflows.json")
1883 @Produces(MediaType.APPLICATION_JSON)
1884 @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 = {
1885 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
1886 @RestResponse(description = "Returns all the data related to the event workflows tab as JSON", responseCode = HttpServletResponse.SC_OK),
1887 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
1888 public Response getEventWorkflows(@PathParam("eventId") String id)
1889 throws UnauthorizedException, SearchIndexException, JobEndpointException {
1890 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1891 if (optEvent.isNone())
1892 return notFound("Cannot find an event with id '%s'.", id);
1893
1894 try {
1895 if (optEvent.get().getEventStatus().equals("EVENTS.EVENTS.STATUS.SCHEDULED")) {
1896 List<Field> fields = new ArrayList<Field>();
1897 Map<String, String> workflowConfig = getSchedulerService().getWorkflowConfig(id);
1898 for (Entry<String, String> entry : workflowConfig.entrySet()) {
1899 fields.add(f(entry.getKey(), v(entry.getValue(), Jsons.BLANK)));
1900 }
1901
1902 Map<String, String> agentConfiguration = getSchedulerService().getCaptureAgentConfiguration(id);
1903 return okJson(obj(f("workflowId", v(agentConfiguration.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION), Jsons.BLANK)),
1904 f("configuration", obj(fields))));
1905 } else {
1906 List<WorkflowInstance> workflowInstances = getWorkflowService().getWorkflowInstancesByMediaPackage(id);
1907 List<JValue> jsonList = new ArrayList<>();
1908
1909 for (WorkflowInstance instance : workflowInstances) {
1910 long instanceId = instance.getId();
1911 Date created = instance.getDateCreated();
1912 String submitter = instance.getCreatorName();
1913
1914 User user = submitter == null ? null : getUserDirectoryService().loadUser(submitter);
1915 String submitterName = null;
1916 String submitterEmail = null;
1917 if (user != null) {
1918 submitterName = user.getName();
1919 submitterEmail = user.getEmail();
1920 }
1921
1922 jsonList.add(obj(f("id", v(instanceId)), f("title", v(instance.getTitle(), Jsons.BLANK)),
1923 f("status", v(WORKFLOW_STATUS_TRANSLATION_PREFIX + instance.getState().toString())),
1924 f("submitted", v(created != null ? DateTimeSupport.toUTC(created.getTime()) : "", Jsons.BLANK)),
1925 f("submitter", v(submitter, Jsons.BLANK)),
1926 f("submitterName", v(submitterName, Jsons.BLANK)),
1927 f("submitterEmail", v(submitterEmail, Jsons.BLANK))));
1928 }
1929 JObject json = obj(f("results", arr(jsonList)), f("count", v(workflowInstances.size())));
1930 return okJson(json);
1931 }
1932 } catch (NotFoundException e) {
1933 return notFound("Cannot find workflows for event %s", id);
1934 } catch (SchedulerException e) {
1935 logger.error("Unable to get workflow data for event with id {}", id);
1936 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
1937 } catch (WorkflowDatabaseException e) {
1938 throw new JobEndpointException(String.format("Not able to get the list of job from the database: %s", e),
1939 e.getCause());
1940 }
1941 }
1942
1943 @PUT
1944 @Path("{eventId}/workflows")
1945 @RestQuery(name = "updateEventWorkflow", description = "Update the workflow configuration for the scheduled event with the given id", pathParameters = {
1946 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {
1947 @RestParameter(name = "configuration", isRequired = true, description = "The workflow configuration as JSON", type = RestParameter.Type.TEXT) }, responses = {
1948 @RestResponse(description = "Request executed succesfully", responseCode = HttpServletResponse.SC_NO_CONTENT),
1949 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "The method does not retrun any content.")
1950 public Response updateEventWorkflow(@PathParam("eventId") String id, @FormParam("configuration") String configuration)
1951 throws SearchIndexException, UnauthorizedException {
1952 Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
1953 if (optEvent.isNone())
1954 return notFound("Cannot find an event with id '%s'.", id);
1955
1956 if (optEvent.get().isScheduledEvent() && !optEvent.get().hasRecordingStarted()) {
1957 try {
1958
1959 JSONObject configJSON;
1960 try {
1961 configJSON = (JSONObject) new JSONParser().parse(configuration);
1962 } catch (Exception e) {
1963 logger.warn("Unable to parse the workflow configuration {}", configuration);
1964 return badRequest();
1965 }
1966
1967 Opt<Map<String, String>> caMetadataOpt = Opt.none();
1968 Opt<Map<String, String>> workflowConfigOpt = Opt.none();
1969
1970 String workflowId = (String) configJSON.get("id");
1971 Map<String, String> caMetadata = new HashMap<>(getSchedulerService().getCaptureAgentConfiguration(id));
1972 if (!workflowId.equals(caMetadata.get(CaptureParameters.INGEST_WORKFLOW_DEFINITION))) {
1973 caMetadata.put(CaptureParameters.INGEST_WORKFLOW_DEFINITION, workflowId);
1974 caMetadataOpt = Opt.some(caMetadata);
1975 }
1976
1977 Map<String, String> workflowConfig = new HashMap<>((JSONObject) configJSON.get("configuration"));
1978 Map<String, String> oldWorkflowConfig = new HashMap<>(getSchedulerService().getWorkflowConfig(id));
1979 if (!oldWorkflowConfig.equals(workflowConfig))
1980 workflowConfigOpt = Opt.some(workflowConfig);
1981
1982 if (caMetadataOpt.isNone() && workflowConfigOpt.isNone())
1983 return Response.noContent().build();
1984
1985 checkAgentAccessForAgent(optEvent.get().getAgentId());
1986
1987 getSchedulerService().updateEvent(id, Opt.<Date> none(), Opt.<Date> none(), Opt.<String> none(),
1988 Opt.<Set<String>> none(), Opt.<MediaPackage> none(), workflowConfigOpt, caMetadataOpt);
1989 return Response.noContent().build();
1990 } catch (NotFoundException e) {
1991 return notFound("Cannot find event %s in scheduler service", id);
1992 } catch (SchedulerException e) {
1993 logger.error("Unable to update scheduling workflow data for event with id {}", id);
1994 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
1995 }
1996 } else {
1997 return badRequest(String.format("Event %s workflow can not be updated as the recording already started.", id));
1998 }
1999 }
2000
2001 @GET
2002 @Path("{eventId}/workflows/{workflowId}")
2003 @Produces(MediaType.APPLICATION_JSON)
2004 @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 = {
2005 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2006 @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2007 @RestResponse(description = "Returns all the data related to the event single workflow tab as JSON", responseCode = HttpServletResponse.SC_OK),
2008 @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2009 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2010 public Response getEventWorkflow(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId)
2011 throws SearchIndexException {
2012 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2013 if (optEvent.isNone())
2014 return notFound("Cannot find an event with id '%s'.", eventId);
2015
2016 long workflowInstanceId;
2017 try {
2018 workflowId = StringUtils.remove(workflowId, ".json");
2019 workflowInstanceId = Long.parseLong(workflowId);
2020 } catch (Exception e) {
2021 logger.warn("Unable to parse workflow id {}", workflowId);
2022 return RestUtil.R.badRequest();
2023 }
2024
2025 try {
2026 WorkflowInstance instance = getWorkflowService().getWorkflowById(workflowInstanceId);
2027
2028
2029 Date created = instance.getDateCreated();
2030
2031 Date completed = instance.getDateCompleted();
2032 if (completed == null)
2033 completed = new Date();
2034
2035 long executionTime = completed.getTime() - created.getTime();
2036
2037 var fields = instance.getConfigurations()
2038 .entrySet()
2039 .stream()
2040 .map(e -> f(e.getKey(), v(e.getValue(), Jsons.BLANK)))
2041 .collect(Collectors.toList());
2042
2043 return okJson(obj(
2044 f("status", v(WORKFLOW_STATUS_TRANSLATION_PREFIX + instance.getState(), Jsons.BLANK)),
2045 f("description", v(instance.getDescription(), Jsons.BLANK)),
2046 f("executionTime", v(executionTime, Jsons.BLANK)),
2047 f("wiid", v(instance.getId(), Jsons.BLANK)), f("title", v(instance.getTitle(), Jsons.BLANK)),
2048 f("wdid", v(instance.getTemplate(), Jsons.BLANK)),
2049 f("configuration", obj(fields)),
2050 f("submittedAt", v(toUTC(created.getTime()), Jsons.BLANK)),
2051 f("creator", v(instance.getCreatorName(), Jsons.BLANK))));
2052 } catch (NotFoundException e) {
2053 return notFound("Cannot find workflow %s", workflowId);
2054 } catch (WorkflowDatabaseException e) {
2055 logger.error("Unable to get workflow {} of event {}", workflowId, eventId, e);
2056 return serverError();
2057 } catch (UnauthorizedException e) {
2058 return forbidden();
2059 }
2060 }
2061
2062 @GET
2063 @Path("{eventId}/workflows/{workflowId}/operations.json")
2064 @Produces(MediaType.APPLICATION_JSON)
2065 @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 = {
2066 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2067 @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2068 @RestResponse(description = "Returns all the data related to the event workflow/operations tab as JSON", responseCode = HttpServletResponse.SC_OK),
2069 @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2070 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2071 public Response getEventOperations(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId)
2072 throws SearchIndexException {
2073 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2074 if (optEvent.isNone())
2075 return notFound("Cannot find an event with id '%s'.", eventId);
2076
2077 long workflowInstanceId;
2078 try {
2079 workflowInstanceId = Long.parseLong(workflowId);
2080 } catch (Exception e) {
2081 logger.warn("Unable to parse workflow id {}", workflowId);
2082 return RestUtil.R.badRequest();
2083 }
2084
2085 try {
2086 WorkflowInstance instance = getWorkflowService().getWorkflowById(workflowInstanceId);
2087
2088 List<WorkflowOperationInstance> operations = instance.getOperations();
2089 List<JValue> operationsJSON = new ArrayList<>();
2090
2091 for (WorkflowOperationInstance wflOp : operations) {
2092 List<Field> fields = new ArrayList<>();
2093 for (String key : wflOp.getConfigurationKeys()) {
2094 fields.add(f(key, v(wflOp.getConfiguration(key), Jsons.BLANK)));
2095 }
2096 operationsJSON.add(obj(
2097 f("status",
2098 v(WORKFLOW_STATUS_TRANSLATION_PREFIX + wflOp.getState(), Jsons.BLANK)),
2099 f("title", v(wflOp.getTemplate(), Jsons.BLANK)),
2100 f("description", v(wflOp.getDescription(), Jsons.BLANK)),
2101 f("id", v(wflOp.getId(), Jsons.BLANK)),
2102 f("configuration", obj(fields))
2103 ));
2104 }
2105
2106 return okJson(arr(operationsJSON));
2107 } catch (NotFoundException e) {
2108 return notFound("Cannot find workflow %s", workflowId);
2109 } catch (WorkflowDatabaseException e) {
2110 logger.error("Unable to get workflow operations of event {} and workflow {}", eventId, workflowId, e);
2111 return serverError();
2112 } catch (UnauthorizedException e) {
2113 return forbidden();
2114 }
2115 }
2116
2117 @GET
2118 @Path("{eventId}/workflows/{workflowId}/operations/{operationPosition}")
2119 @Produces(MediaType.APPLICATION_JSON)
2120 @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 = {
2121 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2122 @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING),
2123 @RestParameter(name = "operationPosition", description = "The operation position", isRequired = true, type = RestParameter.Type.INTEGER) }, responses = {
2124 @RestResponse(description = "Returns all the data related to the event workflow/operation tab as JSON", responseCode = HttpServletResponse.SC_OK),
2125 @RestResponse(description = "Unable to parse workflowId or operationPosition", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2126 @RestResponse(description = "No operation with these identifiers was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2127 public Response getEventOperation(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
2128 @PathParam("operationPosition") Integer operationPosition) throws SearchIndexException {
2129 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2130 if (optEvent.isNone())
2131 return notFound("Cannot find an event with id '%s'.", eventId);
2132
2133 long workflowInstanceId;
2134 try {
2135 workflowInstanceId = Long.parseLong(workflowId);
2136 } catch (Exception e) {
2137 logger.warn("Unable to parse workflow id {}", workflowId);
2138 return RestUtil.R.badRequest();
2139 }
2140
2141 WorkflowInstance instance;
2142 try {
2143 instance = getWorkflowService().getWorkflowById(workflowInstanceId);
2144 } catch (NotFoundException e) {
2145 return notFound("Cannot find workflow %s", workflowId);
2146 } catch (WorkflowDatabaseException e) {
2147 logger.error("Unable to get workflow operation of event {} and workflow {} at position {}", eventId, workflowId,
2148 operationPosition, e);
2149 return serverError();
2150 } catch (UnauthorizedException e) {
2151 return forbidden();
2152 }
2153
2154 List<WorkflowOperationInstance> operations = instance.getOperations();
2155
2156 if (operations.size() > operationPosition) {
2157 WorkflowOperationInstance wflOp = operations.get(operationPosition);
2158 return okJson(obj(f("retry_strategy", v(wflOp.getRetryStrategy(), Jsons.BLANK)),
2159 f("execution_host", v(wflOp.getExecutionHost(), Jsons.BLANK)),
2160 f("failed_attempts", v(wflOp.getFailedAttempts())),
2161 f("max_attempts", v(wflOp.getMaxAttempts())),
2162 f("exception_handler_workflow", v(wflOp.getExceptionHandlingWorkflow(), Jsons.BLANK)),
2163 f("fail_on_error", v(wflOp.isFailOnError())),
2164 f("description", v(wflOp.getDescription(), Jsons.BLANK)),
2165 f("state", v(WORKFLOW_STATUS_TRANSLATION_PREFIX + wflOp.getState(), Jsons.BLANK)),
2166 f("job", v(wflOp.getId(), Jsons.BLANK)),
2167 f("name", v(wflOp.getTemplate(), Jsons.BLANK)),
2168 f("time_in_queue", v(wflOp.getTimeInQueue(), v(0))),
2169 f("started", wflOp.getDateStarted() != null ? v(toUTC(wflOp.getDateStarted().getTime())) : Jsons.BLANK),
2170 f("completed", wflOp.getDateCompleted() != null ? v(toUTC(wflOp.getDateCompleted().getTime())) : Jsons.BLANK))
2171 );
2172 }
2173 return notFound("Cannot find workflow operation of workflow %s at position %s", workflowId, operationPosition);
2174 }
2175
2176 @GET
2177 @Path("{eventId}/workflows/{workflowId}/errors.json")
2178 @Produces(MediaType.APPLICATION_JSON)
2179 @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 = {
2180 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2181 @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2182 @RestResponse(description = "Returns all the data related to the event workflow/errors tab as JSON", responseCode = HttpServletResponse.SC_OK),
2183 @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2184 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2185 public Response getEventErrors(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
2186 @Context HttpServletRequest req) throws JobEndpointException, SearchIndexException {
2187
2188
2189
2190
2191 for (final Event ignore : getIndexService().getEvent(eventId, getIndex())) {
2192 final long workflowIdLong;
2193 try {
2194 workflowIdLong = Long.parseLong(workflowId);
2195 } catch (Exception e) {
2196 logger.warn("Unable to parse workflow id {}", workflowId);
2197 return RestUtil.R.badRequest();
2198 }
2199 try {
2200 return okJson(getJobService().getIncidentsAsJSON(workflowIdLong, req.getLocale(), true));
2201 } catch (NotFoundException e) {
2202 return notFound("Cannot find the incident for the workflow %s", workflowId);
2203 }
2204 }
2205 return notFound("Cannot find an event with id '%s'.", eventId);
2206 }
2207
2208 @GET
2209 @Path("{eventId}/workflows/{workflowId}/errors/{errorId}.json")
2210 @Produces(MediaType.APPLICATION_JSON)
2211 @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 = {
2212 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING),
2213 @RestParameter(name = "workflowId", description = "The workflow id", isRequired = true, type = RestParameter.Type.STRING),
2214 @RestParameter(name = "errorId", description = "The error id", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
2215 @RestResponse(description = "Returns all the data related to the event workflow/error tab as JSON", responseCode = HttpServletResponse.SC_OK),
2216 @RestResponse(description = "Unable to parse workflowId", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2217 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
2218 public Response getEventError(@PathParam("eventId") String eventId, @PathParam("workflowId") String workflowId,
2219 @PathParam("errorId") String errorId, @Context HttpServletRequest req)
2220 throws JobEndpointException, SearchIndexException {
2221
2222
2223
2224
2225 for (Event ignore : getIndexService().getEvent(eventId, getIndex())) {
2226 final long errorIdLong;
2227 try {
2228 errorIdLong = Long.parseLong(errorId);
2229 } catch (Exception e) {
2230 logger.warn("Unable to parse error id {}", errorId);
2231 return RestUtil.R.badRequest();
2232 }
2233 try {
2234 return okJson(getJobService().getIncidentAsJSON(errorIdLong, req.getLocale()));
2235 } catch (NotFoundException e) {
2236 return notFound("Cannot find the incident %s", errorId);
2237 }
2238 }
2239 return notFound("Cannot find an event with id '%s'.", eventId);
2240 }
2241
2242 @GET
2243 @Path("{eventId}/access.json")
2244 @SuppressWarnings("unchecked")
2245 @Produces(MediaType.APPLICATION_JSON)
2246 @RestQuery(name = "getEventAccessInformation", description = "Get the access information of an event", returnDescription = "The access information", pathParameters = {
2247 @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING) }, responses = {
2248 @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required form params were missing in the request."),
2249 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event has not been found."),
2250 @RestResponse(responseCode = SC_OK, description = "The access information ") })
2251 public Response getEventAccessInformation(@PathParam("eventId") String eventId) throws Exception {
2252 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2253 if (optEvent.isNone())
2254 return notFound("Cannot find an event with id '%s'.", eventId);
2255
2256
2257 JSONArray systemAclsJson = new JSONArray();
2258 List<ManagedAcl> acls = getAclService().getAcls();
2259 for (ManagedAcl acl : acls) {
2260 systemAclsJson.add(AccessInformationUtil.serializeManagedAcl(acl));
2261 }
2262
2263 AccessControlList activeAcl = new AccessControlList();
2264 try {
2265 if (optEvent.get().getAccessPolicy() != null)
2266 activeAcl = AccessControlParser.parseAcl(optEvent.get().getAccessPolicy());
2267 } catch (Exception e) {
2268 logger.error("Unable to parse access policy", e);
2269 }
2270 Option<ManagedAcl> currentAcl = AccessInformationUtil.matchAclsLenient(acls, activeAcl,
2271 getAdminUIConfiguration().getMatchManagedAclRolePrefixes());
2272
2273 JSONObject episodeAccessJson = new JSONObject();
2274 episodeAccessJson.put("current_acl", currentAcl.isSome() ? currentAcl.get().getId() : 0L);
2275 episodeAccessJson.put("acl", AccessControlParser.toJsonSilent(activeAcl));
2276 episodeAccessJson.put("privileges", AccessInformationUtil.serializePrivilegesByRole(activeAcl));
2277 if (StringUtils.isNotBlank(optEvent.get().getWorkflowState())
2278 && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(optEvent.get().getWorkflowState())))
2279 episodeAccessJson.put("locked", true);
2280
2281 JSONObject jsonReturnObj = new JSONObject();
2282 jsonReturnObj.put("episode_access", episodeAccessJson);
2283 jsonReturnObj.put("system_acls", systemAclsJson);
2284
2285 return Response.ok(jsonReturnObj.toString()).build();
2286 }
2287
2288
2289 @POST
2290 @Path("{eventId}/assets")
2291 @Consumes(MediaType.MULTIPART_FORM_DATA)
2292 @RestQuery(name = "updateAssets", description = "Update or create an asset for the eventId by the given metadata as JSON and files in the body",
2293 pathParameters = {
2294 @RestParameter(name = "eventId", description = "The event id", isRequired = true, type = RestParameter.Type.STRING) },
2295 restParameters = {
2296 @RestParameter(name = "metadata", isRequired = true, type = RestParameter.Type.TEXT, description = "The list of asset metadata") },
2297 responses = {
2298 @RestResponse(description = "The asset has been added.", responseCode = HttpServletResponse.SC_OK),
2299 @RestResponse(description = "Could not add asset, problem with the metadata or files.", responseCode = HttpServletResponse.SC_BAD_REQUEST),
2300 @RestResponse(description = "No event with this identifier was found.", responseCode = HttpServletResponse.SC_NOT_FOUND) },
2301 returnDescription = "The workflow identifier")
2302 public Response updateAssets(@PathParam("eventId") final String eventId,
2303 @Context HttpServletRequest request) throws Exception {
2304 try {
2305 MediaPackage mp = getMediaPackageByEventId(eventId);
2306 String result = getIndexService().updateEventAssets(mp, request);
2307 return Response.status(Status.CREATED).entity(result).build();
2308 } catch (NotFoundException e) {
2309 return notFound("Cannot find an event with id '%s'.", eventId);
2310 } catch (IllegalArgumentException | UnsupportedAssetException e) {
2311 return RestUtil.R.badRequest(e.getMessage());
2312 } catch (Exception e) {
2313 return RestUtil.R.serverError();
2314 }
2315 }
2316
2317 @GET
2318 @Path("new/metadata")
2319 @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 = {
2320 @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event metadata tab as JSON") })
2321 public Response getNewMetadata() {
2322 MetadataList metadataList = new MetadataList();
2323
2324
2325 List<EventCatalogUIAdapter> extendedCatalogUIAdapters = getIndexService().getExtendedEventCatalogUIAdapters();
2326 for (EventCatalogUIAdapter extendedCatalogUIAdapter : extendedCatalogUIAdapters) {
2327 metadataList.add(extendedCatalogUIAdapter, extendedCatalogUIAdapter.getRawFields());
2328 }
2329
2330
2331
2332
2333 EventCatalogUIAdapter commonCatalogUiAdapter = getIndexService().getCommonEventCatalogUIAdapter();
2334 DublinCoreMetadataCollection commonMetadata = commonCatalogUiAdapter.getRawFields(getCollectionQueryOverrides());
2335
2336 if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_CREATED.getLocalName()))
2337 commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_CREATED.getLocalName()));
2338 if (commonMetadata.getOutputFields().containsKey("duration"))
2339 commonMetadata.removeField(commonMetadata.getOutputFields().get("duration"));
2340 if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_IDENTIFIER.getLocalName()))
2341 commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_IDENTIFIER.getLocalName()));
2342 if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_SOURCE.getLocalName()))
2343 commonMetadata.removeField(commonMetadata.getOutputFields().get(DublinCore.PROPERTY_SOURCE.getLocalName()));
2344 if (commonMetadata.getOutputFields().containsKey("startDate"))
2345 commonMetadata.removeField(commonMetadata.getOutputFields().get("startDate"));
2346 if (commonMetadata.getOutputFields().containsKey("startTime"))
2347 commonMetadata.removeField(commonMetadata.getOutputFields().get("startTime"));
2348 if (commonMetadata.getOutputFields().containsKey("location"))
2349 commonMetadata.removeField(commonMetadata.getOutputFields().get("location"));
2350
2351
2352 if (commonMetadata.getOutputFields().containsKey(DublinCore.PROPERTY_PUBLISHER.getLocalName())) {
2353 MetadataField publisher = commonMetadata.getOutputFields().get(DublinCore.PROPERTY_PUBLISHER.getLocalName());
2354 Map<String, String> users = new HashMap<>();
2355 if (publisher.getCollection() != null) {
2356 users = publisher.getCollection();
2357 }
2358 String loggedInUser = getSecurityService().getUser().getName();
2359 if (!users.containsKey(loggedInUser)) {
2360 users.put(loggedInUser, loggedInUser);
2361 }
2362 publisher.setValue(loggedInUser);
2363 }
2364
2365 metadataList.add(commonCatalogUiAdapter, commonMetadata);
2366
2367
2368 removeSeriesWithNullTitlesFromFieldCollection(metadataList);
2369
2370 return okJson(MetadataJson.listToJson(metadataList, true));
2371 }
2372
2373 @GET
2374 @Path("new/processing")
2375 @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 = {
2376 @RestParameter(name = "tags", isRequired = false, description = "A comma separated list of tags to filter the workflow definitions", type = RestParameter.Type.STRING) }, responses = {
2377 @RestResponse(responseCode = SC_OK, description = "Returns all the data related to the event processing tab as JSON") })
2378 public Response getNewProcessing(@QueryParam("tags") String tagsString) {
2379 List<String> tags = RestUtil.splitCommaSeparatedParam(Option.option(tagsString)).value();
2380
2381 List<JValue> workflows = new ArrayList<>();
2382 try {
2383 List<WorkflowDefinition> workflowsDefinitions = getWorkflowService().listAvailableWorkflowDefinitions();
2384 for (WorkflowDefinition wflDef : workflowsDefinitions) {
2385 if (wflDef.containsTag(tags)) {
2386
2387 workflows.add(obj(f("id", v(wflDef.getId())), f("tags", arr(wflDef.getTags())),
2388 f("title", v(nul(wflDef.getTitle()).getOr(""))),
2389 f("description", v(nul(wflDef.getDescription()).getOr(""))),
2390 f("displayOrder", v(wflDef.getDisplayOrder())),
2391 f("configuration_panel", v(nul(wflDef.getConfigurationPanel()).getOr(""))),
2392 f("configuration_panel_json", v(nul(wflDef.getConfigurationPanelJson()).getOr("")))));
2393 }
2394 }
2395 } catch (WorkflowDatabaseException e) {
2396 logger.error("Unable to get available workflow definitions", e);
2397 return RestUtil.R.serverError();
2398 }
2399
2400 JValue data = obj(f("workflows",arr(workflows)), f("default_workflow_id",v(defaultWorkflowDefinionId,Jsons.NULL)));
2401
2402 return okJson(data);
2403 }
2404
2405 @POST
2406 @Path("new/conflicts")
2407 @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 = {
2408 @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, responses = {
2409 @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
2410 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "There is a conflict"),
2411 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST, description = "Missing or invalid parameters") })
2412 public Response getNewConflicts(@FormParam("metadata") String metadata) throws NotFoundException {
2413 if (StringUtils.isBlank(metadata)) {
2414 logger.warn("Metadata is not specified");
2415 return Response.status(Status.BAD_REQUEST).build();
2416 }
2417
2418 JSONParser parser = new JSONParser();
2419 JSONObject metadataJson;
2420 try {
2421 metadataJson = (JSONObject) parser.parse(metadata);
2422 } catch (Exception e) {
2423 logger.warn("Unable to parse metadata {}", metadata);
2424 return RestUtil.R.badRequest("Unable to parse metadata");
2425 }
2426
2427 String device;
2428 String startDate;
2429 String endDate;
2430 try {
2431 device = (String) metadataJson.get("device");
2432 startDate = (String) metadataJson.get("start");
2433 endDate = (String) metadataJson.get("end");
2434 } catch (Exception e) {
2435 logger.warn("Unable to parse metadata {}", metadata);
2436 return RestUtil.R.badRequest("Unable to parse metadata");
2437 }
2438
2439 if (StringUtils.isBlank(device) || StringUtils.isBlank(startDate) || StringUtils.isBlank(endDate)) {
2440 logger.warn("Either device, start date or end date were not specified");
2441 return Response.status(Status.BAD_REQUEST).build();
2442 }
2443
2444 Date start;
2445 try {
2446 start = new Date(DateTimeSupport.fromUTC(startDate));
2447 } catch (Exception e) {
2448 logger.warn("Unable to parse start date {}", startDate);
2449 return RestUtil.R.badRequest("Unable to parse start date");
2450 }
2451
2452 Date end;
2453 try {
2454 end = new Date(DateTimeSupport.fromUTC(endDate));
2455 } catch (Exception e) {
2456 logger.warn("Unable to parse end date {}", endDate);
2457 return RestUtil.R.badRequest("Unable to parse end date");
2458 }
2459
2460 String rruleString = (String) metadataJson.get("rrule");
2461
2462 RRule rrule = null;
2463 TimeZone timeZone = TimeZone.getDefault();
2464 String durationString = null;
2465 if (StringUtils.isNotEmpty(rruleString)) {
2466 try {
2467 rrule = new RRule(rruleString);
2468 rrule.validate();
2469 } catch (Exception e) {
2470 logger.warn("Unable to parse rrule {}: {}", rruleString, e.getMessage());
2471 return Response.status(Status.BAD_REQUEST).build();
2472 }
2473
2474 durationString = (String) metadataJson.get("duration");
2475 if (StringUtils.isBlank(durationString)) {
2476 logger.warn("If checking recurrence, must include duration.");
2477 return Response.status(Status.BAD_REQUEST).build();
2478 }
2479
2480 Agent agent = getCaptureAgentStateService().getAgent(device);
2481 String timezone = agent.getConfiguration().getProperty("capture.device.timezone");
2482 if (StringUtils.isBlank(timezone)) {
2483 timezone = TimeZone.getDefault().getID();
2484 logger.warn("No 'capture.device.timezone' set on agent {}. The default server timezone {} will be used.",
2485 device, timezone);
2486 }
2487 timeZone = TimeZone.getTimeZone(timezone);
2488 }
2489
2490 String eventId = (String) metadataJson.get("id");
2491
2492 try {
2493 List<MediaPackage> events = null;
2494 if (StringUtils.isNotEmpty(rruleString)) {
2495 events = getSchedulerService().findConflictingEvents(device, rrule, start, end, Long.parseLong(durationString),
2496 timeZone);
2497 } else {
2498 events = getSchedulerService().findConflictingEvents(device, start, end);
2499 }
2500 if (!events.isEmpty()) {
2501 final List<JValue> eventsJSON = convertToConflictObjects(eventId, events);
2502 if (!eventsJSON.isEmpty())
2503 return conflictJson(arr(eventsJSON));
2504 }
2505 return Response.noContent().build();
2506 } catch (Exception e) {
2507 logger.error("Unable to find conflicting events for {}, {}, {}",
2508 device, startDate, endDate, e);
2509 return RestUtil.R.serverError();
2510 }
2511 }
2512
2513 private List<JValue> convertToConflictObjects(final String eventId, final List<MediaPackage> events) throws SearchIndexException {
2514 final List<JValue> eventsJSON = new ArrayList<>();
2515 final Organization organization = getSecurityService().getOrganization();
2516 final User user = SecurityUtil.createSystemUser(systemUserName, organization);
2517
2518 SecurityUtil.runAs(getSecurityService(), organization, user, () -> {
2519 try {
2520 for (final MediaPackage event : events) {
2521 final Opt<Event> eventOpt = getIndexService().getEvent(event.getIdentifier().toString(), getIndex());
2522 if (eventOpt.isSome()) {
2523 final Event e = eventOpt.get();
2524 if (StringUtils.isNotEmpty(eventId) && eventId.equals(e.getIdentifier())) {
2525 continue;
2526 }
2527 eventsJSON.add(convertEventToConflictingObject(e.getTechnicalStartTime(), e.getTechnicalEndTime(), e.getTitle()));
2528 } else {
2529 logger.warn("Index out of sync! Conflicting event catalog {} not found on event index!",
2530 event.getIdentifier().toString());
2531 }
2532 }
2533 } catch (Exception e) {
2534 logger.error("Failed to get conflicting events", e);
2535 }
2536 });
2537
2538 return eventsJSON;
2539 }
2540
2541 private JValue convertEventToConflictingObject(final String start, final String end, final String title) {
2542 return obj(
2543 f("start", v(start)),
2544 f("end", v(end)),
2545 f("title", v(title))
2546 );
2547 }
2548
2549 @POST
2550 @Path("/new")
2551 @Consumes(MediaType.MULTIPART_FORM_DATA)
2552 @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 = {
2553 @RestParameter(name = "metadata", isRequired = true, description = "The metadata as JSON", type = RestParameter.Type.TEXT) }, responses = {
2554 @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event sucessfully added"),
2555 @RestResponse(responseCode = SC_BAD_REQUEST, description = "If the metadata is not set or couldn't be parsed") })
2556 public Response createNewEvent(@Context HttpServletRequest request) {
2557 try {
2558 String result = getIndexService().createEvent(request);
2559 if (StringUtils.isEmpty(result)) {
2560 return RestUtil.R.badRequest("The date range provided did not include any events");
2561 }
2562 return Response.status(Status.CREATED).entity(result).build();
2563 } catch (IllegalArgumentException | UnsupportedAssetException e) {
2564 return RestUtil.R.badRequest(e.getMessage());
2565 } catch (Exception e) {
2566 return RestUtil.R.serverError();
2567 }
2568 }
2569
2570 @GET
2571 @Path("events.json")
2572 @Produces(MediaType.APPLICATION_JSON)
2573 @RestQuery(name = "getevents", description = "Returns all the events as JSON", returnDescription = "All the events as JSON", restParameters = {
2574 @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
2575 @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),
2576 @RestParameter(name = "limit", description = "The maximum number of items to return per page.", isRequired = false, type = RestParameter.Type.INTEGER),
2577 @RestParameter(name = "offset", description = "The page number.", isRequired = false, type = RestParameter.Type.INTEGER),
2578 @RestParameter(name = "getComments", description = "If comments should be fetched", isRequired = false, type = RestParameter.Type.BOOLEAN) }, responses = {
2579 @RestResponse(description = "Returns all events as JSON", responseCode = HttpServletResponse.SC_OK) })
2580 public Response getEvents(@QueryParam("id") String id, @QueryParam("commentReason") String reasonFilter,
2581 @QueryParam("commentResolution") String resolutionFilter, @QueryParam("filter") String filter,
2582 @QueryParam("sort") String sort, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit,
2583 @QueryParam("getComments") Boolean getComments) {
2584
2585 Option<Integer> optLimit = Option.option(limit);
2586 Option<Integer> optOffset = Option.option(offset);
2587 Option<String> optSort = Option.option(trimToNull(sort));
2588 Option<Boolean> optGetComments = Option.option(getComments);
2589 ArrayList<JValue> eventsList = new ArrayList<>();
2590 final Organization organization = getSecurityService().getOrganization();
2591 final User user = getSecurityService().getUser();
2592 if (organization == null || user == null) {
2593 return Response.status(SC_SERVICE_UNAVAILABLE).build();
2594 }
2595 EventSearchQuery query = new EventSearchQuery(organization.getId(), user);
2596
2597
2598 if (optLimit.isSome() && limit == 0) {
2599 optLimit = Option.none();
2600 }
2601
2602 Map<String, String> filters = RestUtils.parseFilter(filter);
2603 for (String name : filters.keySet()) {
2604 if (EventListQuery.FILTER_PRESENTERS_BIBLIOGRAPHIC_NAME.equals(name))
2605 query.withPresenter(filters.get(name));
2606 if (EventListQuery.FILTER_PRESENTERS_TECHNICAL_NAME.equals(name))
2607 query.withTechnicalPresenters(filters.get(name));
2608 if (EventListQuery.FILTER_CONTRIBUTORS_NAME.equals(name))
2609 query.withContributor(filters.get(name));
2610 if (EventListQuery.FILTER_LOCATION_NAME.equals(name))
2611 query.withLocation(filters.get(name));
2612 if (EventListQuery.FILTER_AGENT_NAME.equals(name))
2613 query.withAgentId(filters.get(name));
2614 if (EventListQuery.FILTER_TEXT_NAME.equals(name))
2615 query.withText(QueryPreprocessor.sanitize(filters.get(name)));
2616 if (EventListQuery.FILTER_SERIES_NAME.equals(name))
2617 query.withSeriesId(filters.get(name));
2618 if (EventListQuery.FILTER_STATUS_NAME.equals(name))
2619 query.withEventStatus(filters.get(name));
2620 if (EventListQuery.FILTER_PUBLISHER_NAME.equals(name))
2621 query.withPublisher(filters.get(name));
2622 if (EventListQuery.FILTER_COMMENTS_NAME.equals(name)) {
2623 switch (Comments.valueOf(filters.get(name))) {
2624 case NONE:
2625 query.withComments(false);
2626 break;
2627 case OPEN:
2628 query.withOpenComments(true);
2629 break;
2630 case RESOLVED:
2631 query.withComments(true);
2632 query.withOpenComments(false);
2633 break;
2634 default:
2635 logger.info("Unknown comment {}", filters.get(name));
2636 return Response.status(SC_BAD_REQUEST).build();
2637 }
2638 }
2639 if (EventListQuery.FILTER_IS_PUBLISHED_NAME.equals(name)) {
2640 if (filters.containsKey(name)) {
2641 switch (IsPublished.valueOf(filters.get(name))) {
2642 case YES:
2643 query.withIsPublished(true);
2644 break;
2645 case NO:
2646 query.withIsPublished(false);
2647 break;
2648 default:
2649 break;
2650 }
2651 } else {
2652 logger.info("Query for invalid published status: {}", filters.get(name));
2653 return Response.status(SC_BAD_REQUEST).build();
2654 }
2655 }
2656 if (EventListQuery.FILTER_STARTDATE_NAME.equals(name)) {
2657 try {
2658 Tuple<Date, Date> fromAndToCreationRange = RestUtils.getFromAndToDateRange(filters.get(name));
2659 query.withStartFrom(fromAndToCreationRange.getA());
2660 query.withStartTo(fromAndToCreationRange.getB());
2661 } catch (IllegalArgumentException e) {
2662 return RestUtil.R.badRequest(e.getMessage());
2663 }
2664 }
2665 }
2666
2667 if (optSort.isSome()) {
2668 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
2669 for (SortCriterion criterion : sortCriteria) {
2670 switch (criterion.getFieldName()) {
2671 case EventIndexSchema.UID:
2672 query.sortByUID(criterion.getOrder());
2673 break;
2674 case EventIndexSchema.TITLE:
2675 query.sortByTitle(criterion.getOrder());
2676 break;
2677 case EventIndexSchema.PRESENTER:
2678 query.sortByPresenter(criterion.getOrder());
2679 break;
2680 case EventIndexSchema.TECHNICAL_START:
2681 case "technical_date":
2682 query.sortByTechnicalStartDate(criterion.getOrder());
2683 break;
2684 case EventIndexSchema.TECHNICAL_END:
2685 query.sortByTechnicalEndDate(criterion.getOrder());
2686 break;
2687 case EventIndexSchema.PUBLICATION:
2688 query.sortByPublicationIgnoringInternal(criterion.getOrder());
2689 break;
2690 case EventIndexSchema.START_DATE:
2691 case "date":
2692 query.sortByStartDate(criterion.getOrder());
2693 break;
2694 case EventIndexSchema.END_DATE:
2695 query.sortByEndDate(criterion.getOrder());
2696 break;
2697 case EventIndexSchema.SERIES_NAME:
2698 query.sortBySeriesName(criterion.getOrder());
2699 break;
2700 case EventIndexSchema.LOCATION:
2701 query.sortByLocation(criterion.getOrder());
2702 break;
2703 case EventIndexSchema.EVENT_STATUS:
2704 query.sortByEventStatus(criterion.getOrder());
2705 break;
2706 default:
2707 final String msg = String.format("Unknown sort criteria field %s", criterion.getFieldName());
2708 logger.debug(msg);
2709 return RestUtil.R.badRequest(msg);
2710 }
2711 }
2712 }
2713
2714
2715 if (getOnlyEventsWithWriteAccessEventsTab()) {
2716 query.withoutActions();
2717 query.withAction(Permissions.Action.WRITE);
2718 query.withAction(Permissions.Action.READ);
2719 }
2720
2721 if (optLimit.isSome())
2722 query.withLimit(optLimit.get());
2723 if (optOffset.isSome())
2724 query.withOffset(offset);
2725
2726
2727 SearchResult<Event> results = null;
2728 try {
2729 results = getIndex().getByQuery(query);
2730 } catch (SearchIndexException e) {
2731 logger.error("The admin UI Search Index was not able to get the events list:", e);
2732 return RestUtil.R.serverError();
2733 }
2734
2735
2736 if (results.getPageSize() == 0) {
2737 logger.debug("No events match the given filters.");
2738 return okJsonList(eventsList, nul(offset).getOr(0), nul(limit).getOr(0), 0);
2739 }
2740
2741 for (SearchResultItem<Event> item : results.getItems()) {
2742 Event source = item.getSource();
2743 source.updatePreview(getAdminUIConfiguration().getPreviewSubtype());
2744 List<EventComment> comments = null;
2745 if (optGetComments.isSome() && optGetComments.get()) {
2746 try {
2747 comments = getEventCommentService().getComments(source.getIdentifier());
2748 } catch (EventCommentException e) {
2749 logger.error("Unable to get comments from event {}", source.getIdentifier(), e);
2750 throw new WebApplicationException(e);
2751 }
2752 }
2753 eventsList.add(eventToJSON(source, Optional.ofNullable(comments)));
2754 }
2755
2756 return okJsonList(eventsList, nul(offset).getOr(0), nul(limit).getOr(0), results.getHitCount());
2757 }
2758
2759
2760
2761 private MediaPackage getMediaPackageByEventId(String eventId)
2762 throws SearchIndexException, NotFoundException, IndexServiceException {
2763 Opt<Event> optEvent = getIndexService().getEvent(eventId, getIndex());
2764 if (optEvent.isNone())
2765 throw new NotFoundException(format("Cannot find an event with id '%s'.", eventId));
2766 return getIndexService().getEventMediapackage(optEvent.get());
2767 }
2768
2769 private URI getCommentUrl(String eventId, long commentId) {
2770 return UrlSupport.uri(serverUrl, eventId, "comment", Long.toString(commentId));
2771 }
2772
2773 private JValue eventToJSON(Event event, Optional<List<EventComment>> comments) {
2774 List<Field> fields = new ArrayList<>();
2775
2776 fields.add(f("id", v(event.getIdentifier())));
2777 fields.add(f("title", v(event.getTitle(), BLANK)));
2778 fields.add(f("source", v(event.getSource(), BLANK)));
2779 fields.add(f("presenters", arr($(event.getPresenters()).map(Functions.stringToJValue))));
2780 if (StringUtils.isNotBlank(event.getSeriesId())) {
2781 String seriesTitle = event.getSeriesName();
2782 String seriesID = event.getSeriesId();
2783
2784 fields.add(f("series", obj(f("id", v(seriesID, BLANK)), f("title", v(seriesTitle, BLANK)))));
2785 }
2786 fields.add(f("location", v(event.getLocation(), BLANK)));
2787 fields.add(f("start_date", v(event.getRecordingStartDate(), BLANK)));
2788 fields.add(f("end_date", v(event.getRecordingEndDate(), BLANK)));
2789 fields.add(f("managedAcl", v(event.getManagedAcl(), BLANK)));
2790 fields.add(f("workflow_state", v(event.getWorkflowState(), BLANK)));
2791 fields.add(f("event_status", v(event.getEventStatus())));
2792 fields.add(f("displayable_status", v(event.getDisplayableStatus(getWorkflowService().getWorkflowStateMappings()))));
2793 fields.add(f("source", v(getIndexService().getEventSource(event).toString())));
2794 fields.add(f("has_comments", v(event.hasComments())));
2795 fields.add(f("has_open_comments", v(event.hasOpenComments())));
2796 fields.add(f("needs_cutting", v(event.needsCutting())));
2797 fields.add(f("has_preview", v(event.hasPreview())));
2798 fields.add(f("agent_id", v(event.getAgentId(), BLANK)));
2799 fields.add(f("technical_start", v(event.getTechnicalStartTime(), BLANK)));
2800 fields.add(f("technical_end", v(event.getTechnicalEndTime(), BLANK)));
2801 fields.add(f("technical_presenters", arr($(event.getTechnicalPresenters()).map(Functions.stringToJValue))));
2802 fields.add(f("publications", arr(eventPublicationsToJson(event))));
2803 if (comments.isPresent()) {
2804 fields.add(f("comments", arr(eventCommentsToJson(comments.get()))));
2805 }
2806 return obj(fields);
2807 }
2808
2809 private JValue attachmentToJSON(Attachment attachment) {
2810 List<Field> fields = new ArrayList<>();
2811 fields.addAll(getEventMediaPackageElementFields(attachment));
2812 fields.addAll(getCommonElementFields(attachment));
2813 return obj(fields);
2814 }
2815
2816 private JValue catalogToJSON(Catalog catalog) {
2817 List<Field> fields = new ArrayList<>();
2818 fields.addAll(getEventMediaPackageElementFields(catalog));
2819 fields.addAll(getCommonElementFields(catalog));
2820 return obj(fields);
2821 }
2822
2823 private JValue trackToJSON(Track track) {
2824 List<Field> fields = new ArrayList<>();
2825 fields.addAll(getEventMediaPackageElementFields(track));
2826 fields.addAll(getCommonElementFields(track));
2827 fields.add(f("duration", v(track.getDuration(), BLANK)));
2828 fields.add(f("has_audio", v(track.hasAudio())));
2829 fields.add(f("has_video", v(track.hasVideo())));
2830 fields.add(f("has_subtitle", v(track.hasSubtitle())));
2831 fields.add(f("streams", obj(streamsToJSON(track.getStreams()))));
2832 return obj(fields);
2833 }
2834
2835 private List<Field> streamsToJSON(org.opencastproject.mediapackage.Stream[] streams) {
2836 List<Field> fields = new ArrayList<>();
2837 List<JValue> audioList = new ArrayList<>();
2838 List<JValue> videoList = new ArrayList<>();
2839 List<JValue> subtitleList = new ArrayList<>();
2840 for (org.opencastproject.mediapackage.Stream stream : streams) {
2841
2842 if (stream instanceof AudioStreamImpl) {
2843 List<Field> audio = new ArrayList<>();
2844 AudioStream audioStream = (AudioStream) stream;
2845 audio.add(f("id", v(audioStream.getIdentifier(), BLANK)));
2846 audio.add(f("type", v(audioStream.getFormat(), BLANK)));
2847 audio.add(f("channels", v(audioStream.getChannels(), BLANK)));
2848 audio.add(f("bitrate", v(audioStream.getBitRate(), BLANK)));
2849 audio.add(f("bitdepth", v(audioStream.getBitDepth(), BLANK)));
2850 audio.add(f("samplingrate", v(audioStream.getSamplingRate(), BLANK)));
2851 audio.add(f("framecount", v(audioStream.getFrameCount(), BLANK)));
2852 audio.add(f("peakleveldb", v(audioStream.getPkLevDb(), BLANK)));
2853 audio.add(f("rmsleveldb", v(audioStream.getRmsLevDb(), BLANK)));
2854 audio.add(f("rmspeakdb", v(audioStream.getRmsPkDb(), BLANK)));
2855 audioList.add(obj(audio));
2856 } else if (stream instanceof VideoStreamImpl) {
2857 List<Field> video = new ArrayList<>();
2858 VideoStream videoStream = (VideoStream) stream;
2859 video.add(f("id", v(videoStream.getIdentifier(), BLANK)));
2860 video.add(f("type", v(videoStream.getFormat(), BLANK)));
2861 video.add(f("bitrate", v(videoStream.getBitRate(), BLANK)));
2862 video.add(f("framerate", v(videoStream.getFrameRate(), BLANK)));
2863 video.add(f("resolution", v(videoStream.getFrameWidth() + "x" + videoStream.getFrameHeight(), BLANK)));
2864 video.add(f("framecount", v(videoStream.getFrameCount(), BLANK)));
2865 video.add(f("scantype", v(videoStream.getScanType(), BLANK)));
2866 video.add(f("scanorder", v(videoStream.getScanOrder(), BLANK)));
2867 videoList.add(obj(video));
2868 } else if (stream instanceof SubtitleStreamImpl) {
2869 List<Field> subtitle = new ArrayList<>();
2870 SubtitleStreamImpl subtitleStream = (SubtitleStreamImpl) stream;
2871 subtitle.add(f("id", v(subtitleStream.getIdentifier(), BLANK)));
2872 subtitle.add(f("type", v(subtitleStream.getFormat(), BLANK)));
2873 subtitleList.add(obj(subtitle));
2874 } else {
2875 throw new IllegalArgumentException("Stream must be either audio, video or subtitle");
2876 }
2877 }
2878 fields.add(f("audio", arr(audioList)));
2879 fields.add(f("video", arr(videoList)));
2880 fields.add(f("subtitle", arr(subtitleList)));
2881 return fields;
2882 }
2883
2884 private JValue publicationToJSON(Publication publication) {
2885 List<Field> fields = new ArrayList<>();
2886 fields.add(f("id", v(publication.getIdentifier(), BLANK)));
2887 fields.add(f("channel", v(publication.getChannel(), BLANK)));
2888 fields.add(f("mimetype", v(publication.getMimeType(), BLANK)));
2889 fields.add(f("tags", arr($(publication.getTags()).map(toStringJValue))));
2890 fields.add(f("url", v(signUrl(publication.getURI()), BLANK)));
2891 fields.addAll(getCommonElementFields(publication));
2892 return obj(fields);
2893 }
2894
2895 private List<Field> getCommonElementFields(MediaPackageElement element) {
2896 List<Field> fields = new ArrayList<>();
2897 fields.add(f("size", v(element.getSize(), BLANK)));
2898 fields.add(f("checksum", v(element.getChecksum() != null ? element.getChecksum().getValue() : null, BLANK)));
2899 fields.add(f("reference", v(element.getReference() != null ? element.getReference().getIdentifier() : null, BLANK)));
2900 return fields;
2901 }
2902
2903
2904
2905
2906
2907
2908
2909
2910 private List<JValue> getEventPublications(Publication[] publications) {
2911 List<JValue> publicationJSON = new ArrayList<>();
2912 for (Publication publication : publications) {
2913 publicationJSON.add(obj(f("id", v(publication.getIdentifier(), BLANK)),
2914 f("channel", v(publication.getChannel(), BLANK)), f("mimetype", v(publication.getMimeType(), BLANK)),
2915 f("tags", arr($(publication.getTags()).map(toStringJValue))),
2916 f("url", v(signUrl(publication.getURI()), BLANK))));
2917 }
2918 return publicationJSON;
2919 }
2920
2921 private URI signUrl(URI url) {
2922 if (url == null) {
2923 return null;
2924 }
2925 if (getUrlSigningService().accepts(url.toString())) {
2926 try {
2927 String clientIP = null;
2928 if (signWithClientIP()) {
2929 clientIP = getSecurityService().getUserIP();
2930 }
2931 return URI.create(getUrlSigningService().sign(url.toString(), getUrlSigningExpireDuration(), null, clientIP));
2932 } catch (UrlSigningException e) {
2933 logger.warn("Unable to sign url '{}'", url, e);
2934 }
2935 }
2936 return url;
2937 }
2938
2939
2940
2941
2942
2943
2944
2945
2946 private List<JValue> getEventMediaPackageElements(MediaPackageElement[] elements) {
2947 List<JValue> elementJSON = new ArrayList<>();
2948 for (MediaPackageElement element : elements) {
2949 elementJSON.add(obj(getEventMediaPackageElementFields(element)));
2950 }
2951 return elementJSON;
2952 }
2953
2954 private List<Field> getEventMediaPackageElementFields(MediaPackageElement element) {
2955 List<Field> fields = new ArrayList<>();
2956 fields.add(f("id", v(element.getIdentifier(), BLANK)));
2957 fields.add(f("type", v(element.getFlavor(), BLANK)));
2958 fields.add(f("mimetype", v(element.getMimeType(), BLANK)));
2959 List<JValue> tags = Stream.$(element.getTags()).map(toStringJValue).toList();
2960 fields.add(f("tags", arr(tags)));
2961 fields.add(f("url", v(signUrl(element.getURI()), BLANK)));
2962 return fields;
2963 }
2964
2965 private static final Fn<String, JValue> toStringJValue = new Fn<String, JValue>() {
2966 @Override
2967 public JValue apply(String stringValue) {
2968 return v(stringValue, BLANK);
2969 }
2970 };
2971
2972 private final Fn<Publication, JObject> publicationToJson = new Fn<Publication, JObject>() {
2973 @Override
2974 public JObject apply(Publication publication) {
2975 final Opt<String> channel = Opt.nul(EventUtils.PUBLICATION_CHANNELS.get(publication.getChannel()));
2976 String url = publication.getURI() == null ? "" : signUrl(publication.getURI()).toString();
2977 return obj(f("id", v(publication.getChannel())),
2978 f("name", v(channel.getOr("EVENTS.EVENTS.DETAILS.PUBLICATIONS.CUSTOM"))), f("url", v(url, NULL)));
2979 }
2980 };
2981
2982 protected static final Fn<TechnicalMetadata, JObject> technicalMetadataToJson = new Fn<TechnicalMetadata, JObject>() {
2983 @Override
2984 public JObject apply(TechnicalMetadata technicalMetadata) {
2985 JValue agentConfig = technicalMetadata.getCaptureAgentConfiguration() == null ? v("")
2986 : JSONUtils.mapToJSON(technicalMetadata.getCaptureAgentConfiguration());
2987 JValue start = technicalMetadata.getStartDate() == null ? v("")
2988 : v(DateTimeSupport.toUTC(technicalMetadata.getStartDate().getTime()));
2989 JValue end = technicalMetadata.getEndDate() == null ? v("")
2990 : v(DateTimeSupport.toUTC(technicalMetadata.getEndDate().getTime()));
2991 return obj(f("agentId", v(technicalMetadata.getAgentId(), BLANK)), f("agentConfiguration", agentConfig),
2992 f("start", start), f("end", end), f("eventId", v(technicalMetadata.getEventId(), BLANK)),
2993 f("presenters", JSONUtils.setToJSON(technicalMetadata.getPresenters())),
2994 f("recording", recordingToJson.apply(technicalMetadata.getRecording())));
2995 }
2996 };
2997
2998 protected static final Fn<Opt<Recording>, JObject> recordingToJson = new Fn<Opt<Recording>, JObject>() {
2999 @Override
3000 public JObject apply(Opt<Recording> recording) {
3001 if (recording.isNone()) {
3002 return obj();
3003 }
3004 return obj(f("id", v(recording.get().getID(), BLANK)),
3005 f("lastCheckInTime", v(recording.get().getLastCheckinTime(), BLANK)),
3006 f("lastCheckInTimeUTC", v(toUTC(recording.get().getLastCheckinTime()), BLANK)),
3007 f("state", v(recording.get().getState(), BLANK)));
3008 }
3009 };
3010
3011 @PUT
3012 @Path("{eventId}/workflows/{workflowId}/action/{action}")
3013 @RestQuery(name = "workflowAction", description = "Performs the given action for the given workflow.", returnDescription = "", pathParameters = {
3014 @RestParameter(name = "eventId", description = "The id of the media package", isRequired = true, type = RestParameter.Type.STRING),
3015 @RestParameter(name = "workflowId", description = "The id of the workflow", isRequired = true, type = RestParameter.Type.STRING),
3016 @RestParameter(name = "action", description = "The action to take: STOP, RETRY or NONE (abort processing)", isRequired = true, type = RestParameter.Type.STRING) }, responses = {
3017 @RestResponse(responseCode = SC_OK, description = "Workflow resumed."),
3018 @RestResponse(responseCode = SC_NOT_FOUND, description = "Event or workflow instance not found."),
3019 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid action entered."),
3020 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to perform the action. Maybe you need to authenticate."),
3021 @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "An exception occurred.") })
3022 public Response workflowAction(@PathParam("eventId") String id, @PathParam("workflowId") long wfId,
3023 @PathParam("action") String action) {
3024 if (StringUtils.isEmpty(id) || StringUtils.isEmpty(action)) {
3025 return badRequest();
3026 }
3027
3028 try {
3029 final Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
3030 if (optEvent.isNone()) {
3031 return notFound("Cannot find an event with id '%s'.", id);
3032 }
3033
3034 final WorkflowInstance wfInstance = getWorkflowService().getWorkflowById(wfId);
3035 if (!wfInstance.getMediaPackage().getIdentifier().toString().equals(id)) {
3036 return badRequest(String.format("Workflow %s is not associated to event %s", wfId, id));
3037 }
3038
3039 if (RetryStrategy.NONE.toString().equalsIgnoreCase(action)
3040 || RetryStrategy.RETRY.toString().equalsIgnoreCase(action)) {
3041 getWorkflowService().resume(wfId, Collections.singletonMap("retryStrategy", action));
3042 return ok();
3043 }
3044
3045 if (WORKFLOW_ACTION_STOP.equalsIgnoreCase(action)) {
3046 getWorkflowService().stop(wfId);
3047 return ok();
3048 }
3049
3050 return badRequest("Action not supported: " + action);
3051 } catch (NotFoundException e) {
3052 return notFound("Workflow not found: '%d'.", wfId);
3053 } catch (IllegalStateException e) {
3054 return badRequest(String.format("Action %s not allowed for current workflow state. EventId: %s", action, id));
3055 } catch (UnauthorizedException e) {
3056 return forbidden();
3057 } catch (Exception e) {
3058 return serverError();
3059 }
3060 }
3061
3062 @DELETE
3063 @Path("{eventId}/workflows/{workflowId}")
3064 @RestQuery(name = "deleteWorkflow", description = "Deletes a workflow", returnDescription = "The method doesn't return any content", pathParameters = {
3065 @RestParameter(name = "eventId", isRequired = true, description = "The event identifier", type = RestParameter.Type.STRING),
3066 @RestParameter(name = "workflowId", isRequired = true, description = "The workflow identifier", type = RestParameter.Type.INTEGER) }, responses = {
3067 @RestResponse(responseCode = SC_BAD_REQUEST, description = "When trying to delete the latest workflow of the event."),
3068 @RestResponse(responseCode = SC_NOT_FOUND, description = "If the event or the workflow has not been found."),
3069 @RestResponse(responseCode = SC_NO_CONTENT, description = "The method does not return any content") })
3070 public Response deleteWorkflow(@PathParam("eventId") String id, @PathParam("workflowId") long wfId)
3071 throws SearchIndexException {
3072 final Opt<Event> optEvent = getIndexService().getEvent(id, getIndex());
3073 try {
3074 if (optEvent.isNone()) {
3075 return notFound("Cannot find an event with id '%s'.", id);
3076 }
3077
3078 final WorkflowInstance wfInstance = getWorkflowService().getWorkflowById(wfId);
3079 if (!wfInstance.getMediaPackage().getIdentifier().toString().equals(id)) {
3080 return badRequest(String.format("Workflow %s is not associated to event %s", wfId, id));
3081 }
3082
3083 if (wfId == optEvent.get().getWorkflowId()) {
3084 return badRequest(String.format("Cannot delete current workflow %s from event %s."
3085 + " Only older workflows can be deleted.", wfId, id));
3086 }
3087
3088 getWorkflowService().remove(wfId);
3089
3090 return Response.noContent().build();
3091 } catch (WorkflowStateException e) {
3092 return badRequest("Deleting is not allowed for current workflow state. EventId: " + id);
3093 } catch (NotFoundException e) {
3094 return notFound("Workflow not found: '%d'.", wfId);
3095 } catch (UnauthorizedException e) {
3096 return forbidden();
3097 } catch (Exception e) {
3098 return serverError();
3099 }
3100 }
3101
3102 private Opt<Event> checkAgentAccessForEvent(final String eventId) throws UnauthorizedException, SearchIndexException {
3103 final Opt<Event> event = getIndexService().getEvent(eventId, getIndex());
3104 if (event.isNone() || !event.get().getEventStatus().contains("SCHEDULE")) {
3105 return event;
3106 }
3107 SecurityUtil.checkAgentAccess(getSecurityService(), event.get().getAgentId());
3108 return event;
3109 }
3110
3111 private void checkAgentAccessForAgent(final String agentId) throws UnauthorizedException {
3112 SecurityUtil.checkAgentAccess(getSecurityService(), agentId);
3113 }
3114
3115 }