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