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.scheduler.endpoint;
23
24 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25 import static javax.servlet.http.HttpServletResponse.SC_OK;
26 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
27 import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
28 import static org.opencastproject.capture.CaptureParameters.AGENT_REGISTRATION_TYPE;
29 import static org.opencastproject.capture.CaptureParameters.AGENT_REGISTRATION_TYPE_ADHOC;
30 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATED;
31 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SPATIAL;
32 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TEMPORAL;
33 import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TITLE;
34 import static org.opencastproject.util.Jsons.arr;
35 import static org.opencastproject.util.Jsons.obj;
36 import static org.opencastproject.util.Jsons.p;
37 import static org.opencastproject.util.Jsons.v;
38 import static org.opencastproject.util.RestUtil.generateErrorResponse;
39 import static org.opencastproject.util.data.Monadics.mlist;
40
41 import org.opencastproject.capture.admin.api.Agent;
42 import org.opencastproject.capture.admin.api.AgentState;
43 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
44 import org.opencastproject.mediapackage.Catalog;
45 import org.opencastproject.mediapackage.MediaPackage;
46 import org.opencastproject.mediapackage.MediaPackageBuilderFactory;
47 import org.opencastproject.mediapackage.MediaPackageElement;
48 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
49 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
50 import org.opencastproject.mediapackage.MediaPackageElements;
51 import org.opencastproject.mediapackage.MediaPackageException;
52 import org.opencastproject.mediapackage.MediaPackageParser;
53 import org.opencastproject.metadata.dublincore.DCMIPeriod;
54 import org.opencastproject.metadata.dublincore.DublinCore;
55 import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
56 import org.opencastproject.metadata.dublincore.DublinCoreUtil;
57 import org.opencastproject.metadata.dublincore.DublinCores;
58 import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
59 import org.opencastproject.metadata.dublincore.Precision;
60 import org.opencastproject.rest.RestConstants;
61 import org.opencastproject.scheduler.api.Recording;
62 import org.opencastproject.scheduler.api.SchedulerConflictException;
63 import org.opencastproject.scheduler.api.SchedulerException;
64 import org.opencastproject.scheduler.api.SchedulerService;
65 import org.opencastproject.scheduler.api.TechnicalMetadata;
66 import org.opencastproject.scheduler.impl.CaptureNowProlongingService;
67 import org.opencastproject.security.api.UnauthorizedException;
68 import org.opencastproject.systems.OpencastConstants;
69 import org.opencastproject.util.DateTimeSupport;
70 import org.opencastproject.util.Jsons;
71 import org.opencastproject.util.Jsons.Arr;
72 import org.opencastproject.util.Jsons.Prop;
73 import org.opencastproject.util.Jsons.Val;
74 import org.opencastproject.util.NotFoundException;
75 import org.opencastproject.util.RestUtil;
76 import org.opencastproject.util.UrlSupport;
77 import org.opencastproject.util.doc.rest.RestParameter;
78 import org.opencastproject.util.doc.rest.RestParameter.Type;
79 import org.opencastproject.util.doc.rest.RestQuery;
80 import org.opencastproject.util.doc.rest.RestResponse;
81 import org.opencastproject.util.doc.rest.RestService;
82 import org.opencastproject.workspace.api.Workspace;
83
84 import com.google.gson.Gson;
85 import com.google.gson.GsonBuilder;
86 import com.google.gson.JsonPrimitive;
87 import com.google.gson.JsonSerializer;
88
89 import net.fortuna.ical4j.model.property.RRule;
90
91 import org.apache.commons.io.IOUtils;
92 import org.apache.commons.lang3.StringUtils;
93 import org.joda.time.DateTime;
94 import org.joda.time.DateTimeZone;
95 import org.json.simple.JSONArray;
96 import org.json.simple.JSONObject;
97 import org.json.simple.parser.JSONParser;
98 import org.osgi.service.component.ComponentContext;
99 import org.osgi.service.component.annotations.Activate;
100 import org.osgi.service.component.annotations.Component;
101 import org.osgi.service.component.annotations.Reference;
102 import org.osgi.service.component.annotations.ReferenceCardinality;
103 import org.osgi.service.component.annotations.ReferencePolicy;
104 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
105 import org.slf4j.Logger;
106 import org.slf4j.LoggerFactory;
107
108 import java.io.IOException;
109 import java.io.InputStream;
110 import java.io.StringReader;
111 import java.net.URI;
112 import java.text.ParseException;
113 import java.util.ArrayList;
114 import java.util.Arrays;
115 import java.util.Collections;
116 import java.util.Date;
117 import java.util.HashMap;
118 import java.util.HashSet;
119 import java.util.List;
120 import java.util.Map;
121 import java.util.Map.Entry;
122 import java.util.Objects;
123 import java.util.Optional;
124 import java.util.Properties;
125 import java.util.Set;
126 import java.util.TimeZone;
127 import java.util.UUID;
128
129 import javax.servlet.http.HttpServletRequest;
130 import javax.servlet.http.HttpServletResponse;
131 import javax.ws.rs.DELETE;
132 import javax.ws.rs.FormParam;
133 import javax.ws.rs.GET;
134 import javax.ws.rs.POST;
135 import javax.ws.rs.PUT;
136 import javax.ws.rs.Path;
137 import javax.ws.rs.PathParam;
138 import javax.ws.rs.Produces;
139 import javax.ws.rs.QueryParam;
140 import javax.ws.rs.WebApplicationException;
141 import javax.ws.rs.core.Context;
142 import javax.ws.rs.core.HttpHeaders;
143 import javax.ws.rs.core.MediaType;
144 import javax.ws.rs.core.Response;
145 import javax.ws.rs.core.Response.ResponseBuilder;
146 import javax.ws.rs.core.Response.Status;
147
148
149
150
151 @Path("/recordings")
152 @RestService(name = "schedulerservice",
153 title = "Scheduler Service",
154 abstractText = "This service creates, edits and retrieves and helps managing scheduled capture events.",
155 notes = {
156 "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
157 "If the service is down or not working it will return a status 503, this means the the underlying service is "
158 + "not working and is either restarting or has failed",
159 "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
160 + "other words, there is a bug! You should file an error report with your server logs from the time "
161 + "when the error occurred: "
162 + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
163 }
164 )
165 @Component(
166 immediate = true,
167 service = SchedulerRestService.class,
168 property = {
169 "service.description=Scheduler REST Endpoint",
170 "opencast.service.type=org.opencastproject.scheduler",
171 "opencast.service.path=/recordings"
172 }
173 )
174 @JaxrsResource
175 public class SchedulerRestService {
176
177 private static final Logger logger = LoggerFactory.getLogger(SchedulerRestService.class);
178
179
180 private static final String DEFAULT_WORKFLOW_DEFINITION = "org.opencastproject.workflow.default.definition";
181
182 private SchedulerService service;
183 private CaptureAgentStateService agentService;
184 private CaptureNowProlongingService prolongingService;
185 private Workspace workspace;
186
187 private final Gson gson = new Gson();
188 private final Gson gsonTimestamp = new GsonBuilder()
189 .registerTypeAdapter(
190 Date.class,
191 (JsonSerializer<Date>) (date, type, jsonSerializationContext) -> new JsonPrimitive(date.getTime()))
192 .create();
193
194 private String defaultWorkflowDefinitionId;
195
196 protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
197 protected String serviceUrl = null;
198
199
200
201
202
203
204 @Reference(
205 policy = ReferencePolicy.DYNAMIC,
206 unbind = "unsetService"
207 )
208 public void setService(SchedulerService service) {
209 this.service = service;
210 }
211
212
213
214
215
216
217 public void unsetService(SchedulerService service) {
218 if (this.service == service) {
219 this.service = null;
220 }
221 }
222
223
224
225
226
227
228 @Reference(
229 policy = ReferencePolicy.DYNAMIC,
230 unbind = "unsetProlongingService"
231 )
232 public void setProlongingService(CaptureNowProlongingService prolongingService) {
233 this.prolongingService = prolongingService;
234 }
235
236
237
238
239
240
241 public void unsetProlongingService(CaptureNowProlongingService prolongingService) {
242 if (this.prolongingService == prolongingService) {
243 this.prolongingService = null;
244 }
245 }
246
247
248
249
250
251
252 @Reference(
253 cardinality = ReferenceCardinality.OPTIONAL,
254 policy = ReferencePolicy.DYNAMIC,
255 unbind = "unsetCaptureAgentStateService"
256 )
257 public void setCaptureAgentStateService(CaptureAgentStateService agentService) {
258 this.agentService = agentService;
259 }
260
261
262
263
264
265
266 public void unsetCaptureAgentStateService(CaptureAgentStateService agentService) {
267 if (this.agentService == agentService) {
268 this.agentService = null;
269 }
270 }
271
272
273
274
275
276
277 @Reference
278 public void setWorkspace(Workspace workspace) {
279 this.workspace = workspace;
280 }
281
282
283
284
285
286
287
288 @Activate
289 public void activate(ComponentContext cc) {
290
291 if (cc != null) {
292 String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
293 logger.debug("configured server url is {}", ccServerUrl);
294 if (ccServerUrl == null) {
295 serverUrl = UrlSupport.DEFAULT_BASE_URL;
296 } else {
297 serverUrl = ccServerUrl;
298 }
299 serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
300 defaultWorkflowDefinitionId = StringUtils
301 .trimToNull(cc.getBundleContext().getProperty(DEFAULT_WORKFLOW_DEFINITION));
302 if (defaultWorkflowDefinitionId == null) {
303 defaultWorkflowDefinitionId = "schedule-and-upload";
304 }
305 }
306 }
307
308
309
310
311
312
313
314
315 @GET
316 @Produces(MediaType.TEXT_XML)
317 @Path("{id:.+}/mediapackage.xml")
318 @RestQuery(name = "getmediapackagexml",
319 description = "Retrieves media package for specified event",
320 returnDescription = "media package in XML",
321 pathParameters = {
322 @RestParameter(name = "id", isRequired = true, description = "ID of event for which media package will be "
323 + "retrieved", type = Type.STRING)
324 },
325 responses = {
326 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of event is in the body "
327 + "of response"),
328 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does "
329 + "not exist"),
330 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
331 + "to remove the event. Maybe you need to authenticate.")
332 }
333 )
334 public Response getMediaPackageXml(@PathParam("id") String eventId) throws UnauthorizedException {
335 try {
336 MediaPackage result = service.getMediaPackage(eventId);
337 return Response.ok(MediaPackageParser.getAsXml(result)).build();
338 } catch (NotFoundException e) {
339 logger.info("Event with id '{}' does not exist.", eventId);
340 return Response.status(Status.NOT_FOUND).build();
341 } catch (SchedulerException e) {
342 logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
343 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
344 }
345 }
346
347
348
349
350
351
352
353
354 @GET
355 @Produces(MediaType.TEXT_XML)
356 @Path("{id:.+}/dublincore.xml")
357 @RestQuery(name = "recordingsasxml",
358 description = "Retrieves DublinCore for specified event",
359 returnDescription = "DublinCore in XML",
360 pathParameters = {
361 @RestParameter(name = "id", isRequired = true, description = "ID of event for which DublinCore will be "
362 + "retrieved", type = Type.STRING)
363 },
364 responses = {
365 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of event is in the body "
366 + "of response"),
367 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does "
368 + "not exist"),
369 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
370 + "to remove the event. Maybe you need to authenticate.")
371 }
372 )
373 public Response getDublinCoreMetadataXml(@PathParam("id") String eventId) throws UnauthorizedException {
374 try {
375 DublinCoreCatalog result = service.getDublinCore(eventId);
376 return Response.ok(result.toXmlString()).build();
377 } catch (NotFoundException e) {
378 logger.info("Event with id '{}' does not exist.", eventId);
379 return Response.status(Status.NOT_FOUND).build();
380 } catch (UnauthorizedException e) {
381 throw e;
382 } catch (Exception e) {
383 logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
384 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
385 }
386 }
387
388
389
390
391
392
393
394
395 @GET
396 @Produces(MediaType.APPLICATION_JSON)
397 @Path("{id:.+}/dublincore.json")
398 @RestQuery(name = "recordingsasjson",
399 description = "Retrieves DublinCore for specified event",
400 returnDescription = "DublinCore in JSON",
401 pathParameters = {
402 @RestParameter(name = "id", isRequired = true, description = "ID of event for which DublinCore will be "
403 + "retrieved", type = Type.STRING)
404 },
405 responses = {
406 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "DublinCore of event is in the body "
407 + "of response"),
408 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does "
409 + "not exist"),
410 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
411 + "to remove the event. Maybe you need to authenticate.")
412 }
413 )
414 public Response getDublinCoreMetadataJSON(@PathParam("id") String eventId) throws UnauthorizedException {
415 try {
416 DublinCoreCatalog result = service.getDublinCore(eventId);
417 return Response.ok(result.toJson()).build();
418 } catch (NotFoundException e) {
419 logger.info("Event with id '{}' does not exist.", eventId);
420 return Response.status(Status.NOT_FOUND).build();
421 } catch (UnauthorizedException e) {
422 throw e;
423 } catch (Exception e) {
424 logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
425 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
426 }
427 }
428
429
430
431
432
433
434
435
436 @GET
437 @Produces(MediaType.TEXT_XML)
438 @Path("{id:.+}/technical.json")
439 @RestQuery(name = "gettechnicalmetadatajson",
440 description = "Retrieves the technical metadata for specified event",
441 returnDescription = "technical metadata as JSON",
442 pathParameters = {
443 @RestParameter(name = "id", isRequired = true, description = "ID of event for which the technical metadata "
444 + "will be retrieved", type = Type.STRING)
445 },
446 responses = {
447 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "technical metadata of event is in "
448 + "the body of response"),
449 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does "
450 + "not exist"),
451 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
452 + "to remove the event. Maybe you need to authenticate.")
453 }
454 )
455 public Response getTechnicalMetadataJSON(@PathParam("id") String eventId) throws UnauthorizedException {
456 try {
457 TechnicalMetadata metadata = service.getTechnicalMetadata(eventId);
458
459 Val state = v("");
460 Val lastHeard = v("");
461 if (metadata.getRecording().isPresent()) {
462 state = v(metadata.getRecording().get().getState());
463 lastHeard = v(DateTimeSupport.toUTC(metadata.getRecording().get().getLastCheckinTime()));
464 }
465
466 Arr presenters = arr(mlist(metadata.getPresenters()).map(Jsons.stringVal));
467 List<Prop> wfProperties = new ArrayList<>();
468 for (Entry<String, String> entry : metadata.getWorkflowProperties().entrySet()) {
469 wfProperties.add(p(entry.getKey(), entry.getValue()));
470 }
471 List<Prop> agentConfig = new ArrayList<>();
472 for (Entry<String, String> entry : metadata.getCaptureAgentConfiguration().entrySet()) {
473 agentConfig.add(p(entry.getKey(), entry.getValue()));
474 }
475 return RestUtil.R.ok(obj(p("id", metadata.getEventId()), p("location", metadata.getAgentId()),
476 p("start", DateTimeSupport.toUTC(metadata.getStartDate().getTime())),
477 p("end", DateTimeSupport.toUTC(metadata.getEndDate().getTime())),
478 p("presenters", presenters), p("wfProperties", obj(wfProperties.toArray(new Prop[wfProperties.size()]))),
479 p("agentConfig", obj(agentConfig.toArray(new Prop[agentConfig.size()]))), p("state", state),
480 p("lastHeardFrom", lastHeard)));
481 } catch (NotFoundException e) {
482 logger.info("Event with id '{}' does not exist.", eventId);
483 return Response.status(Status.NOT_FOUND).build();
484 } catch (SchedulerException e) {
485 logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
486 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
487 }
488 }
489
490
491
492
493
494
495
496
497 @GET
498 @Produces(MediaType.TEXT_PLAIN)
499 @Path("{id:.+}/workflow.properties")
500 @RestQuery(name = "recordingsagentproperties",
501 description = "Retrieves workflow configuration for specified event",
502 returnDescription = "workflow configuration in the form of key, value pairs",
503 pathParameters = {
504 @RestParameter(name = "id", isRequired = true, description = "ID of event for which workflow configuration "
505 + "will be retrieved", type = Type.STRING)
506 },
507 responses = {
508 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "workflow configuration of event is "
509 + "in the body of response"),
510 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does "
511 + "not exist"),
512 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
513 + "to remove the event. Maybe you need to authenticate.")
514 }
515 )
516 public Response getWorkflowConfiguration(@PathParam("id") String eventId) throws UnauthorizedException {
517 try {
518 Map<String, String> result = service.getWorkflowConfig(eventId);
519 String serializedProperties = serializeProperties(result);
520 return Response.ok(serializedProperties).build();
521 } catch (NotFoundException e) {
522 logger.info("Event with id '{}' does not exist.", eventId);
523 return Response.status(Status.NOT_FOUND).build();
524 } catch (SchedulerException e) {
525 logger.error("Unable to retrieve workflow configuration for event with id '{}': {}", eventId, getMessage(e));
526 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
527 }
528 }
529
530
531
532
533
534
535
536
537 @GET
538 @Produces(MediaType.TEXT_PLAIN)
539 @Path("{id:.+}/agent.properties")
540 @RestQuery(name = "recordingsagentproperties",
541 description = "Retrieves Capture Agent properties for specified event",
542 returnDescription = "Capture Agent properties in the form of key, value pairs",
543 pathParameters = {
544 @RestParameter(name = "id", isRequired = true, description = "ID of event for which agent properties will "
545 + "be retrieved", type = Type.STRING)
546 },
547 responses = {
548 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Capture Agent properties of "
549 + "event is in the body of response"),
550 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with "
551 + "specified ID does not exist"),
552 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have "
553 + "permission to remove the event. Maybe you need to authenticate.")
554 }
555 )
556 public Response getCaptureAgentMetadata(@PathParam("id") String eventId) throws UnauthorizedException {
557 try {
558 Map<String, String> result = service.getCaptureAgentConfiguration(eventId);
559 String serializedProperties = serializeProperties(result);
560 return Response.ok(serializedProperties).build();
561 } catch (NotFoundException e) {
562 logger.info("Event with id '{}' does not exist.", eventId);
563 return Response.status(Status.NOT_FOUND).build();
564 } catch (SchedulerException e) {
565 logger.error("Unable to retrieve event with id '{}': {}", eventId, getMessage(e));
566 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
567 }
568 }
569
570
571
572
573
574
575
576
577
578 @DELETE
579 @Path("{id:.+}")
580 @Produces(MediaType.TEXT_PLAIN)
581 @RestQuery(name = "deleterecordings",
582 description = "Removes scheduled event with specified ID.",
583 returnDescription = "OK if event were successfully removed or NOT FOUND if event with specified ID does not "
584 + "exist",
585 pathParameters = {
586 @RestParameter(name = "id", isRequired = true, description = "Event ID", type = Type.STRING)
587 },
588 responses = {
589 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Event was successfully removed"),
590 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "Event with specified ID does "
591 + "not exist"),
592 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
593 + "to remove the event. Maybe you need to authenticate."),
594 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "Event with specified ID is "
595 + "locked by a transaction, unable to delete event.")
596 }
597 )
598 public Response deleteEvent(@PathParam("id") String eventId) throws UnauthorizedException {
599 try {
600 service.removeEvent(eventId);
601 return Response.status(Response.Status.OK).build();
602 } catch (NotFoundException e) {
603 logger.info("Event with id '{}' does not exist.", eventId);
604 return Response.status(Status.NOT_FOUND).build();
605 } catch (UnauthorizedException e) {
606 throw e;
607 } catch (Exception e) {
608 logger.error("Unable to delete event with id '{}': {}", eventId, getMessage(e));
609 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
610 }
611 }
612
613
614
615
616
617
618
619
620
621
622
623 @GET
624 @Produces("text/calendar")
625
626 @Path("calendars")
627 @RestQuery(name = "getcalendar",
628 description = "Returns iCalendar for specified set of events",
629 returnDescription = "ICalendar for events",
630 restParameters = {
631 @RestParameter(name = "agentid", description = "Filter events by capture agent", isRequired = false,
632 type = Type.STRING),
633 @RestParameter(name = "seriesid", description = "Filter events by series", isRequired = false,
634 type = Type.STRING),
635 @RestParameter(name = "cutoff", description = "A cutoff date in UNIX milliseconds to limit the number of "
636 + "events returned in the calendar.", isRequired = false, type = Type.INTEGER)
637 },
638 responses = {
639 @RestResponse(responseCode = HttpServletResponse.SC_NOT_MODIFIED, description = "Events were not modified "
640 + "since last request"),
641 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Events were modified, new calendar "
642 + "is in the body")
643 }
644 )
645 public Response getCalendar(@QueryParam("agentid") String captureAgentId, @QueryParam("seriesid") String seriesId,
646 @QueryParam("cutoff") Long cutoff, @Context HttpServletRequest request) {
647 Date endDate = null;
648 if (cutoff != null) {
649 try {
650 endDate = new Date(cutoff);
651 } catch (NumberFormatException e) {
652 return Response.status(Status.BAD_REQUEST).build();
653 }
654 }
655
656 try {
657 String lastModified = null;
658
659 if (StringUtils.isNotBlank(captureAgentId)) {
660 lastModified = service.getScheduleLastModified(captureAgentId);
661 String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
662 if (StringUtils.isNotBlank(ifNoneMatch) && ifNoneMatch.equals(lastModified)) {
663 return Response.notModified(lastModified).expires(null).build();
664 }
665 }
666
667 String result = service.getCalendar(Optional.ofNullable(StringUtils.trimToNull(captureAgentId)),
668 Optional.ofNullable(StringUtils.trimToNull(seriesId)), Optional.ofNullable(endDate));
669
670 ResponseBuilder response = Response.ok(result).header(HttpHeaders.CONTENT_TYPE, "text/calendar; charset=UTF-8");
671 if (StringUtils.isNotBlank(lastModified)) {
672 response.header(HttpHeaders.ETAG, lastModified);
673 }
674 return response.build();
675 } catch (Exception e) {
676 logger.error("Unable to get calendar for capture agent '{}':", captureAgentId, e);
677 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
678 }
679 }
680
681 @GET
682 @Produces("application/json")
683 @Path("calendar.json")
684 @RestQuery(name = "getCalendarJSON",
685 description = "Returns a calendar in JSON format for specified events.",
686 returnDescription = "Calendar for events in JSON format",
687 restParameters = {
688 @RestParameter(name = "agentid", description = "Filter events by capture agent", isRequired = false,
689 type = Type.STRING),
690 @RestParameter(name = "cutoff", description = "A cutoff date in UNIX milliseconds to limit the number of "
691 + "events returned in the calendar.", isRequired = false, type = Type.INTEGER),
692 @RestParameter(name = "timestamp", description = "Return dates as UNIX timestamp in milliseconds instead "
693 + "of a date string.", isRequired = false, type = Type.BOOLEAN)
694 }, responses = {
695 @RestResponse(responseCode = HttpServletResponse.SC_NOT_MODIFIED, description = "Events were not modified "
696 + "since last request"),
697 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Events were modified, new calendar "
698 + "is in the body")
699 }
700 )
701 public Response getCalendarJson(
702 @QueryParam("agentid") String captureAgentId,
703 @QueryParam("cutoff") Long cutoff,
704 @QueryParam("timestamp") Boolean timestamp,
705 @Context HttpServletRequest request) {
706 try {
707 var endDate = Optional.ofNullable(cutoff)
708 .map(Date::new);
709 var agent = Optional.ofNullable(captureAgentId)
710 .map(String::trim)
711 .filter(id -> !id.isEmpty());
712 timestamp = !Objects.isNull(timestamp) && timestamp;
713
714 String lastModified = null;
715
716 if (agent.isPresent()) {
717 lastModified = service.getScheduleLastModified(agent.get());
718 String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH);
719 if (StringUtils.isNotBlank(ifNoneMatch) && ifNoneMatch.equals(lastModified)) {
720 return Response.notModified(lastModified).expires(null).build();
721 }
722 }
723
724 var result = new ArrayList<Map<String, Object>>();
725 for (var event: service.search(agent, Optional.empty(), Optional.empty(), Optional.of(new Date()), endDate)) {
726 var id = event.getIdentifier().toString();
727 result.add(Map.of(
728 "data", service.getTechnicalMetadata(id),
729 "episode-dublincore", service.getDublinCore(id).toXmlString()
730 ));
731 }
732
733 final ResponseBuilder response = Response.ok((timestamp ? gsonTimestamp : gson).toJson(result));
734 if (StringUtils.isNotBlank(lastModified)) {
735 response.header(HttpHeaders.ETAG, lastModified);
736 }
737 return response.build();
738 } catch (Exception e) {
739 throw new WebApplicationException(
740 String.format("Unable to get calendar for capture agent %s", captureAgentId),
741 e, Response.Status.INTERNAL_SERVER_ERROR);
742 }
743 }
744
745
746 @GET
747 @Produces(MediaType.TEXT_PLAIN)
748 @Path("{id}/lastmodified")
749 @RestQuery(name = "agentlastmodified",
750 description = "Retrieves the last modified hash for specified agent",
751 returnDescription = "The last modified hash",
752 pathParameters = {
753 @RestParameter(name = "id", isRequired = true, description = "ID of capture agent for which the last "
754 + "modified hash will be retrieved", type = Type.STRING)
755 },
756 responses = {
757 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "The last modified hash of agent "
758 + "is in the body of response")
759 }
760 )
761 public Response getLastModified(@PathParam("id") String agentId) {
762 try {
763 String lastModified = service.getScheduleLastModified(agentId);
764 return Response.ok(lastModified).build();
765 } catch (Exception e) {
766 logger.error("Unable to retrieve agent last modified hash of agent id '{}': {}", agentId, getMessage(e));
767 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
768 }
769 }
770
771 @POST
772 @Path("/removeOldScheduledRecordings")
773 @RestQuery(name = "removeOldScheduledRecordings",
774 description = "This will find and remove any scheduled events before the buffer time to keep performance in the "
775 + "scheduler optimum.",
776 returnDescription = "No return value",
777 responses = {
778 @RestResponse(responseCode = SC_OK, description = "Removed old scheduled recordings."),
779 @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse buffer."),
780 @RestResponse(responseCode = SC_UNAUTHORIZED, description = "You do not have permission to remove old "
781 + "schedulings. Maybe you need to authenticate.")
782 },
783 restParameters = {
784 @RestParameter(name = "buffer", type = RestParameter.Type.INTEGER, defaultValue = "604800", isRequired = true,
785 description = "The amount of seconds before now that a capture has to have stopped capturing. "
786 + "It must be 0 or greater.")
787 }
788 )
789 public Response removeOldScheduledRecordings(@FormParam("buffer") long buffer) throws UnauthorizedException {
790 if (buffer < 0) {
791 return Response.status(SC_BAD_REQUEST).build();
792 }
793
794 try {
795 service.removeScheduledRecordingsBeforeBuffer(buffer);
796 } catch (SchedulerException e) {
797 logger.error("Error while trying to remove old scheduled recordings", e);
798 throw new WebApplicationException(e);
799 }
800 return Response.ok().build();
801 }
802
803
804
805
806
807 @POST
808 @Path("/")
809 @RestQuery(name = "newrecording",
810 description = "Creates new event with specified parameters",
811 returnDescription = "If an event was successfully created",
812 restParameters = {
813 @RestParameter(name = "start", isRequired = true, type = Type.INTEGER,
814 description = "The start date of the event in milliseconds from 1970-01-01T00:00:00Z"),
815 @RestParameter(name = "end", isRequired = true, type = Type.INTEGER,
816 description = "The end date of the event in milliseconds from 1970-01-01T00:00:00Z"),
817 @RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent of the event"),
818 @RestParameter(name = "users", isRequired = false, type = Type.STRING,
819 description = "Comma separated list of user ids (speakers/lecturers) for the event"),
820 @RestParameter(name = "mediaPackage", isRequired = true, type = Type.TEXT,
821 description = "The media package of the event"),
822 @RestParameter(name = "wfproperties", isRequired = false, type = Type.TEXT, description = "Workflow "
823 + "configuration keys for the event. Each key will be prefixed by 'org.opencastproject.workflow"
824 + ".config.' and added to the capture agent parameters."),
825 @RestParameter(name = "agentparameters", isRequired = false, type = Type.TEXT,
826 description = "The capture agent properties for the event"),
827 @RestParameter(name = "source", isRequired = false, type = Type.STRING,
828 description = "The scheduling source of the event"),
829 },
830 responses = {
831 @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event is successfully created"),
832 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT,
833 description = "Unable to create event, conflicting events found (ConflicsFound)"),
834 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT,
835 description = "Unable to create event, event locked by a transaction (TransactionLock)"),
836 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED,
837 description = "You do not have permission to create the event. Maybe you need to authenticate."),
838 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST,
839 description = "Missing or invalid information for this request")
840 }
841 )
842 public Response addEvent(@FormParam("start") long startTime, @FormParam("end") long endTime,
843 @FormParam("agent") String agentId, @FormParam("users") String users,
844 @FormParam("mediaPackage") String mediaPackageXml, @FormParam("wfproperties") String workflowProperties,
845 @FormParam("agentparameters") String agentParameters,
846 @FormParam("source") String schedulingSource)
847 throws UnauthorizedException {
848 if (endTime <= startTime || startTime < 0) {
849 logger.debug("Cannot add event without proper start and end time");
850 return RestUtil.R.badRequest("Cannot add event without proper start and end time");
851 }
852
853 if (StringUtils.isBlank(agentId)) {
854 logger.debug("Cannot add event without agent identifier");
855 return RestUtil.R.badRequest("Cannot add event without agent identifier");
856 }
857
858 if (StringUtils.isBlank(mediaPackageXml)) {
859 logger.debug("Cannot add event without media package");
860 return RestUtil.R.badRequest("Cannot add event without media package");
861 }
862
863 MediaPackage mediaPackage;
864 try {
865 mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
866 } catch (MediaPackageException e) {
867 logger.debug("Could not parse media package", e);
868 return RestUtil.R.badRequest("Could not parse media package");
869 }
870
871 String eventId = mediaPackage.getIdentifier().toString();
872
873 Map<String, String> caProperties = new HashMap<>();
874 if (StringUtils.isNotBlank(agentParameters)) {
875 try {
876 Properties prop = parseProperties(agentParameters);
877 caProperties.putAll((Map) prop);
878 } catch (Exception e) {
879 logger.info("Could not parse capture agent properties: {}", agentParameters);
880 return RestUtil.R.badRequest("Could not parse capture agent properties");
881 }
882 }
883
884 Map<String, String> wfProperties = new HashMap<>();
885 if (StringUtils.isNotBlank(workflowProperties)) {
886 try {
887 Properties prop = parseProperties(workflowProperties);
888 wfProperties.putAll((Map) prop);
889 } catch (IOException e) {
890 logger.info("Could not parse workflow configuration properties: {}", workflowProperties);
891 return RestUtil.R.badRequest("Could not parse workflow configuration properties");
892 }
893 }
894 Set<String> userIds = new HashSet<>();
895 String[] ids = StringUtils.split(users, ",");
896 if (ids != null) {
897 userIds.addAll(Arrays.asList(ids));
898 }
899
900 DateTime startDate = new DateTime(startTime).toDateTime(DateTimeZone.UTC);
901 DateTime endDate = new DateTime(endTime).toDateTime(DateTimeZone.UTC);
902
903 try {
904 service.addEvent(startDate.toDate(), endDate.toDate(), agentId, userIds, mediaPackage, wfProperties, caProperties,
905 Optional.ofNullable(schedulingSource));
906 return Response.status(Status.CREATED)
907 .header("Location", serverUrl + serviceUrl + '/' + eventId + "/mediapackage.xml").build();
908 } catch (UnauthorizedException e) {
909 throw e;
910 } catch (SchedulerConflictException e) {
911 return Response.status(Status.CONFLICT).entity(generateErrorResponse(e)).type(MediaType.APPLICATION_JSON).build();
912 } catch (Exception e) {
913 logger.error("Unable to create new event with id '{}'", eventId, e);
914 return Response.serverError().build();
915 }
916 }
917
918
919
920
921 @POST
922 @Path("/multiple")
923 @RestQuery(name = "newrecordings",
924 description = "Creates new event with specified parameters",
925 returnDescription = "If an event was successfully created",
926 restParameters = {
927 @RestParameter(name = "rrule", isRequired = true, type = Type.STRING,
928 description = "The recurrence rule for the events"),
929 @RestParameter(name = "start", isRequired = true, type = Type.INTEGER,
930 description = "The start date of the event in milliseconds from 1970-01-01T00:00:00Z"),
931 @RestParameter(name = "end", isRequired = true, type = Type.INTEGER,
932 description = "The end date of the event in milliseconds from 1970-01-01T00:00:00Z"),
933 @RestParameter(name = "duration", isRequired = true, type = Type.INTEGER,
934 description = "The duration of the events in milliseconds"),
935 @RestParameter(name = "tz", isRequired = true, type = Type.INTEGER,
936 description = "The timezone of the events"),
937 @RestParameter(name = "agent", isRequired = true, type = Type.STRING,
938 description = "The agent of the event"),
939 @RestParameter(name = "users", isRequired = false, type = Type.STRING,
940 description = "Comma separated list of user ids (speakers/lecturers) for the event"),
941 @RestParameter(name = "templateMp", isRequired = true, type = Type.TEXT,
942 description = "The template mediapackage for the events"),
943 @RestParameter(name = "wfproperties", isRequired = false, type = Type.TEXT, description = "Workflow "
944 + "configuration keys for the event. Each key will be prefixed by 'org.opencastproject.workflow"
945 + ".config.' and added to the capture agent parameters."),
946 @RestParameter(name = "agentparameters", isRequired = false, type = Type.TEXT,
947 description = "The capture agent properties for the event"),
948 @RestParameter(name = "source", isRequired = false, type = Type.STRING,
949 description = "The scheduling source of the event"),
950 },
951 responses = {
952 @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Event is successfully created"),
953 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT,
954 description = "Unable to create event, conflicting events found (ConflicsFound)"),
955 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT,
956 description = "Unable to create event, event locked by a transaction (TransactionLock)"),
957 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED,
958 description = "You do not have permission to create the event. Maybe you need to authenticate."),
959 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST,
960 description = "Missing or invalid information for this request")
961 }
962 )
963 public Response addMultipleEvents(@FormParam("rrule") String rruleString, @FormParam("start") long startTime,
964 @FormParam("end") long endTime, @FormParam("duration") long duration, @FormParam("tz") String tzString,
965 @FormParam("agent") String agentId, @FormParam("users") String users,
966 @FormParam("templateMp") MediaPackage templateMp, @FormParam("wfproperties") String workflowProperties,
967 @FormParam("agentparameters") String agentParameters,
968 @FormParam("source") String schedulingSource)
969 throws UnauthorizedException {
970 if (endTime <= startTime || startTime < 0) {
971 logger.debug("Cannot add event without proper start and end time");
972 return RestUtil.R.badRequest("Cannot add event without proper start and end time");
973 }
974
975 RRule rrule;
976 try {
977 rrule = new RRule(rruleString);
978 } catch (ParseException e) {
979 logger.debug("Could not parse recurrence rule");
980 return RestUtil.R.badRequest("Could not parse recurrence rule");
981 }
982
983 if (duration < 1) {
984 logger.debug("Cannot schedule events with durations less than 1");
985 return RestUtil.R.badRequest("Cannot schedule events with durations less than 1");
986 }
987
988 if (StringUtils.isBlank(tzString)) {
989 logger.debug("Cannot schedule events with blank timezone");
990 return RestUtil.R.badRequest("Cannot schedule events with blank timezone");
991 }
992 TimeZone tz = TimeZone.getTimeZone(tzString);
993
994 if (StringUtils.isBlank(agentId)) {
995 logger.debug("Cannot add event without agent identifier");
996 return RestUtil.R.badRequest("Cannot add event without agent identifier");
997 }
998
999 Map<String, String> caProperties = new HashMap<>();
1000 if (StringUtils.isNotBlank(agentParameters)) {
1001 try {
1002 Properties prop = parseProperties(agentParameters);
1003 caProperties.putAll((Map) prop);
1004 } catch (Exception e) {
1005 logger.info("Could not parse capture agent properties: {}", agentParameters);
1006 return RestUtil.R.badRequest("Could not parse capture agent properties");
1007 }
1008 }
1009
1010 Map<String, String> wfProperties = new HashMap<>();
1011 if (StringUtils.isNotBlank(workflowProperties)) {
1012 try {
1013 Properties prop = parseProperties(workflowProperties);
1014 wfProperties.putAll((Map) prop);
1015 } catch (IOException e) {
1016 logger.info("Could not parse workflow configuration properties: {}", workflowProperties);
1017 return RestUtil.R.badRequest("Could not parse workflow configuration properties");
1018 }
1019 }
1020 Set<String> userIds = new HashSet<>();
1021 String[] ids = StringUtils.split(users, ",");
1022 if (ids != null) {
1023 userIds.addAll(Arrays.asList(ids));
1024 }
1025
1026
1027 DateTime startDate = new DateTime(startTime).toDateTime(DateTimeZone.forTimeZone(tz));
1028 DateTime endDate = new DateTime(endTime).toDateTime(DateTimeZone.forTimeZone(tz));
1029
1030 try {
1031 service.addMultipleEvents(rrule, startDate.toDate(), endDate.toDate(), duration, tz, agentId, userIds, templateMp,
1032 wfProperties, caProperties, Optional.ofNullable(schedulingSource));
1033 return Response.status(Status.CREATED).build();
1034 } catch (UnauthorizedException e) {
1035 throw e;
1036 } catch (SchedulerConflictException e) {
1037 return Response.status(Status.CONFLICT).entity(generateErrorResponse(e)).type(MediaType.APPLICATION_JSON).build();
1038 } catch (Exception e) {
1039 logger.error("Unable to create new events", e);
1040 return Response.serverError().build();
1041 }
1042 }
1043
1044 @PUT
1045 @Path("{id}")
1046 @RestQuery(name = "updaterecordings",
1047 description = "Updates specified event",
1048 returnDescription = "Status OK is returned if event was successfully updated, NOT FOUND if specified event does "
1049 + "not exist or BAD REQUEST if data is missing or invalid",
1050 pathParameters = {
1051 @RestParameter(name = "id", description = "ID of event to be updated", isRequired = true, type = Type.STRING)
1052 },
1053 restParameters = {
1054 @RestParameter(name = "start", isRequired = false, type = Type.INTEGER,
1055 description = "Updated start date for event"),
1056 @RestParameter(name = "end", isRequired = false, type = Type.INTEGER,
1057 description = "Updated end date for event"),
1058 @RestParameter(name = "agent", isRequired = false, type = Type.STRING,
1059 description = "Updated agent for event"),
1060 @RestParameter(name = "users", isRequired = false, type = Type.STRING,
1061 description = "Updated comma separated list of user ids (speakers/lecturers) for the event"),
1062 @RestParameter(name = "mediaPackage", isRequired = false, type = Type.TEXT,
1063 description = "Updated media package for event"),
1064 @RestParameter(name = "wfproperties", isRequired = false, type = Type.TEXT,
1065 description = "Workflow configuration properties"),
1066 @RestParameter(name = "agentparameters", isRequired = false, type = Type.TEXT,
1067 description = "Updated Capture Agent properties")
1068 },
1069 responses = {
1070 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1071 description = "Event was successfully updated"),
1072 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND,
1073 description = "Event with specified ID does not exist"),
1074 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT,
1075 description = "Unable to update event, conflicting events found (ConflicsFound)"),
1076 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT,
1077 description = "Unable to update event, event locked by a transaction (TransactionLock)"),
1078 @RestResponse(responseCode = HttpServletResponse.SC_FORBIDDEN,
1079 description = "Event with specified ID cannot be updated"),
1080 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED,
1081 description = "You do not have permission to update the event. Maybe you need to authenticate."),
1082 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST,
1083 description = "Data is missing or invalid")
1084 }
1085 )
1086 public Response updateEvent(@PathParam("id") String eventID, @FormParam("start") Long startTime,
1087 @FormParam("end") Long endTime, @FormParam("agent") String agentId, @FormParam("users") String users,
1088 @FormParam("mediaPackage") String mediaPackageXml, @FormParam("wfproperties") String workflowProperties,
1089 @FormParam("agentparameters") String agentParameters) throws UnauthorizedException {
1090 if (startTime != null) {
1091 if (startTime < 0) {
1092 logger.debug("Cannot add event with negative start time ({} < 0)", startTime);
1093 return RestUtil.R.badRequest("Cannot add event with negative start time");
1094 }
1095 if (endTime != null && endTime <= startTime) {
1096 logger.debug("Cannot add event without proper end time ({} <= {})", startTime, endTime);
1097 return RestUtil.R.badRequest("Cannot add event without proper end time");
1098 }
1099 }
1100
1101 MediaPackage mediaPackage = null;
1102 if (StringUtils.isNotBlank(mediaPackageXml)) {
1103 try {
1104 mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
1105 } catch (Exception e) {
1106 logger.debug("Could not parse media packagey", e);
1107 return Response.status(Status.BAD_REQUEST).build();
1108 }
1109 }
1110
1111 Map<String, String> caProperties = null;
1112 if (StringUtils.isNotBlank(agentParameters)) {
1113 try {
1114 Properties prop = parseProperties(agentParameters);
1115 caProperties = new HashMap<>();
1116 caProperties.putAll((Map) prop);
1117 } catch (Exception e) {
1118 logger.debug("Could not parse capture agent properties: {}", agentParameters, e);
1119 return Response.status(Status.BAD_REQUEST).build();
1120 }
1121 }
1122
1123 Map<String, String> wfProperties = null;
1124 if (StringUtils.isNotBlank(workflowProperties)) {
1125 try {
1126 Properties prop = parseProperties(workflowProperties);
1127 wfProperties = new HashMap<>();
1128 wfProperties.putAll((Map) prop);
1129 } catch (IOException e) {
1130 logger.debug("Could not parse workflow configuration properties: {}", workflowProperties, e);
1131 return Response.status(Status.BAD_REQUEST).build();
1132 }
1133 }
1134
1135 Set<String> userIds = null;
1136 String[] ids = StringUtils.split(StringUtils.trimToNull(users), ",");
1137 if (ids != null) {
1138 userIds = new HashSet<>(Arrays.asList(ids));
1139 }
1140
1141 Date startDate = null;
1142 if (startTime != null) {
1143 startDate = new DateTime(startTime).toDateTime(DateTimeZone.UTC).toDate();
1144 }
1145
1146 Date endDate = null;
1147 if (endTime != null) {
1148 endDate = new DateTime(endTime).toDateTime(DateTimeZone.UTC).toDate();
1149 }
1150
1151 try {
1152 service.updateEvent(eventID, Optional.ofNullable(startDate), Optional.ofNullable(endDate),
1153 Optional.ofNullable(StringUtils.trimToNull(agentId)), Optional.ofNullable(userIds),
1154 Optional.ofNullable(mediaPackage), Optional.ofNullable(wfProperties), Optional.ofNullable(caProperties));
1155 return Response.ok().build();
1156 } catch (SchedulerConflictException e) {
1157 return Response.status(Status.CONFLICT).entity(generateErrorResponse(e)).type(MediaType.APPLICATION_JSON).build();
1158 } catch (SchedulerException e) {
1159 logger.warn("Error updating event with id '{}'", eventID, e);
1160 return Response.status(Status.FORBIDDEN).build();
1161 } catch (NotFoundException e) {
1162 logger.info("Event with id '{}' does not exist.", eventID);
1163 return Response.status(Status.NOT_FOUND).build();
1164 } catch (UnauthorizedException e) {
1165 throw e;
1166 } catch (Exception e) {
1167 logger.error("Unable to update event with id '{}'", eventID, e);
1168 return Response.serverError().build();
1169 }
1170 }
1171
1172 @GET
1173 @Path("currentRecording/{agent}")
1174 @Produces(MediaType.TEXT_XML)
1175 @RestQuery(name = "currentrecording",
1176 description = "Get the current capture event as XML",
1177 returnDescription = "The current capture event as XML",
1178 pathParameters = {
1179 @RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier")
1180 },
1181 responses = {
1182 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1183 description = "current event is in the body of response"),
1184 @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "There is no current recording")
1185 }
1186 )
1187 public Response currentRecording(@PathParam("agent") String agentId) throws UnauthorizedException {
1188 try {
1189 Optional<MediaPackage> current = service.getCurrentRecording(agentId);
1190 if (current.isEmpty()) {
1191 return Response.noContent().build();
1192 } else {
1193 return Response.ok(MediaPackageParser.getAsXml(current.get())).build();
1194 }
1195 } catch (UnauthorizedException e) {
1196 throw e;
1197 } catch (Exception e) {
1198 logger.error("Unable to get the current recording for agent '{}'", agentId, e);
1199 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1200 }
1201 }
1202
1203 @GET
1204 @Path("upcomingRecording/{agent}")
1205 @Produces(MediaType.TEXT_XML)
1206 @RestQuery(name = "upcomingrecording",
1207 description = "Get the upcoming capture event as XML",
1208 returnDescription = "The upcoming capture event as XML",
1209 pathParameters = {
1210 @RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier")
1211 },
1212 responses = {
1213 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1214 description = "upcoming event is in the body of response"),
1215 @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT,
1216 description = "There is no upcoming recording")
1217 }
1218 )
1219 public Response upcomingRecording(@PathParam("agent") String agentId) throws UnauthorizedException {
1220 try {
1221 Optional<MediaPackage> upcoming = service.getUpcomingRecording(agentId);
1222 if (upcoming.isEmpty()) {
1223 return Response.noContent().build();
1224 } else {
1225 return Response.ok(MediaPackageParser.getAsXml(upcoming.get())).build();
1226 }
1227 } catch (UnauthorizedException e) {
1228 throw e;
1229 } catch (Exception e) {
1230 logger.error("Unable to get the upcoming recording for agent '{}'", agentId, e);
1231 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1232 }
1233 }
1234
1235 @GET
1236 @Path("eventCount")
1237 @Produces(MediaType.TEXT_PLAIN)
1238 @RestQuery(name = "eventcount",
1239 description = "Get the number of scheduled events",
1240 returnDescription = "The number of scheduled events",
1241 responses = {
1242 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "The event count")
1243 }
1244 )
1245 public Response eventCount() throws UnauthorizedException {
1246 try {
1247 return Response.ok("" + service.getEventCount()).build();
1248 } catch (UnauthorizedException e) {
1249 throw e;
1250 } catch (Exception e) {
1251 logger.error("Unable to get the event count", e);
1252 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1253 }
1254 }
1255
1256 @GET
1257 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
1258 @Path("recordings.{type:xml|json}")
1259 @RestQuery(name = "recordingsaslist",
1260 description = "Searches recordings and returns result as XML or JSON",
1261 returnDescription = "XML or JSON formated results",
1262 pathParameters = {
1263 @RestParameter(name = "type", isRequired = true, description = "The media type of the response [xml|json]",
1264 type = Type.STRING)
1265 },
1266 restParameters = {
1267 @RestParameter(name = "agent", description = "Search by device", isRequired = false, type = Type.STRING),
1268 @RestParameter(name = "startsfrom", description = "Search by when does event start", isRequired = false,
1269 type = Type.INTEGER),
1270 @RestParameter(name = "startsto", description = "Search by when does event start", isRequired = false,
1271 type = Type.INTEGER),
1272 @RestParameter(name = "endsfrom", description = "Search by when does event finish", isRequired = false,
1273 type = Type.INTEGER),
1274 @RestParameter(name = "endsto", description = "Search by when does event finish", isRequired = false,
1275 type = Type.INTEGER)
1276 },
1277 responses = {
1278 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1279 description = "Search completed, results returned in body")
1280 }
1281 )
1282 public Response getEventsAsList(@PathParam("type") final String type, @QueryParam("agent") String device,
1283 @QueryParam("startsfrom") Long startsFromTime,
1284 @QueryParam("startsto") Long startsToTime, @QueryParam("endsfrom") Long endsFromTime,
1285 @QueryParam("endsto") Long endsToTime) throws UnauthorizedException {
1286 Date startsfrom = null;
1287 Date startsTo = null;
1288 Date endsFrom = null;
1289 Date endsTo = null;
1290 if (startsFromTime != null) {
1291 startsfrom = new DateTime(startsFromTime).toDateTime(DateTimeZone.UTC).toDate();
1292 }
1293 if (startsToTime != null) {
1294 startsTo = new DateTime(startsToTime).toDateTime(DateTimeZone.UTC).toDate();
1295 }
1296 if (endsFromTime != null) {
1297 endsFrom = new DateTime(endsFromTime).toDateTime(DateTimeZone.UTC).toDate();
1298 }
1299 if (endsToTime != null) {
1300 endsTo = new DateTime(endsToTime).toDateTime(DateTimeZone.UTC).toDate();
1301 }
1302
1303 try {
1304 List<MediaPackage> events = service.search(Optional.ofNullable(StringUtils.trimToNull(device)),
1305 Optional.ofNullable(startsfrom), Optional.ofNullable(startsTo), Optional.ofNullable(endsFrom),
1306 Optional.ofNullable(endsTo));
1307 if ("json".equalsIgnoreCase(type)) {
1308 return Response.ok(getEventListAsJsonString(events)).build();
1309 } else {
1310 return Response.ok(MediaPackageParser.getArrayAsXml(events)).build();
1311 }
1312 } catch (UnauthorizedException e) {
1313 throw e;
1314 } catch (Exception e) {
1315 logger.error("Unable to perform search: {}", getMessage(e));
1316 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1317 }
1318 }
1319 @GET
1320 @Produces(MediaType.APPLICATION_JSON)
1321 @Path("conflicts.json")
1322 @RestQuery(name = "conflictingrecordingsasjson",
1323 description = "Searches for conflicting recordings based on parameters",
1324 returnDescription = "Returns NO CONTENT if no recordings are in conflict within specified period or list of "
1325 + "conflicting recordings in JSON",
1326 restParameters = {
1327 @RestParameter(name = "agent", description = "Device identifier for which conflicts will be searched",
1328 isRequired = true, type = Type.STRING),
1329 @RestParameter(name = "start", description = "Start time of conflicting period, in milliseconds",
1330 isRequired = true, type = Type.INTEGER),
1331 @RestParameter(name = "end", description = "End time of conflicting period, in milliseconds",
1332 isRequired = true, type = Type.INTEGER),
1333 @RestParameter(name = "rrule", description = "Rule for recurrent conflicting, "
1334 + "specified as: \"FREQ=WEEKLY;BYDAY=day(s);BYHOUR=hour;BYMINUTE=minute\". FREQ is required. "
1335 + "BYDAY may include one or more (separated by commas) of the following: SU,MO,TU,WE,TH,FR,SA.",
1336 isRequired = false, type = Type.STRING),
1337 @RestParameter(name = "duration", description = "If recurrence rule is specified duration of each "
1338 + "conflicting period, in milliseconds", isRequired = false, type = Type.INTEGER),
1339 @RestParameter(name = "timezone", description = "The timezone of the capture device",
1340 isRequired = false, type = Type.STRING)
1341 },
1342 responses = {
1343 @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
1344 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1345 description = "Found conflicting events, returned in body of response"),
1346 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST,
1347 description = "Missing or invalid parameters"),
1348 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED,
1349 description = "Not authorized to make this request"),
1350 @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
1351 description = "A detailed stack track of the internal issue.")
1352 }
1353 )
1354 public Response getConflictingEventsJson(@QueryParam("agent") String device, @QueryParam("rrule") String rrule,
1355 @QueryParam("start") Long startDate, @QueryParam("end") Long endDate, @QueryParam("duration") Long duration,
1356 @QueryParam("timezone") String timezone) throws UnauthorizedException {
1357 try {
1358 List<MediaPackage> events = getConflictingEvents(device, rrule, startDate, endDate, duration, timezone);
1359 if (!events.isEmpty()) {
1360 String eventsJsonString = getEventListAsJsonString(events);
1361 return Response.ok(eventsJsonString).build();
1362 } else {
1363 return Response.noContent().build();
1364 }
1365 } catch (IllegalArgumentException e) {
1366 return Response.status(Status.BAD_REQUEST).build();
1367 } catch (UnauthorizedException e) {
1368 throw e;
1369 } catch (Exception e) {
1370 logger.error("Unable to find conflicting events for {}, {}, {}, {}, {}:",
1371 device, rrule, startDate, endDate, duration, e);
1372 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1373 }
1374 }
1375
1376 @GET
1377 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
1378 @Path("conflicts.{type:xml|json}")
1379 @RestQuery(name = "conflictingrecordings",
1380 description = "Searches for conflicting recordings based on parameters and returns result as XML or JSON",
1381 returnDescription = "Returns NO CONTENT if no recordings are in conflict within specified period or list of "
1382 + "conflicting recordings in XML or JSON",
1383 pathParameters = {
1384 @RestParameter(name = "type", isRequired = true, description = "The media type of the response [xml|json]",
1385 type = Type.STRING)
1386 },
1387 restParameters = {
1388 @RestParameter(name = "agent", description = "Device identifier for which conflicts will be searched",
1389 isRequired = true, type = Type.STRING),
1390 @RestParameter(name = "start", description = "Start time of conflicting period, in milliseconds",
1391 isRequired = true, type = Type.INTEGER),
1392 @RestParameter(name = "end", description = "End time of conflicting period, in milliseconds",
1393 isRequired = true, type = Type.INTEGER),
1394 @RestParameter(name = "rrule", description = "Rule for recurrent conflicting, "
1395 + "specified as: \"FREQ=WEEKLY;BYDAY=day(s);BYHOUR=hour;BYMINUTE=minute\". FREQ is required. "
1396 + "BYDAY may include one or more (separated by commas) of the following: SU,MO,TU,WE,TH,FR,SA.",
1397 isRequired = false, type = Type.STRING),
1398 @RestParameter(name = "duration", description = "If recurrence rule is specified duration of each "
1399 + "conflicting period, in milliseconds", isRequired = false, type = Type.INTEGER),
1400 @RestParameter(name = "timezone", description = "The timezone of the capture device",
1401 isRequired = false, type = Type.STRING)
1402 },
1403 responses = {
1404 @RestResponse(responseCode = HttpServletResponse.SC_NO_CONTENT, description = "No conflicting events found"),
1405 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1406 description = "Found conflicting events, returned in body of response"),
1407 @RestResponse(responseCode = HttpServletResponse.SC_BAD_REQUEST,
1408 description = "Missing or invalid parameters"),
1409 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED,
1410 description = "Not authorized to make this request"),
1411 @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
1412 description = "A detailed stack track of the internal issue.")
1413 }
1414 )
1415 public Response getConflicts(@PathParam("type") final String type, @QueryParam("agent") String device,
1416 @QueryParam("rrule") String rrule, @QueryParam("start") Long startDate, @QueryParam("end") Long endDate,
1417 @QueryParam("duration") Long duration, @QueryParam("timezone") String timezone) throws UnauthorizedException {
1418
1419
1420 if (StringUtils.isBlank(timezone)) {
1421 timezone = DateTimeZone.getDefault().toString();
1422 }
1423
1424 try {
1425 List<MediaPackage> events = getConflictingEvents(device, rrule, startDate, endDate, duration, timezone);
1426 if (!events.isEmpty()) {
1427 if ("json".equalsIgnoreCase(type)) {
1428 return Response.ok(getEventListAsJsonString(events)).build();
1429 } else {
1430 return Response.ok(MediaPackageParser.getArrayAsXml(events)).build();
1431 }
1432 } else {
1433 return Response.noContent().build();
1434 }
1435 } catch (IllegalArgumentException e) {
1436 return Response.status(Status.BAD_REQUEST).build();
1437 } catch (UnauthorizedException e) {
1438 throw e;
1439 } catch (Exception e) {
1440 logger.error("Unable to find conflicting events for {}, {}, {}, {}, {}",
1441 device, rrule, startDate, endDate, duration, e);
1442 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1443 }
1444 }
1445
1446 @PUT
1447 @Path("{id}/recordingStatus")
1448 @RestQuery(name = "updateRecordingState",
1449 description = "Set the status of a given recording, registering it if it is new",
1450 pathParameters = {
1451 @RestParameter(description = "The ID of a given recording", isRequired = true, name = "id",
1452 type = Type.STRING)
1453 },
1454 restParameters = {
1455 @RestParameter(description = "The state of the recording. Must be one of the following: unknown, capturing, "
1456 + "capture_finished, capture_error, manifest, manifest_error, manifest_finished, compressing, "
1457 + "compressing_error, uploading, upload_finished, upload_error.",
1458 isRequired = true, name = "state", type = Type.STRING)
1459 },
1460 responses = {
1461 @RestResponse(description = "{id} set to {state}", responseCode = HttpServletResponse.SC_OK),
1462 @RestResponse(description = "{id} or state {state} is empty or the {state} is not known",
1463 responseCode = HttpServletResponse.SC_BAD_REQUEST),
1464 @RestResponse(description = "Recording with {id} could not be found",
1465 responseCode = HttpServletResponse.SC_NOT_FOUND)
1466 },
1467 returnDescription = ""
1468 )
1469 public Response updateRecordingState(@PathParam("id") String id, @FormParam("state") String state)
1470 throws NotFoundException {
1471 if (StringUtils.isEmpty(id) || StringUtils.isEmpty(state)) {
1472 return Response.serverError().status(Response.Status.BAD_REQUEST).build();
1473 }
1474
1475 try {
1476 if (service.updateRecordingState(id, state)) {
1477 return Response.ok(id + " set to " + state).build();
1478 } else {
1479 return Response.status(Response.Status.BAD_REQUEST).build();
1480 }
1481 } catch (SchedulerException e) {
1482 logger.debug("Unable to set recording state of {}:", id, e);
1483 return Response.serverError().build();
1484 }
1485 }
1486
1487 @GET
1488 @Produces(MediaType.APPLICATION_JSON)
1489 @Path("{id}/recordingStatus")
1490 @RestQuery(name = "getRecordingState",
1491 description = "Return the state of a given recording",
1492 pathParameters = {
1493 @RestParameter(description = "The ID of a given recording", isRequired = true, name = "id",
1494 type = Type.STRING)
1495 },
1496 restParameters = {},
1497 responses = {
1498 @RestResponse(description = "Returns the state of the recording with the correct id",
1499 responseCode = HttpServletResponse.SC_OK),
1500 @RestResponse(description = "The recording with the specified ID does not exist",
1501 responseCode = HttpServletResponse.SC_NOT_FOUND)
1502 },
1503 returnDescription = ""
1504 )
1505 public Response getRecordingState(@PathParam("id") String id) throws NotFoundException {
1506 try {
1507 Recording rec = service.getRecordingState(id);
1508 return RestUtil.R
1509 .ok(obj(p("id", rec.getID()), p("state", rec.getState()), p("lastHeardFrom", rec.getLastCheckinTime())));
1510 } catch (SchedulerException e) {
1511 logger.debug("Unable to get recording state of {}:", id, e);
1512 return Response.serverError().build();
1513 }
1514 }
1515
1516 @DELETE
1517 @Path("{id}/recordingStatus")
1518 @RestQuery(name = "removeRecording",
1519 description = "Remove record of a given recording",
1520 pathParameters = {
1521 @RestParameter(description = "The ID of a given recording", isRequired = true, name = "id",
1522 type = Type.STRING)
1523 },
1524 restParameters = {},
1525 responses = {
1526 @RestResponse(description = "{id} removed", responseCode = HttpServletResponse.SC_OK),
1527 @RestResponse(description = "{id} is empty", responseCode = HttpServletResponse.SC_BAD_REQUEST),
1528 @RestResponse(description = "Recording with {id} could not be found",
1529 responseCode = HttpServletResponse.SC_NOT_FOUND)
1530 },
1531 returnDescription = ""
1532 )
1533 public Response removeRecording(@PathParam("id") String id) throws NotFoundException {
1534 if (StringUtils.isEmpty(id)) {
1535 return Response.serverError().status(Response.Status.BAD_REQUEST).build();
1536 }
1537
1538 try {
1539 service.removeRecording(id);
1540 return Response.ok(id + " removed").build();
1541 } catch (SchedulerException e) {
1542 logger.debug("Unable to remove recording with id '{}':", id, e);
1543 return Response.serverError().build();
1544 }
1545 }
1546
1547 @GET
1548 @Produces(MediaType.APPLICATION_JSON)
1549 @Path("recordingStatus")
1550 @RestQuery(name = "getAllRecordings",
1551 description = "Return all registered recordings and their state",
1552 pathParameters = {},
1553 restParameters = {},
1554 responses = {
1555 @RestResponse(description = "Returns all known recordings.", responseCode = HttpServletResponse.SC_OK)
1556 },
1557 returnDescription = ""
1558 )
1559 public Response getAllRecordings() {
1560 try {
1561 List<Val> update = new ArrayList<>();
1562 for (Entry<String, Recording> e : service.getKnownRecordings().entrySet()) {
1563 update.add(obj(p("id", e.getValue().getID()), p("state", e.getValue().getState()),
1564 p("lastHeardFrom", e.getValue().getLastCheckinTime())));
1565 }
1566 return RestUtil.R.ok(arr(update).toJson());
1567 } catch (SchedulerException e) {
1568 logger.debug("Unable to get all recordings:", e);
1569 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1570 }
1571 }
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584 @GET
1585 @Path("capture/{agent}")
1586 @Produces(MediaType.APPLICATION_JSON)
1587 @RestQuery(name = "currentcapture",
1588 description = "Get the current capture event catalog as JSON",
1589 returnDescription = "The current capture event catalog as JSON",
1590 pathParameters = {
1591 @RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier")
1592 },
1593 responses = {
1594 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1595 description = "DublinCore of current capture event is in the body of response"),
1596 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no ongoing recording"),
1597 @RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE,
1598 description = "The agent is not ready to communicate")
1599 }
1600 )
1601 public Response currentCapture(@PathParam("agent") String agentId) throws NotFoundException {
1602 if (service == null || agentService == null) {
1603 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
1604 .entity("Scheduler service is unavailable, please wait...").build();
1605 }
1606
1607 try {
1608 Optional<MediaPackage> current = service.getCurrentRecording(agentId);
1609 if (current.isEmpty()) {
1610 logger.info("No recording to stop found for agent '{}'!", agentId);
1611 throw new NotFoundException("No recording to stop found for agent: " + agentId);
1612 } else {
1613 DublinCoreCatalog catalog = DublinCoreUtil.loadEpisodeDublinCore(workspace, current.get()).get();
1614 return Response.ok(catalog.toJson()).build();
1615 }
1616 } catch (NotFoundException e) {
1617 throw e;
1618 } catch (Exception e) {
1619 logger.error("Unable to get the immediate recording for agent '{}'", agentId, e);
1620 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1621 }
1622 }
1623
1624 @GET
1625 @Path("capture/{agent}/upcoming")
1626 @Produces(MediaType.APPLICATION_JSON)
1627 @RestQuery(name = "upcomingcapture",
1628 description = "Get the upcoming capture event catalog as JSON",
1629 returnDescription = "The upcoming capture event catalog as JSON",
1630 pathParameters = {
1631 @RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier")
1632 },
1633 responses = {
1634 @RestResponse(responseCode = HttpServletResponse.SC_OK,
1635 description = "DublinCore of the upcomfing capture event is in the body of response"),
1636 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND,
1637 description = "There is no upcoming recording"),
1638 @RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE,
1639 description = "The agent is not ready to communicate")
1640 }
1641 )
1642 public Response upcomingCapture(@PathParam("agent") String agentId) throws NotFoundException {
1643 if (service == null || agentService == null) {
1644 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
1645 .entity("Scheduler service is unavailable, please wait...").build();
1646 }
1647
1648 try {
1649 Optional<MediaPackage> upcoming = service.getUpcomingRecording(agentId);
1650 if (upcoming.isEmpty()) {
1651 logger.info("No recording to stop found for agent '{}'!", agentId);
1652 throw new NotFoundException("No recording to stop found for agent: " + agentId);
1653 } else {
1654 DublinCoreCatalog catalog = DublinCoreUtil.loadEpisodeDublinCore(workspace, upcoming.get()).get();
1655 return Response.ok(catalog.toJson()).build();
1656 }
1657 } catch (NotFoundException e) {
1658 throw e;
1659 } catch (Exception e) {
1660 logger.error("Unable to get the immediate recording for agent '{}'", agentId, e);
1661 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1662 }
1663 }
1664
1665 @POST
1666 @Path("capture/{agent}")
1667 @RestQuery(name = "startcapture",
1668 description = "Create an immediate event",
1669 returnDescription = "If events were successfully generated, status CREATED is returned",
1670 pathParameters = {
1671 @RestParameter(name = "agent", isRequired = true, type = Type.STRING, description = "The agent identifier")
1672 },
1673 restParameters = {
1674 @RestParameter(name = "workflowDefinitionId", isRequired = false, type = Type.STRING,
1675 description = "The workflow definition id to use")
1676 },
1677 responses = {
1678 @RestResponse(responseCode = HttpServletResponse.SC_CREATED, description = "Recording started"),
1679 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no such agent"),
1680 @RestResponse(responseCode = HttpServletResponse.SC_CONFLICT, description = "The agent is already recording"),
1681 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
1682 + "to start this immediate capture. Maybe you need to authenticate."),
1683 @RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE,
1684 description = "The agent is not ready to communicate")
1685 }
1686 )
1687 public Response startCapture(@PathParam("agent") String agentId, @FormParam("workflowDefinitionId") String wfId)
1688 throws NotFoundException, UnauthorizedException {
1689 if (service == null || agentService == null || prolongingService == null) {
1690 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
1691 .entity("Scheduler service is unavailable, please wait...").build();
1692 }
1693
1694
1695 boolean adHocRegistration = false;
1696 try {
1697 agentService.getAgent(agentId);
1698 } catch (NotFoundException e) {
1699 Properties adHocProperties = new Properties();
1700 adHocProperties.put(AGENT_REGISTRATION_TYPE, AGENT_REGISTRATION_TYPE_ADHOC);
1701 agentService.setAgentConfiguration(agentId, adHocProperties);
1702 agentService.setAgentState(agentId, AgentState.CAPTURING);
1703 adHocRegistration = true;
1704 logger.info("Temporarily registered agent '{}' for ad-hoc recording", agentId);
1705 }
1706
1707 try {
1708 Date now = new Date();
1709 Date temporaryEndDate = DateTime.now().plus(prolongingService.getInitialTime()).toDate();
1710 try {
1711 List<MediaPackage> events = service.findConflictingEvents(agentId, now, temporaryEndDate);
1712 if (!events.isEmpty()) {
1713 logger.info("An already existing event is in a conflict with the the one to be created on the agent {}!",
1714 agentId);
1715 return Response.status(Status.CONFLICT).build();
1716 }
1717 } catch (SchedulerException e) {
1718 logger.error("Unable to create immediate event on agent {}", agentId, e);
1719 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1720 }
1721
1722 String workflowId = defaultWorkflowDefinitionId;
1723 if (StringUtils.isNotBlank(wfId)) {
1724 workflowId = wfId;
1725 }
1726
1727 Map<String, String> caProperties = new HashMap<>();
1728 caProperties.put("org.opencastproject.workflow.definition", workflowId);
1729 caProperties.put("event.location", agentId);
1730 caProperties.put("event.title", "Capture now event");
1731
1732
1733
1734
1735
1736
1737 DublinCoreCatalog eventCatalog = DublinCores.mkOpencastEpisode().getCatalog();
1738 eventCatalog.set(PROPERTY_TITLE, "Capture now event");
1739 eventCatalog.set(PROPERTY_TEMPORAL,
1740 EncodingSchemeUtils.encodePeriod(new DCMIPeriod(now, temporaryEndDate), Precision.Second));
1741 eventCatalog.set(PROPERTY_SPATIAL, agentId);
1742 eventCatalog.set(PROPERTY_CREATED, EncodingSchemeUtils.encodeDate(new Date(), Precision.Minute));
1743
1744
1745
1746
1747
1748
1749
1750 Map<String, String> wfProperties = new HashMap<>();
1751
1752 MediaPackage mediaPackage = null;
1753 try {
1754 mediaPackage = MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().createNew();
1755 mediaPackage = addCatalog(workspace, IOUtils.toInputStream(eventCatalog.toXmlString(), "UTF-8"),
1756 "dublincore.xml", MediaPackageElements.EPISODE, mediaPackage);
1757
1758 prolongingService.schedule(agentId);
1759 service.addEvent(now, temporaryEndDate, agentId, Collections.<String> emptySet(), mediaPackage, wfProperties,
1760 caProperties, Optional.empty());
1761 return Response.status(Status.CREATED)
1762 .header("Location", serverUrl + serviceUrl + '/' + mediaPackage.getIdentifier().toString() + ".xml")
1763 .build();
1764 } catch (Exception e) {
1765 prolongingService.stop(agentId);
1766 if (e instanceof UnauthorizedException) {
1767 throw (UnauthorizedException) e;
1768 }
1769 logger.error("Unable to create immediate event on agent {}", agentId, e);
1770 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1771 } finally {
1772 if (mediaPackage != null) {
1773 for (MediaPackageElement elem : mediaPackage.getElements()) {
1774 if (MediaPackageElements.EPISODE.matches(elem.getFlavor())) {
1775 try {
1776 workspace.delete(elem.getURI());
1777 } catch (NotFoundException e) {
1778 logger.warn("Unable to find (and hence, delete), this mediapackage '{}' element '{}'",
1779 mediaPackage.getIdentifier(), elem.getIdentifier());
1780 } catch (IOException e) {
1781 throw new RuntimeException(e);
1782 }
1783 }
1784 }
1785 }
1786 }
1787 } catch (Throwable t) {
1788 throw t;
1789 } finally {
1790 if (adHocRegistration) {
1791 agentService.removeAgent(agentId);
1792 logger.info("Removed temporary registration for agent '{}'", agentId);
1793 }
1794 }
1795 }
1796
1797 @DELETE
1798 @Path("capture/{agent}")
1799 @Produces(MediaType.TEXT_PLAIN)
1800 @RestQuery(name = "stopcapture",
1801 description = "Stops an immediate capture.",
1802 returnDescription = "OK if event were successfully stopped",
1803 pathParameters = {
1804 @RestParameter(name = "agent", isRequired = true, description = "The agent identifier", type = Type.STRING)
1805 },
1806 responses = {
1807 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Recording stopped"),
1808 @RestResponse(responseCode = HttpServletResponse.SC_NOT_MODIFIED,
1809 description = "The recording was already stopped"),
1810 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND, description = "There is no such agent"),
1811 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
1812 + "to stop this immediate capture. Maybe you need to authenticate."),
1813 @RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE,
1814 description = "The agent is not ready to communicate")
1815 }
1816 )
1817 public Response stopCapture(@PathParam("agent") String agentId) throws NotFoundException, UnauthorizedException {
1818 if (service == null || agentService == null || prolongingService == null) {
1819 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
1820 .entity("Scheduler service is unavailable, please wait...").build();
1821 }
1822
1823 boolean isAdHoc = false;
1824 try {
1825 Agent agent = agentService.getAgent(agentId);
1826 String registrationType = (String) agent.getConfiguration().get(AGENT_REGISTRATION_TYPE);
1827 isAdHoc = AGENT_REGISTRATION_TYPE_ADHOC.equals(registrationType);
1828 } catch (NotFoundException e) {
1829 logger.debug("Temporarily registered agent '{}' for ad-hoc recording already removed", agentId);
1830 }
1831
1832 try {
1833 String eventId;
1834 MediaPackage mp;
1835 DublinCoreCatalog eventCatalog;
1836 try {
1837 Optional<MediaPackage> current = service.getCurrentRecording(agentId);
1838 if (current.isEmpty()) {
1839 logger.info("No recording to stop found for agent '{}'!", agentId);
1840 return Response.notModified().build();
1841 } else {
1842 mp = current.get();
1843 eventCatalog = DublinCoreUtil.loadEpisodeDublinCore(workspace, mp).get();
1844 eventId = mp.getIdentifier().toString();
1845 }
1846 } catch (Exception e) {
1847 logger.error("Unable to get the immediate recording for agent '{}'", agentId, e);
1848 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1849 }
1850
1851 try {
1852 DCMIPeriod period = EncodingSchemeUtils
1853 .decodeMandatoryPeriod(eventCatalog.getFirst(DublinCore.PROPERTY_TEMPORAL));
1854 eventCatalog.set(PROPERTY_TEMPORAL,
1855 EncodingSchemeUtils.encodePeriod(new DCMIPeriod(period.getStart(), new Date()), Precision.Second));
1856
1857 mp = addCatalog(workspace, IOUtils.toInputStream(eventCatalog.toXmlString(), "UTF-8"), "dublincore.xml",
1858 MediaPackageElements.EPISODE, mp);
1859
1860 service.updateEvent(eventId, Optional.empty(), Optional.empty(), Optional.empty(),
1861 Optional.empty(), Optional.of(mp), Optional.empty(),
1862 Optional.empty());
1863 prolongingService.stop(agentId);
1864 return Response.ok().build();
1865 } catch (UnauthorizedException e) {
1866 throw e;
1867 } catch (Exception e) {
1868 logger.error("Unable to update the temporal of event '{}'", eventId, e);
1869 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1870 }
1871 } catch (Throwable t) {
1872 throw t;
1873 } finally {
1874 if (isAdHoc) {
1875 agentService.removeAgent(agentId);
1876 logger.info("Removed temporary agent registration '{}'", agentId);
1877 }
1878 }
1879 }
1880
1881 @PUT
1882 @Path("capture/{agent}/prolong")
1883 @Produces(MediaType.TEXT_PLAIN)
1884 @RestQuery(name = "prolongcapture",
1885 description = "Prolong an immediate capture.",
1886 returnDescription = "OK if event were successfully prolonged",
1887 pathParameters = {
1888 @RestParameter(name = "agent", isRequired = true, description = "The agent identifier", type = Type.STRING)
1889 }, responses = {
1890 @RestResponse(responseCode = HttpServletResponse.SC_OK, description = "Recording prolonged"),
1891 @RestResponse(responseCode = HttpServletResponse.SC_NOT_FOUND,
1892 description = "No recording found for prolonging"),
1893 @RestResponse(responseCode = HttpServletResponse.SC_UNAUTHORIZED, description = "You do not have permission "
1894 + "to prolong this immediate capture. Maybe you need to authenticate."),
1895 @RestResponse(responseCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE,
1896 description = "The agent is not ready to communicate")
1897 }
1898 )
1899 public Response prolongCapture(@PathParam("agent") String agentId) throws NotFoundException, UnauthorizedException {
1900 if (service == null || agentService == null || prolongingService == null) {
1901 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE)
1902 .entity("Scheduler service is unavailable, please wait...").build();
1903 }
1904 try {
1905 MediaPackage event = prolongingService.getCurrentRecording(agentId);
1906 DublinCoreCatalog dc = DublinCoreUtil.loadEpisodeDublinCore(workspace, event).get();
1907 prolongingService.prolongEvent(event, dc, agentId);
1908 return Response.ok().build();
1909 } catch (NotFoundException | UnauthorizedException e) {
1910 throw e;
1911 } catch (Exception e) {
1912 logger.error("Unable to prolong the immediate recording for agent '{}'", agentId, e);
1913 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1914 }
1915 }
1916
1917 private List<MediaPackage> getConflictingEvents(String device, String rrule,
1918 Long startDate, Long endDate, Long duration, String timezone)
1919 throws IllegalArgumentException, UnauthorizedException, SchedulerException {
1920
1921 List<MediaPackage> events = null;
1922
1923 if (StringUtils.isBlank(device) || startDate == null || endDate == null) {
1924 logger.info("Either agent, start date or end date were not specified");
1925 throw new IllegalArgumentException();
1926 }
1927
1928 RRule rule = null;
1929 if (StringUtils.isNotBlank(rrule)) {
1930 if (duration == null || StringUtils.isBlank(timezone)) {
1931 logger.info("Either duration or timezone were not specified");
1932 throw new IllegalArgumentException();
1933 }
1934
1935 try {
1936 rule = new RRule(rrule);
1937 rule.validate();
1938 } catch (Exception e) {
1939 logger.info("Unable to parse rrule {}: {}", rrule, getMessage(e));
1940 throw new IllegalArgumentException();
1941 }
1942
1943 if (!Arrays.asList(TimeZone.getAvailableIDs()).contains(timezone)) {
1944 logger.info("Unable to parse timezone: {}", timezone);
1945 throw new IllegalArgumentException();
1946 }
1947 }
1948
1949 Date start = new DateTime(startDate).toDateTime(DateTimeZone.UTC).toDate();
1950
1951 Date end = new DateTime(endDate).toDateTime(DateTimeZone.UTC).toDate();
1952
1953 if (StringUtils.isNotBlank(rrule)) {
1954 events = service.findConflictingEvents(device, rule, start, end, duration, TimeZone.getTimeZone(timezone));
1955 } else {
1956 events = service.findConflictingEvents(device, start, end);
1957 }
1958 return events;
1959 }
1960
1961 private MediaPackage addCatalog(Workspace workspace, InputStream in, String fileName,
1962 MediaPackageElementFlavor flavor, MediaPackage mediaPackage) throws IOException {
1963 Catalog[] catalogs = mediaPackage.getCatalogs(flavor);
1964 Catalog c = null;
1965 if (catalogs.length == 1) {
1966 c = catalogs[0];
1967 }
1968
1969
1970 if (c == null) {
1971 c = (Catalog) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
1972 .newElement(MediaPackageElement.Type.Catalog, flavor);
1973 c.setIdentifier(UUID.randomUUID().toString());
1974 logger.info("Adding catalog with flavor {} to mediapackage {}", flavor, mediaPackage);
1975 mediaPackage.add(c);
1976 }
1977
1978
1979 try {
1980 URI catalogUrl = workspace.put(mediaPackage.getIdentifier().toString(), c.getIdentifier(), fileName, in);
1981 c.setURI(catalogUrl);
1982
1983 c.setChecksum(null);
1984 } finally {
1985 IOUtils.closeQuietly(in);
1986 }
1987 return mediaPackage;
1988 }
1989
1990 private String serializeProperties(Map<String, String> properties) {
1991 StringBuilder wfPropertiesString = new StringBuilder();
1992 for (Map.Entry<String, String> entry : properties.entrySet()) {
1993 wfPropertiesString.append(entry.getKey() + "=" + entry.getValue() + "\n");
1994 }
1995 return wfPropertiesString.toString();
1996 }
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007 private Properties parseProperties(String serializedProperties) throws IOException {
2008 Properties caProperties = new Properties();
2009 logger.debug("properties: {}", serializedProperties);
2010 caProperties.load(new StringReader(serializedProperties));
2011 return caProperties;
2012 }
2013
2014
2015
2016
2017
2018
2019
2020
2021 public String getEventListAsJsonString(List<MediaPackage> mpList) throws SchedulerException {
2022 JSONParser parser = new JSONParser();
2023 JSONObject jsonObj = new JSONObject();
2024 JSONArray jsonArray = new JSONArray();
2025 for (MediaPackage mp: mpList) {
2026 JSONObject mpJson;
2027 try {
2028 mpJson = (JSONObject) parser.parse(MediaPackageParser.getAsJSON(mp));
2029 mpJson = (JSONObject) mpJson.get("mediapackage");
2030 jsonArray.add(mpJson);
2031 } catch (org.json.simple.parser.ParseException e) {
2032 logger.warn("Unexpected JSON parse exception for getAsJSON on mp {}", mp.getIdentifier().toString(), e);
2033 throw new SchedulerException(e);
2034 }
2035 }
2036 jsonObj.put("totalCount", String.valueOf(mpList.size()));
2037 jsonObj.put("events", jsonArray);
2038 return jsonObj.toJSONString();
2039 }
2040 }
2041