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