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