1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.adminui.endpoint;
22
23 import static org.opencastproject.index.service.util.JSONUtils.mapToJsonObject;
24 import static org.opencastproject.index.service.util.JSONUtils.safeString;
25 import static org.opencastproject.util.DateTimeSupport.toUTC;
26
27 import org.opencastproject.adminui.exception.JobEndpointException;
28 import org.opencastproject.index.service.resources.list.query.JobsListQuery;
29 import org.opencastproject.index.service.util.RestUtils;
30 import org.opencastproject.job.api.Incident;
31 import org.opencastproject.job.api.IncidentTree;
32 import org.opencastproject.job.api.Job;
33 import org.opencastproject.security.api.UserDirectoryService;
34 import org.opencastproject.serviceregistry.api.HostRegistration;
35 import org.opencastproject.serviceregistry.api.IncidentL10n;
36 import org.opencastproject.serviceregistry.api.IncidentService;
37 import org.opencastproject.serviceregistry.api.IncidentServiceException;
38 import org.opencastproject.serviceregistry.api.ServiceRegistry;
39 import org.opencastproject.serviceregistry.api.ServiceRegistryException;
40 import org.opencastproject.util.DateTimeSupport;
41 import org.opencastproject.util.NotFoundException;
42 import org.opencastproject.util.RestUtil;
43 import org.opencastproject.util.SmartIterator;
44 import org.opencastproject.util.data.Tuple;
45 import org.opencastproject.util.doc.rest.RestParameter;
46 import org.opencastproject.util.doc.rest.RestQuery;
47 import org.opencastproject.util.doc.rest.RestResponse;
48 import org.opencastproject.util.doc.rest.RestService;
49 import org.opencastproject.util.requests.SortCriterion;
50 import org.opencastproject.util.requests.SortCriterion.Order;
51 import org.opencastproject.workflow.api.WorkflowService;
52
53 import com.google.gson.JsonArray;
54 import com.google.gson.JsonObject;
55
56 import org.apache.commons.lang3.StringUtils;
57 import org.osgi.framework.BundleContext;
58 import org.osgi.service.component.annotations.Activate;
59 import org.osgi.service.component.annotations.Component;
60 import org.osgi.service.component.annotations.Reference;
61 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import java.util.ArrayList;
66 import java.util.Collections;
67 import java.util.Comparator;
68 import java.util.List;
69 import java.util.Locale;
70 import java.util.Optional;
71
72 import javax.servlet.http.HttpServletResponse;
73 import javax.ws.rs.GET;
74 import javax.ws.rs.Path;
75 import javax.ws.rs.Produces;
76 import javax.ws.rs.QueryParam;
77 import javax.ws.rs.WebApplicationException;
78 import javax.ws.rs.core.MediaType;
79 import javax.ws.rs.core.Response;
80
81 @Path("/admin-ng/job")
82 @RestService(
83 name = "JobProxyService",
84 title = "UI Jobs",
85 abstractText = "This service provides the job data for the UI.",
86 notes = { "These Endpoints deliver informations about the job required for the 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 = JobEndpoint.class,
94 property = {
95 "service.description=Admin UI - Job facade Endpoint",
96 "opencast.service.type=org.opencastproject.adminui.endpoint.JobEndpoint",
97 "opencast.service.path=/admin-ng/job"
98 }
99 )
100 @JaxrsResource
101 public class JobEndpoint {
102
103 private static final Logger logger = LoggerFactory.getLogger(JobEndpoint.class);
104 public static final Response NOT_FOUND = Response.status(Response.Status.NOT_FOUND).build();
105
106 private enum JobSort {
107 CREATOR, OPERATION, PROCESSINGHOST, PROCESSINGNODE, STATUS, STARTED, SUBMITTED, TYPE, ID
108 }
109
110 private static final String NEGATE_PREFIX = "-";
111 private static final String WORKFLOW_STATUS_TRANSLATION_PREFIX = "EVENTS.EVENTS.DETAILS.WORKFLOWS.OPERATION_STATUS.";
112 private static final String JOB_STATUS_TRANSLATION_PREFIX = "SYSTEMS.JOBS.STATUS.";
113
114 private WorkflowService workflowService;
115 private ServiceRegistry serviceRegistry;
116 private IncidentService incidentService;
117 private UserDirectoryService userDirectoryService;
118
119
120 @Reference
121 public void setWorkflowService(WorkflowService workflowService) {
122 this.workflowService = workflowService;
123 }
124
125
126 @Reference
127 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
128 this.serviceRegistry = serviceRegistry;
129 }
130
131
132 @Reference
133 public void setIncidentService(IncidentService incidentService) {
134 this.incidentService = incidentService;
135 }
136
137 @Reference
138 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
139 this.userDirectoryService = userDirectoryService;
140 }
141
142 @Activate
143 protected void activate(BundleContext bundleContext) {
144 logger.info("Activate job endpoint");
145 }
146
147 @GET
148 @Path("jobs.json")
149 @Produces(MediaType.APPLICATION_JSON)
150 @RestQuery(
151 description = "Returns the list of active jobs",
152 name = "jobs",
153 restParameters = {
154 @RestParameter(name = "limit", description = "The maximum number of items to return per page",
155 isRequired = false, type = RestParameter.Type.INTEGER),
156 @RestParameter(name = "offset", description = "The offset",
157 isRequired = false, type = RestParameter.Type.INTEGER),
158 @RestParameter(name = "filter", description = "Filter results by hostname, status or free text query",
159 isRequired = false, type = RestParameter.Type.STRING),
160 @RestParameter(name = "sort", description = "The sort order. May include any of the following: CREATOR, "
161 + "OPERATION, PROCESSINGHOST, STATUS, STARTED, SUBMITTED or TYPE. "
162 + "The suffix must be :ASC for ascending or :DESC for descending sort order (e.g. OPERATION:DESC)",
163 isRequired = false, type = RestParameter.Type.STRING)
164 },
165 responses = {
166 @RestResponse(description = "Returns the list of active jobs from Opencast",
167 responseCode = HttpServletResponse.SC_OK)
168 },
169 returnDescription = "The list of jobs as JSON")
170 public Response getJobs(@QueryParam("limit") final int limit, @QueryParam("offset") final int offset,
171 @QueryParam("filter") final String filter, @QueryParam("sort") final String sort) {
172 JobsListQuery query = new JobsListQuery();
173 EndpointUtil.addRequestFiltersToQuery(filter, query);
174 query.setLimit(limit);
175 query.setOffset(offset);
176
177 String fHostname = null;
178 if (query.getHostname().isPresent()) {
179 fHostname = StringUtils.trimToNull(query.getHostname().get());
180 }
181 String fNodeName = null;
182 if (query.getNodeName().isPresent()) {
183 fNodeName = StringUtils.trimToNull(query.getNodeName().get());
184 }
185 String fStatus = null;
186 if (query.getStatus().isPresent()) {
187 fStatus = StringUtils.trimToNull(query.getStatus().get());
188 }
189 String fFreeText = null;
190 if (query.getFreeText().isPresent()) {
191 fFreeText = StringUtils.trimToNull(query.getFreeText().get());
192 }
193
194 List<JobExtended> jobs = new ArrayList<>();
195 try {
196 String vNodeName;
197 Optional<HostRegistration> server;
198 List<HostRegistration> servers = serviceRegistry.getHostRegistrations();
199 for (Job job : serviceRegistry.getActiveJobs()) {
200
201 if (StringUtils.equals(WorkflowService.JOB_TYPE, job.getJobType())
202 && StringUtils.equals("START_WORKFLOW", job.getOperation())) {
203 continue;
204 }
205
206
207 if (fHostname != null && !StringUtils.equalsIgnoreCase(job.getProcessingHost(), fHostname)) {
208 continue;
209 }
210
211 server = findServerByHost(job.getProcessingHost(), servers);
212 vNodeName = server.isPresent() ? server.get().getNodeName() : "";
213
214
215 if (fNodeName != null && (server.isPresent()) && !StringUtils.equalsIgnoreCase(vNodeName, fNodeName)) {
216 continue;
217 }
218
219
220 if (fStatus != null && !StringUtils.equalsIgnoreCase(job.getStatus().toString(), fStatus)) {
221 continue;
222 }
223
224
225 if (fFreeText != null
226 && !StringUtils.equalsIgnoreCase(job.getProcessingHost(), fFreeText)
227 && !StringUtils.equalsIgnoreCase(vNodeName, fFreeText)
228 && !StringUtils.equalsIgnoreCase(job.getJobType(), fFreeText)
229 && !StringUtils.equalsIgnoreCase(job.getOperation(), fFreeText)
230 && !StringUtils.equalsIgnoreCase(job.getCreator(), fFreeText)
231 && !StringUtils.equalsIgnoreCase(job.getStatus().toString(), fFreeText)
232 && !StringUtils.equalsIgnoreCase(Long.toString(job.getId()), fFreeText)
233 && (job.getRootJobId() != null && !StringUtils.equalsIgnoreCase(Long.toString(job.getRootJobId()),
234 fFreeText))) {
235 continue;
236 }
237 jobs.add(new JobExtended(job, vNodeName));
238 }
239 } catch (ServiceRegistryException ex) {
240 logger.error("Failed to retrieve jobs list from service registry.", ex);
241 return RestUtil.R.serverError();
242 }
243
244 JobSort sortKey = JobSort.SUBMITTED;
245 boolean ascending = true;
246 if (StringUtils.isNotBlank(sort)) {
247 try {
248 SortCriterion sortCriterion = RestUtils.parseSortQueryParameter(sort).iterator().next();
249 sortKey = JobSort.valueOf(sortCriterion.getFieldName().toUpperCase());
250 ascending = Order.Ascending == sortCriterion.getOrder()
251 || Order.None == sortCriterion.getOrder();
252 } catch (WebApplicationException ex) {
253 logger.warn("Failed to parse sort criterion \"{}\", invalid format.", sort);
254 } catch (IllegalArgumentException ex) {
255 logger.warn("Can not apply sort criterion \"{}\", no field with this name.", sort);
256 }
257 }
258
259 JobComparator comparator = new JobComparator(sortKey, ascending);
260 Collections.sort(jobs, comparator);
261 List<JsonObject> json = getJobsAsJSON(new SmartIterator(
262 query.getLimit().orElse(0),
263 query.getOffset().orElse(0))
264 .applyLimitAndOffset(jobs));
265
266 return RestUtils.okJsonList(json, offset, limit, jobs.size());
267 }
268
269
270 class JobExtended {
271
272 private final Job job;
273 private final String nodeName;
274
275 JobExtended(Job job, String nodeName) {
276 this.job = job;
277 this.nodeName = nodeName;
278 }
279
280 public Job getJob() {
281 return job;
282 }
283
284 public String getNodeName() {
285 return nodeName;
286 }
287 }
288
289 public List<JsonObject> getJobsAsJSON(List<JobExtended> jobs) {
290 List<JsonObject> jsonList = new ArrayList<>();
291 for (JobExtended jobEx : jobs) {
292 Job job = jobEx.getJob();
293
294 JsonObject jobJson = new JsonObject();
295 jobJson.addProperty("id", job.getId());
296 jobJson.addProperty("type", job.getJobType());
297 jobJson.addProperty("operation", job.getOperation());
298 jobJson.addProperty("status", JOB_STATUS_TRANSLATION_PREFIX + job.getStatus().toString());
299 jobJson.addProperty("submitted", job.getDateCreated() != null
300 ? DateTimeSupport.toUTC(job.getDateCreated().getTime()) : "");
301 jobJson.addProperty("started", job.getDateStarted() != null
302 ? DateTimeSupport.toUTC(job.getDateStarted().getTime()) : "");
303 jobJson.addProperty("creator", safeString(job.getCreator()));
304 jobJson.addProperty("processingHost", safeString(job.getProcessingHost()));
305 jobJson.addProperty("processingNode", safeString(jobEx.getNodeName()));
306
307 jsonList.add(jobJson);
308 }
309 return jsonList;
310 }
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326 public JsonArray getIncidentsAsJSON(long jobId, final Locale locale, boolean cascade)
327 throws JobEndpointException, NotFoundException {
328 final List<Incident> incidents;
329 try {
330 final IncidentTree it = incidentService.getIncidentsOfJob(jobId, cascade);
331 incidents = cascade ? flatten(it) : it.getIncidents();
332 } catch (IncidentServiceException e) {
333 throw new JobEndpointException(String.format(
334 "Not able to get the incidents for the job %d from the incident service : %s", jobId, e), e.getCause());
335 }
336
337 JsonArray resultArray = new JsonArray();
338 for (Incident i : incidents) {
339 JsonObject incidentJson = new JsonObject();
340
341 incidentJson.addProperty("id", i.getId());
342 incidentJson.addProperty("severity", safeString(i.getSeverity()));
343 incidentJson.addProperty("timestamp", safeString(toUTC(i.getTimestamp().getTime())));
344
345
346 JsonObject localized = localizeIncident(i, locale);
347 for (String key : localized.keySet()) {
348 incidentJson.add(key, localized.get(key));
349 }
350
351 resultArray.add(incidentJson);
352 }
353
354 return resultArray;
355 }
356
357
358
359
360
361
362 private List<Incident> flatten(IncidentTree incidentsTree) {
363 final List<Incident> incidents = new ArrayList<>();
364 incidents.addAll(incidentsTree.getIncidents());
365 for (IncidentTree descendantTree : incidentsTree.getDescendants()) {
366 incidents.addAll(flatten(descendantTree));
367 }
368 return incidents;
369 }
370
371
372
373
374
375
376
377
378
379
380 private JsonObject localizeIncident(Incident incident, Locale locale) {
381 JsonObject localized = new JsonObject();
382
383 try {
384 IncidentL10n loc = incidentService.getLocalization(incident.getId(), locale);
385 localized.addProperty("title", safeString(loc.getTitle()));
386 localized.addProperty("description", safeString(loc.getDescription()));
387 } catch (Exception e) {
388 localized.addProperty("title", "");
389 localized.addProperty("description", "");
390 }
391
392 return localized;
393 }
394
395
396
397
398
399
400
401
402
403
404 public JsonObject getIncidentAsJSON(long id, Locale locale) throws JobEndpointException, NotFoundException {
405 final Incident incident;
406 try {
407 incident = incidentService.getIncident(id);
408 } catch (IncidentServiceException e) {
409 throw new JobEndpointException(String.format("Not able to get the incident %d: %s", id, e), e.getCause());
410 }
411
412 Long rootJobId = null;
413 try {
414 Job job = serviceRegistry.getJob(incident.getJobId());
415 rootJobId = job.getRootJobId();
416 } catch (ServiceRegistryException e) {
417 logger.info("Could not find job \"{}\" in service registry", incident.getJobId());
418 }
419
420 JsonObject json = new JsonObject();
421 json.addProperty("id", incident.getId());
422 json.addProperty("job_id", incident.getJobId());
423 json.addProperty("root_job_id", safeString(rootJobId));
424 json.addProperty("severity", safeString(incident.getSeverity().toString()));
425 json.addProperty("timestamp", toUTC(incident.getTimestamp().getTime()));
426 json.addProperty("processing_host", safeString(incident.getProcessingHost()));
427 json.addProperty("service_type", safeString(incident.getServiceType()));
428 json.add("technical_details", mapToJsonObject(incident.getDescriptionParameters()));
429
430 JsonArray detailsArray = new JsonArray();
431 for (Tuple<String, String> detail : incident.getDetails()) {
432 detailsArray.add(errorDetailToJson(detail));
433 }
434 json.add("details", detailsArray);
435
436 JsonObject localized = localizeIncident(incident, locale);
437 json.add("title", localized.get("title"));
438 json.add("description", localized.get("description"));
439
440 return json;
441 }
442
443 public JsonObject errorDetailToJson(Tuple<String, String> detail) {
444 JsonObject json = new JsonObject();
445 json.addProperty("name", safeString(detail.getA()));
446 json.addProperty("value", safeString(detail.getB()));
447 return json;
448 }
449
450 private class JobComparator implements Comparator<JobExtended> {
451
452 private JobSort sortType;
453 private boolean ascending;
454
455 JobComparator(JobSort sortType, boolean ascending) {
456 this.sortType = sortType;
457 this.ascending = ascending;
458 }
459
460 @Override
461 public int compare(JobExtended jobEx1, JobExtended jobEx2) {
462 int result = 0;
463 Object value1 = null;
464 Object value2 = null;
465 Job job1 = jobEx1.getJob();
466 Job job2 = jobEx2.getJob();
467 switch (sortType) {
468 case CREATOR:
469 value1 = job1.getCreator();
470 value2 = job2.getCreator();
471 break;
472 case OPERATION:
473 value1 = job1.getOperation();
474 value2 = job2.getOperation();
475 break;
476 case PROCESSINGHOST:
477 value1 = job1.getProcessingHost();
478 value2 = job2.getProcessingHost();
479 break;
480 case PROCESSINGNODE:
481 value1 = jobEx1.getNodeName();
482 value2 = jobEx2.getNodeName();
483 break;
484 case STARTED:
485 value1 = job1.getDateStarted();
486 value2 = job2.getDateStarted();
487 break;
488 case STATUS:
489 value1 = job1.getStatus();
490 value2 = job2.getStatus();
491 break;
492 case SUBMITTED:
493 value1 = job1.getDateCreated();
494 value2 = job2.getDateCreated();
495 break;
496 case TYPE:
497 value1 = job1.getJobType();
498 value2 = job2.getJobType();
499 break;
500 case ID:
501 value1 = job1.getId();
502 value2 = job2.getId();
503 break;
504 default:
505 }
506
507 if (value1 == null) {
508 return value2 == null ? 0 : 1;
509 }
510 if (value2 == null) {
511 return -1;
512 }
513 try {
514 result = ((Comparable)value1).compareTo(value2);
515 } catch (ClassCastException ex) {
516 logger.debug("Can not compare \"{}\" with \"{}\"",
517 value1, value2, ex);
518 }
519
520 return ascending ? result : -1 * result;
521 }
522 }
523
524
525
526
527
528 private Optional<HostRegistration> findServerByHost(String hostname, List<HostRegistration> servers) {
529 return servers.stream().filter(o -> o.getBaseUrl().equals(hostname)).findFirst();
530 }
531 }