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