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.adminui.endpoint;
23  
24  import static com.entwinemedia.fn.data.json.Jsons.arr;
25  import static com.entwinemedia.fn.data.json.Jsons.f;
26  import static com.entwinemedia.fn.data.json.Jsons.obj;
27  import static com.entwinemedia.fn.data.json.Jsons.v;
28  import static org.apache.commons.lang3.StringUtils.trimToNull;
29  import static org.apache.http.HttpStatus.SC_OK;
30  import static org.opencastproject.index.service.util.RestUtils.okJson;
31  import static org.opencastproject.index.service.util.RestUtils.okJsonList;
32  import static org.opencastproject.util.DateTimeSupport.toUTC;
33  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
34  
35  import org.opencastproject.adminui.util.TextFilter;
36  import org.opencastproject.capture.CaptureParameters;
37  import org.opencastproject.capture.admin.api.Agent;
38  import org.opencastproject.capture.admin.api.AgentState;
39  import org.opencastproject.capture.admin.api.CaptureAgentStateService;
40  import org.opencastproject.index.service.resources.list.query.AgentsListQuery;
41  import org.opencastproject.index.service.util.RestUtils;
42  import org.opencastproject.security.api.SecurityService;
43  import org.opencastproject.security.api.UnauthorizedException;
44  import org.opencastproject.security.util.SecurityUtil;
45  import org.opencastproject.util.NotFoundException;
46  import org.opencastproject.util.SmartIterator;
47  import org.opencastproject.util.data.Option;
48  import org.opencastproject.util.doc.rest.RestParameter;
49  import org.opencastproject.util.doc.rest.RestQuery;
50  import org.opencastproject.util.doc.rest.RestResponse;
51  import org.opencastproject.util.doc.rest.RestService;
52  import org.opencastproject.util.requests.SortCriterion;
53  import org.opencastproject.util.requests.SortCriterion.Order;
54  
55  import com.entwinemedia.fn.data.json.Field;
56  import com.entwinemedia.fn.data.json.JValue;
57  import com.entwinemedia.fn.data.json.Jsons;
58  
59  import org.apache.commons.lang3.StringUtils;
60  import org.osgi.service.component.annotations.Component;
61  import org.osgi.service.component.annotations.Reference;
62  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  import java.util.ArrayList;
67  import java.util.Collections;
68  import java.util.Comparator;
69  import java.util.List;
70  import java.util.Map;
71  import java.util.Map.Entry;
72  import java.util.Properties;
73  
74  import javax.servlet.http.HttpServletResponse;
75  import javax.ws.rs.DELETE;
76  import javax.ws.rs.GET;
77  import javax.ws.rs.Path;
78  import javax.ws.rs.PathParam;
79  import javax.ws.rs.Produces;
80  import javax.ws.rs.QueryParam;
81  import javax.ws.rs.core.MediaType;
82  import javax.ws.rs.core.Response;
83  import javax.ws.rs.core.Response.Status;
84  
85  @Path("/admin-ng/capture-agents")
86  @RestService(name = "captureAgents", title = "Capture agents façade service",
87    abstractText = "Provides operations for the capture agents",
88    notes = { "This service offers the default capture agents CRUD Operations for the admin UI.",
89              "<strong>Important:</strong> "
90                + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
91                + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
92                + "DO NOT use this for integration of third-party applications.<em>"})
93  @Component(
94    immediate = true,
95    service = CaptureAgentsEndpoint.class,
96    property = {
97      "service.description=Admin UI - Capture agents facade Endpoint",
98      "opencast.service.type=org.opencastproject.adminui.endpoint.UsersEndpoint",
99      "opencast.service.path=/admin-ng/capture-agents"
100   }
101 )
102 @JaxrsResource
103 public class CaptureAgentsEndpoint {
104 
105   private static final String TRANSLATION_KEY_PREFIX = "CAPTURE_AGENT.DEVICE.";
106 
107   /** The logging facility */
108   private static final Logger logger = LoggerFactory.getLogger(CaptureAgentsEndpoint.class);
109 
110   /** The capture agent service */
111   private CaptureAgentStateService service;
112 
113   private SecurityService securityService;
114 
115   /**
116    * Sets the capture agent service
117    *
118    * @param service
119    *          the capture agent service to set
120    */
121   @Reference
122   public void setCaptureAgentService(CaptureAgentStateService service) {
123     this.service = service;
124   }
125 
126   @Reference
127   public void setSecurityService(SecurityService securityService) {
128     this.securityService = securityService;
129   }
130 
131   @GET
132   @Produces({ MediaType.APPLICATION_JSON })
133   @Path("agents.json")
134   @RestQuery(name = "getAgents", description = "Return all of the known capture agents on the system", restParameters = {
135           @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
136           @RestParameter(defaultValue = "100", description = "The maximum number of items to return per page.", isRequired = false, name = "limit", type = RestParameter.Type.STRING),
137           @RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset", type = RestParameter.Type.STRING),
138           @RestParameter(defaultValue = "false", description = "Define if the inputs should or not returned with the capture agent.", isRequired = false, name = "inputs", type = RestParameter.Type.BOOLEAN),
139           @RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any of the following: STATUS, NAME OR LAST_UPDATED.  Add '_DESC' to reverse the sort order (e.g. STATUS_DESC).", type = STRING) }, responses = { @RestResponse(description = "An XML representation of the agent capabilities", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "")
140   public Response getAgents(@QueryParam("limit") int limit, @QueryParam("offset") int offset,
141           @QueryParam("inputs") boolean inputs, @QueryParam("filter") String filter, @QueryParam("sort") String sort) {
142     Option<String> filterName = Option.none();
143     Option<String> filterStatus = Option.none();
144     Option<Long> filterLastUpdated = Option.none();
145     Option<String> filterText = Option.none();
146     Option<String> optSort = Option.option(trimToNull(sort));
147 
148     Map<String, String> filters = RestUtils.parseFilter(filter);
149     for (String name : filters.keySet()) {
150       if (AgentsListQuery.FILTER_NAME_NAME.equals(name))
151         filterName = Option.some(filters.get(name));
152       if (AgentsListQuery.FILTER_STATUS_NAME.equals(name))
153         filterStatus = Option.some(filters.get(name));
154       if (AgentsListQuery.FILTER_LAST_UPDATED.equals(name)) {
155         try {
156           filterLastUpdated = Option.some(Long.parseLong(filters.get(name)));
157         } catch (NumberFormatException e) {
158           logger.info("Unable to parse long {}", filters.get(name));
159           return Response.status(Status.BAD_REQUEST).build();
160         }
161       }
162       if (AgentsListQuery.FILTER_TEXT_NAME.equals(name) && StringUtils.isNotBlank(filters.get(name)))
163         filterText = Option.some(filters.get(name));
164     }
165 
166     // Filter agents by filter criteria
167     List<Agent> filteredAgents = new ArrayList<>();
168     for (Entry<String, Agent> entry : service.getKnownAgents().entrySet()) {
169       Agent agent = entry.getValue();
170 
171       // Filter list
172       if ((filterName.isSome() && !filterName.get().equals(agent.getName()))
173               || (filterStatus.isSome() && !filterStatus.get().equals(agent.getState()))
174               || (filterLastUpdated.isSome() && filterLastUpdated.get() != agent.getLastHeardFrom())
175               || (filterText.isSome() && !TextFilter.match(filterText.get(), agent.getName(), agent.getState())))
176         continue;
177       filteredAgents.add(agent);
178     }
179     int total = filteredAgents.size();
180 
181     // Sort by status, name or last updated date
182     if (optSort.isSome()) {
183       final ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
184       Collections.sort(filteredAgents, new Comparator<Agent>() {
185         @Override
186         public int compare(Agent agent1, Agent agent2) {
187           for (SortCriterion criterion : sortCriteria) {
188             Order order = criterion.getOrder();
189             switch (criterion.getFieldName()) {
190               case "status":
191                 if (order.equals(Order.Descending))
192                   return agent2.getState().compareTo(agent1.getState());
193                 return agent1.getState().compareTo(agent2.getState());
194               case "name":
195                 if (order.equals(Order.Descending))
196                   return agent2.getName().compareTo(agent1.getName());
197                 return agent1.getName().compareTo(agent2.getName());
198               case "updated":
199                 if (order.equals(Order.Descending))
200                   return agent2.getLastHeardFrom().compareTo(agent1.getLastHeardFrom());
201                 return agent1.getLastHeardFrom().compareTo(agent2.getLastHeardFrom());
202               default:
203                 logger.info("Unknown sort type: {}", criterion.getFieldName());
204                 return 0;
205             }
206           }
207           return 0;
208         }
209       });
210     }
211 
212     // Apply Limit and offset
213     filteredAgents = new SmartIterator<Agent>(limit, offset).applyLimitAndOffset(filteredAgents);
214 
215     // Run through and build a map of updates (rather than states)
216     List<JValue> agentsJSON = new ArrayList<>();
217     for (Agent agent : filteredAgents) {
218       agentsJSON.add(generateJsonAgent(agent, inputs, false));
219     }
220 
221     return okJsonList(agentsJSON, offset, limit, total);
222   }
223 
224   @DELETE
225   @Path("{name}")
226   @Produces({ MediaType.APPLICATION_JSON })
227   @RestQuery(name = "removeAgent", description = "Remove record of a given capture agent", pathParameters = { @RestParameter(name = "name", description = "The name of a given capture agent", isRequired = true, type = RestParameter.Type.STRING) }, restParameters = {}, responses = {
228           @RestResponse(description = "{agentName} removed", responseCode = HttpServletResponse.SC_OK),
229           @RestResponse(description = "The agent {agentname} does not exist", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "")
230   public Response removeAgent(@PathParam("name") String agentName) throws NotFoundException, UnauthorizedException {
231     if (service == null)
232       return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
233 
234     SecurityUtil.checkAgentAccess(securityService, agentName);
235 
236     service.removeAgent(agentName);
237 
238     logger.debug("The agent {} was successfully removed", agentName);
239     return Response.status(SC_OK).build();
240   }
241 
242   @GET
243   @Path("{name}")
244   @Produces({ MediaType.APPLICATION_JSON })
245   @RestQuery(
246     name = "getAgent",
247     description = "Return the capture agent including its configuration and capabilities",
248     pathParameters = {
249       @RestParameter(description = "Name of the capture agent", isRequired = true, name = "name", type = RestParameter.Type.STRING),
250     }, restParameters = {}, responses = {
251       @RestResponse(description = "A JSON representation of the capture agent", responseCode = HttpServletResponse.SC_OK),
252       @RestResponse(description = "The agent {name} does not exist in the system", responseCode = HttpServletResponse.SC_NOT_FOUND)
253     }, returnDescription = "")
254   public Response getAgent(@PathParam("name") String agentName)
255           throws NotFoundException {
256     if (service != null) {
257       Agent agent = service.getAgent(agentName);
258       if (agent != null) {
259         return okJson(generateJsonAgent(agent, true, true));
260       } else {
261         return Response.status(Status.NOT_FOUND).build();
262       }
263     } else {
264       return Response.serverError().status(Response.Status.SERVICE_UNAVAILABLE).build();
265     }
266   }
267 
268   /**
269    * Generate a JSON Object for the given capture agent
270    *
271    * @param agent
272    *          The target capture agent
273    * @param withInputs
274    *          Whether the agent has inputs
275    * @param details
276    *          Whether the configuration and capabilities should be serialized
277    * @return A {@link JValue} representing the capture agent
278    */
279   private JValue generateJsonAgent(Agent agent, boolean withInputs, boolean details) {
280     List<Field> fields = new ArrayList<>();
281     fields.add(f("Status", v(AgentState.TRANSLATION_PREFIX + agent.getState().toUpperCase(), Jsons.BLANK)));
282     fields.add(f("Name", v(agent.getName())));
283     fields.add(f("Update", v(toUTC(agent.getLastHeardFrom()), Jsons.BLANK)));
284     fields.add(f("URL", v(agent.getUrl(), Jsons.BLANK)));
285 
286     if (withInputs) {
287       String devices = (String) agent.getCapabilities().get(CaptureParameters.CAPTURE_DEVICE_NAMES);
288       fields.add(f("inputs", (StringUtils.isEmpty(devices)) ? arr() : generateJsonDevice(devices.split(","))));
289     }
290 
291     if (details) {
292       fields.add(f("configuration", generateJsonProperties(agent.getConfiguration())));
293       fields.add(f("capabilities", generateJsonProperties(agent.getCapabilities())));
294     }
295 
296     return obj(fields);
297   }
298 
299   /**
300    * Generate JSON property list
301    *
302    * @param properties
303    *          Java properties to be serialized
304    * @return A JSON array containing the Java properties as key/value paris
305    */
306   private JValue generateJsonProperties(Properties properties) {
307     List<JValue> fields = new ArrayList<>();
308     if (properties != null) {
309       for (String key : properties.stringPropertyNames()) {
310         fields.add(obj(f("key", v(key)), f("value", v(properties.getProperty(key)))));
311       }
312     }
313     return arr(fields);
314   }
315 
316   /**
317    * Generate a JSON devices list
318    *
319    * @param devices
320    *          an array of devices String
321    * @return A {@link JValue} representing the devices
322    */
323   private JValue generateJsonDevice(String[] devices) {
324     List<JValue> jsonDevices = new ArrayList<>();
325     for (String device : devices) {
326       jsonDevices.add(obj(f("id", v(device)), f("value", v(TRANSLATION_KEY_PREFIX + device.toUpperCase()))));
327     }
328     return arr(jsonDevices);
329   }
330 }