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.capture.admin.endpoint;
23
24 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25 import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
26 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
27 import static javax.servlet.http.HttpServletResponse.SC_OK;
28 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
29 import static org.opencastproject.capture.admin.api.AgentState.KNOWN_STATES;
30
31 import org.opencastproject.capture.admin.api.Agent;
32 import org.opencastproject.capture.admin.api.AgentStateUpdate;
33 import org.opencastproject.capture.admin.api.CaptureAgentStateService;
34 import org.opencastproject.capture.admin.impl.RecordingStateUpdate;
35 import org.opencastproject.scheduler.api.Recording;
36 import org.opencastproject.scheduler.api.SchedulerException;
37 import org.opencastproject.scheduler.api.SchedulerService;
38 import org.opencastproject.util.NotFoundException;
39 import org.opencastproject.util.PropertiesResponse;
40 import org.opencastproject.util.doc.rest.RestParameter;
41 import org.opencastproject.util.doc.rest.RestParameter.Type;
42 import org.opencastproject.util.doc.rest.RestQuery;
43 import org.opencastproject.util.doc.rest.RestResponse;
44 import org.opencastproject.util.doc.rest.RestService;
45
46 import com.google.gson.Gson;
47 import com.google.gson.JsonSyntaxException;
48
49 import org.apache.commons.io.IOUtils;
50 import org.apache.commons.lang3.StringUtils;
51 import org.osgi.service.component.ComponentContext;
52 import org.osgi.service.component.annotations.Activate;
53 import org.osgi.service.component.annotations.Component;
54 import org.osgi.service.component.annotations.Reference;
55 import org.osgi.service.component.annotations.ReferenceCardinality;
56 import org.osgi.service.component.annotations.ReferencePolicy;
57 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 import java.io.ByteArrayInputStream;
62 import java.io.IOException;
63 import java.util.LinkedList;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Map.Entry;
67 import java.util.Properties;
68
69 import javax.servlet.http.HttpServletRequest;
70 import javax.servlet.http.HttpServletResponse;
71 import javax.ws.rs.DELETE;
72 import javax.ws.rs.FormParam;
73 import javax.ws.rs.GET;
74 import javax.ws.rs.POST;
75 import javax.ws.rs.Path;
76 import javax.ws.rs.PathParam;
77 import javax.ws.rs.Produces;
78 import javax.ws.rs.WebApplicationException;
79 import javax.ws.rs.core.Context;
80 import javax.ws.rs.core.MediaType;
81 import javax.ws.rs.core.Response;
82
83
84
85
86 @Path("/capture-admin")
87 @RestService(
88 name = "captureadminservice",
89 title = "Capture Admin Service",
90 abstractText = "This service is a registry of capture agents and their recordings.",
91 notes = {
92 "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
93 "If the service is down or not working it will return a status 503, this means the the underlying service is "
94 + "not working and is either restarting or has failed",
95 "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
96 + "other words, there is a bug! You should file an error report with your server logs from the time when the "
97 + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
98 @Component(
99 immediate = true,
100 service = CaptureAgentStateRestService.class,
101 property = {
102 "service.description=Capture Agent Admin REST Endpoint",
103 "opencast.service.type=org.opencastproject.capture.admin",
104 "opencast.service.path=/capture-admin"
105 }
106 )
107 @JaxrsResource
108 public class CaptureAgentStateRestService {
109
110 private static final Logger logger = LoggerFactory.getLogger(CaptureAgentStateRestService.class);
111 private CaptureAgentStateService service;
112 private SchedulerService schedulerService;
113
114
115
116
117
118
119
120 @Activate
121 public void activate(ComponentContext cc) {
122 }
123
124 @Reference(
125 cardinality = ReferenceCardinality.OPTIONAL,
126 policy = ReferencePolicy.DYNAMIC,
127 unbind = "unsetService"
128 )
129 public void setService(CaptureAgentStateService service) {
130 this.service = service;
131 }
132
133 public void unsetService(CaptureAgentStateService service) {
134 if (this.service == service) {
135 this.service = null;
136 }
137 }
138
139 @Reference
140 public void setSchedulerService(SchedulerService schedulerService) {
141 this.schedulerService = schedulerService;
142 }
143
144 public CaptureAgentStateRestService() {
145 }
146
147 @GET
148 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
149 @Path("agents/{name}.{format:xml|json}")
150 @RestQuery(
151 name = "getAgent",
152 description = "Return the state of a given capture agent",
153 pathParameters = {
154 @RestParameter(name = "name", description = "Name of the capture agent", isRequired = true, type = Type.STRING),
155 @RestParameter(name = "format", description = "The output format (json or xml) of the response body.",
156 isRequired = true, type = RestParameter.Type.STRING)
157 },
158 restParameters = {},
159 responses = {
160 @RestResponse(description = "{agentState}", responseCode = SC_OK),
161 @RestResponse(description = "The agent {agentName} does not exist", responseCode = SC_NOT_FOUND),
162 @RestResponse(description = "If the {format} is not xml or json", responseCode = SC_METHOD_NOT_ALLOWED),
163 @RestResponse(description = "iCapture agent state service unavailable", responseCode = SC_SERVICE_UNAVAILABLE)
164 },
165 returnDescription = "")
166 public Response getAgentState(@PathParam("name") String agentName, @PathParam("format") String format)
167 throws NotFoundException {
168 if (service == null) {
169 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
170 }
171
172 Agent ret = service.getAgent(agentName);
173 logger.debug("Returning agent state for {}", agentName);
174 if ("json".equals(format)) {
175 return Response.ok(new AgentStateUpdate(ret)).type(MediaType.APPLICATION_JSON).build();
176 } else {
177 return Response.ok(new AgentStateUpdate(ret)).type(MediaType.APPLICATION_XML).build();
178 }
179 }
180
181 @POST
182 @Produces(MediaType.TEXT_HTML)
183 @Path("agents/{name}")
184
185
186 @RestQuery(
187 name = "setAgentState",
188 description = "Set the status of a given capture agent",
189 pathParameters = {
190 @RestParameter(name = "name", isRequired = true, type = Type.STRING, description = "Name of the capture agent")
191 },
192 restParameters = {
193 @RestParameter(name = "address", isRequired = false, type = Type.STRING, description = "Address of the agent"),
194 @RestParameter(name = "state", isRequired = true, type = Type.STRING, description = "The state of the capture "
195 + "agent. Known states are: idle, shutting_down, capturing, uploading, unknown, offline, error")
196 },
197 responses = {
198 @RestResponse(description = "{agentName} set to {state}", responseCode = SC_OK),
199 @RestResponse(description = "{state} is empty or not known", responseCode = SC_BAD_REQUEST),
200 @RestResponse(description = "Capture agent state service not available", responseCode = SC_SERVICE_UNAVAILABLE)
201 },
202 returnDescription = "")
203 public Response setAgentState(@Context HttpServletRequest request, @FormParam("address") String address,
204 @PathParam("name") String agentName, @FormParam("state") String state) throws NotFoundException {
205 if (service == null) {
206 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
207 }
208
209 if (!KNOWN_STATES.contains(state)) {
210 logger.debug("'{}' is not a valid state", state);
211 return Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).build();
212 }
213
214 if (StringUtils.isEmpty(address)) {
215 address = request.getRemoteHost();
216 }
217
218 logger.debug("Agents URL: {}", address);
219
220 boolean agentStateUpdated = service.setAgentState(agentName, state);
221 boolean agentUrlUpdated = service.setAgentUrl(agentName, address);
222
223 if (!agentStateUpdated && !agentUrlUpdated) {
224 logger.debug("{}'s state '{}' and url '{}' has not changed, nothing has been updated", agentName, state, address);
225 return Response.ok().build();
226 }
227 logger.debug("{}'s state successfully set to {}", agentName, state);
228 return Response.ok(agentName + " set to " + state).build();
229 }
230
231 @DELETE
232 @Path("agents/{name}")
233 @Produces(MediaType.TEXT_HTML)
234 @RestQuery(
235 name = "removeAgent",
236 description = "Remove record of a given capture agent",
237 pathParameters = {
238 @RestParameter(name = "name", description = "Name of the capture agent", isRequired = true, type = Type.STRING)
239 },
240 restParameters = {},
241 responses = {
242 @RestResponse(description = "{agentName} removed", responseCode = SC_OK),
243 @RestResponse(description = "The agent {agentname} does not exist", responseCode = SC_NOT_FOUND)
244 },
245 returnDescription = "")
246 public Response removeAgent(@PathParam("name") String agentName) throws NotFoundException {
247 if (service == null) {
248 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
249 }
250
251 service.removeAgent(agentName);
252
253 logger.debug("The agent {} was successfully removed", agentName);
254 return Response.ok(agentName + " removed").build();
255 }
256
257 @GET
258 @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON })
259 @Path("agents.{type:xml|json}")
260 @RestQuery(
261 name = "getKnownAgents",
262 description = "Return all of the known capture agents on the system",
263 pathParameters = {
264 @RestParameter(description = "The Document type", isRequired = true, name = "type", type = Type.STRING)
265 },
266 restParameters = {},
267 responses = {
268 @RestResponse(description = "An XML representation of the agent capabilities", responseCode = SC_OK)
269 },
270 returnDescription = "")
271 public Response getKnownAgents(@PathParam("type") String type) {
272 if (service == null) {
273 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
274 }
275
276 logger.debug("Returning list of known agents...");
277 LinkedList<AgentStateUpdate> update = new LinkedList<AgentStateUpdate>();
278 Map<String, Agent> data = service.getKnownAgents();
279 logger.debug("Agents: {}", data);
280
281 for (Entry<String, Agent> e : data.entrySet()) {
282 update.add(new AgentStateUpdate(e.getValue()));
283 }
284
285 if ("json".equals(type)) {
286 return Response.ok(new AgentStateUpdateList(update)).type(MediaType.APPLICATION_JSON).build();
287 } else {
288 return Response.ok(new AgentStateUpdateList(update)).type(MediaType.TEXT_XML).build();
289 }
290 }
291
292 @GET
293 @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON })
294 @Path("agents/{name}/capabilities.{type:xml|json}")
295 @RestQuery(
296 name = "getAgentCapabilities",
297 description = "Return the capabilities of a given capture agent",
298 pathParameters = {
299 @RestParameter(description = "Name of the capture agent", isRequired = true, name = "name", type = Type.STRING),
300 @RestParameter(description = "The Document type", isRequired = true, name = "type", type = Type.STRING)
301 },
302 restParameters = {},
303 responses = {
304 @RestResponse(description = "An XML representation of the agent capabilities", responseCode = SC_OK),
305 @RestResponse(description = "The agent {name} does not exist in the system", responseCode = SC_NOT_FOUND)
306 },
307 returnDescription = "")
308 public Response getCapabilities(@PathParam("name") String agentName, @PathParam("type") String type)
309 throws NotFoundException {
310 if (service == null) {
311 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
312 }
313
314 PropertiesResponse r = new PropertiesResponse(service.getAgentCapabilities(agentName));
315 if ("json".equals(type)) {
316 return Response.ok(r).type(MediaType.APPLICATION_JSON).build();
317 } else {
318 return Response.ok(r).type(MediaType.TEXT_XML).build();
319 }
320 }
321
322 @GET
323 @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON })
324 @Path("agents/{name}/configuration.{type:xml|json}")
325 @RestQuery(
326 name = "getAgentConfiguration",
327 description = "Return the configuration of a given capture agent",
328 pathParameters = {
329 @RestParameter(description = "Name of the capture agent", isRequired = true, name = "name", type = Type.STRING),
330 @RestParameter(description = "The Document type", isRequired = true, name = "type", type = Type.STRING)
331 },
332 restParameters = {},
333 responses = {
334 @RestResponse(description = "An XML or JSON representation of the agent configuration", responseCode = SC_OK),
335 @RestResponse(description = "The agent {name} does not exist in the system", responseCode = SC_NOT_FOUND)
336 },
337 returnDescription = "")
338 public Response getConfiguration(@PathParam("name") String agentName, @PathParam("type") String type)
339 throws NotFoundException {
340 if (service == null) {
341 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
342 }
343
344 PropertiesResponse r = new PropertiesResponse(service.getAgentConfiguration(agentName));
345 logger.debug("Returning configuration for the agent {}", agentName);
346
347 if ("json".equals(type)) {
348 return Response.ok(r).type(MediaType.APPLICATION_JSON).build();
349 } else {
350 return Response.ok(r).type(MediaType.TEXT_XML).build();
351 }
352 }
353
354 @POST
355 @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON })
356 @Path("agents/{name}/configuration")
357 @RestQuery(
358 name = "setAgentStateConfiguration",
359 description = "Set the configuration of a given capture agent, registering it if it does not exist",
360 pathParameters = {
361 @RestParameter(description = "Name of the capture agent", isRequired = true, name = "name", type = Type.STRING)
362 },
363 restParameters = {
364 @RestParameter(description = "An XML or JSON representation of the capabilities. XML as specified in "
365 + "http://java.sun.com/dtd/properties.dtd (friendly names as keys, device locations as corresponding values)",
366 type = Type.TEXT, isRequired = true, name = "configuration")
367 },
368 responses = {
369 @RestResponse(description = "An XML or JSON representation of the agent configuration", responseCode = SC_OK),
370 @RestResponse(description = "The configuration format is incorrect OR the agent name is blank or null",
371 responseCode = SC_BAD_REQUEST)
372 },
373 returnDescription = "")
374 public Response setConfiguration(@PathParam("name") String agentName,
375 @FormParam("configuration") String configuration) {
376 if (service == null) {
377 return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
378 }
379
380 if (StringUtils.isBlank(configuration)) {
381 logger.debug("The configuration data cannot be blank");
382 return Response.serverError().status(Response.Status.BAD_REQUEST).build();
383 }
384
385 Properties caps;
386
387 if (StringUtils.startsWith(configuration, "{")) {
388
389 Gson gson = new Gson();
390 try {
391 caps = gson.fromJson(configuration, Properties.class);
392 if (!service.setAgentConfiguration(agentName, caps)) {
393 logger.debug("'{}''s configuration has not been updated because nothing has been changed", agentName);
394 }
395 return Response.ok(gson.toJson(caps)).type(MediaType.APPLICATION_JSON).build();
396 } catch (JsonSyntaxException e) {
397 logger.debug("Exception when deserializing capabilities: {}", e.getMessage());
398 return Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).build();
399 }
400
401 } else {
402
403 caps = new Properties();
404 ByteArrayInputStream bais = null;
405 try {
406 bais = new ByteArrayInputStream(configuration.getBytes());
407 caps.loadFromXML(bais);
408 if (!service.setAgentConfiguration(agentName, caps)) {
409 logger.debug("'{}''s configuration has not been updated because nothing has been changed", agentName);
410 }
411
412
413 PropertiesResponse r = new PropertiesResponse(caps);
414 logger.debug("{}'s configuration updated", agentName);
415 return Response.ok(r).type(MediaType.TEXT_XML).build();
416 } catch (IOException e) {
417 logger.debug("Unexpected I/O Exception when unmarshalling the capabilities: {}", e.getMessage());
418 return Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).build();
419 } finally {
420 IOUtils.closeQuietly(bais);
421 }
422 }
423 }
424
425 @GET
426 @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON })
427 @Path("recordings/{id}.{type:xml|json|}")
428 @RestQuery(
429 name = "getRecordingState",
430 description = "Return the state of a given recording",
431 pathParameters = {
432 @RestParameter(description = "The ID of a given recording", isRequired = true, name = "id", type = Type.STRING),
433 @RestParameter(description = "The Documenttype", isRequired = true, name = "type", type = Type.STRING)
434 },
435 restParameters = {},
436 responses = {
437 @RestResponse(description = "Returns the state of the recording with the correct id", responseCode = SC_OK),
438 @RestResponse(description = "The recording with the specified ID does not exist", responseCode = SC_NOT_FOUND)
439 },
440 returnDescription = "")
441 public Response getRecordingState(@PathParam("id") String id, @PathParam("type") String type)
442 throws NotFoundException {
443 try {
444 Recording rec = schedulerService.getRecordingState(id);
445
446 logger.debug("Submitting state for recording {}", id);
447 if ("json".equals(type)) {
448 return Response.ok(new RecordingStateUpdate(rec)).type(MediaType.APPLICATION_JSON).build();
449 } else {
450 return Response.ok(new RecordingStateUpdate(rec)).type(MediaType.TEXT_XML).build();
451 }
452 } catch (SchedulerException e) {
453 logger.debug("Unable to get recording state of {}", id, e);
454 return Response.serverError().build();
455 }
456 }
457
458 @POST
459 @Path("recordings/{id}")
460 @RestQuery(
461 name = "setRecordingState",
462 description = "Set the status of a given recording, registering it if it is new",
463 pathParameters = {
464 @RestParameter(description = "The ID of a given recording", isRequired = true, name = "id", type = Type.STRING)
465 },
466 restParameters = {
467 @RestParameter(description = "The state of the recording. Known states: unknown, capturing, capture_finished, "
468 + "capture_error, manifest, manifest_error, manifest_finished, compressing, compressing_error, uploading, "
469 + "upload_finished, upload_error.", isRequired = true, name = "state", type = Type.STRING)
470 },
471 responses = {
472 @RestResponse(description = "{id} set to {state}", responseCode = SC_OK),
473 @RestResponse(description = "{id} or {state} is empty or {state} is not known", responseCode = SC_BAD_REQUEST),
474 @RestResponse(description = "Recording with {id} could not be found",
475 responseCode = HttpServletResponse.SC_NOT_FOUND)
476 },
477 returnDescription = "")
478 public Response setRecordingState(@PathParam("id") String id, @FormParam("state") String state)
479 throws NotFoundException {
480 if (StringUtils.isEmpty(id) || StringUtils.isEmpty(state)) {
481 return Response.serverError().status(Response.Status.BAD_REQUEST).build();
482 }
483
484 try {
485 if (schedulerService.updateRecordingState(id, state)) {
486 return Response.ok(id + " set to " + state).build();
487 } else {
488 return Response.status(Response.Status.BAD_REQUEST).build();
489 }
490 } catch (SchedulerException e) {
491 logger.debug("Unable to set recording state of {}", id, e);
492 return Response.serverError().build();
493 }
494 }
495
496 @DELETE
497 @Path("recordings/{id}")
498 @RestQuery(
499 name = "removeRecording",
500 description = "Remove record of a given recording",
501 pathParameters = {
502 @RestParameter(description = "The ID of a given recording", isRequired = true, name = "id", type = Type.STRING)
503 },
504 restParameters = {},
505 responses = {
506 @RestResponse(description = "{id} removed", responseCode = SC_OK),
507 @RestResponse(description = "{id} is empty", responseCode = SC_BAD_REQUEST),
508 @RestResponse(description = "Recording with {id} could not be found", responseCode = SC_NOT_FOUND),
509 },
510 returnDescription = "")
511 public Response removeRecording(@PathParam("id") String id) throws NotFoundException {
512 if (StringUtils.isEmpty(id)) {
513 return Response.serverError().status(Response.Status.BAD_REQUEST).build();
514 }
515
516 try {
517 schedulerService.removeRecording(id);
518 return Response.ok(id + " removed").build();
519 } catch (SchedulerException e) {
520 logger.debug("Unable to remove recording with id '{}'", id, e);
521 return Response.serverError().build();
522 }
523 }
524
525 @GET
526 @Produces(MediaType.TEXT_XML)
527 @Path("recordings")
528 @RestQuery(
529 name = "getAllRecordings",
530 description = "Return all registered recordings and their state",
531 pathParameters = {},
532 restParameters = {},
533 responses = {
534 @RestResponse(description = "Returns all known recordings.", responseCode = SC_OK)
535 },
536 returnDescription = "")
537 public List<RecordingStateUpdate> getAllRecordings() {
538 try {
539 LinkedList<RecordingStateUpdate> update = new LinkedList<RecordingStateUpdate>();
540 Map<String, Recording> data = schedulerService.getKnownRecordings();
541
542 for (Entry<String, Recording> e : data.entrySet()) {
543 update.add(new RecordingStateUpdate(e.getValue()));
544 }
545 return update;
546 } catch (SchedulerException e) {
547 logger.debug("Unable to get all recordings", e);
548 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
549 }
550 }
551
552 }