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