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(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    * Callback from OSGi that is called when this service is activated.
115    *
116    * @param cc
117    *          OSGi component context
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   // Todo: Capture agent may send an optional FormParam containing it's configured address.
180   // If this exists don't use request.getRemoteHost() for the URL
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     // Run through and build a map of updates (rather than states)
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       // JSON
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       // XML
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         // Prepares the value to return
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       // Run through and build a map of updates (rather than states)
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 }