View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   * The REST endpoint for the capture agent service on the capture device
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    * Callback from OSGi that is called when this service is activated.
116    *
117    * @param cc
118    *          OSGi component context
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   // Todo: Capture agent may send an optional FormParam containing it's configured address.
185   // If this exists don't use request.getRemoteHost() for the URL
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     // Run through and build a map of updates (rather than states)
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       // JSON
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       // XML
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         // Prepares the value to return
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       // Run through and build a map of updates (rather than states)
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 }