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