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