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  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   /** OSGi callback for the workflow service. */
128   @Reference
129   public void setWorkflowService(WorkflowService workflowService) {
130     this.workflowService = workflowService;
131   }
132 
133   /** OSGi callback for the service registry. */
134   @Reference
135   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
136     this.serviceRegistry = serviceRegistry;
137   }
138 
139   /** OSGi callback for the incident service. */
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         // filter workflow jobs
193         if (StringUtils.equals(WorkflowService.JOB_TYPE, job.getJobType())
194                 && StringUtils.equals("START_WORKFLOW", job.getOperation()))
195           continue;
196 
197         // filter by hostname
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         // filter by node name
205         if (fNodeName != null && (server.isPresent()) && !StringUtils.equalsIgnoreCase(vNodeName, fNodeName))
206           continue;
207 
208         // filter by status
209         if (fStatus != null && !StringUtils.equalsIgnoreCase(job.getStatus().toString(), fStatus))
210           continue;
211 
212         // fitler by user free text
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   /* Class to handle additional information related to a job */
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    * Returns the list of incidents for a given workflow instance
311    *
312    * @param jobId
313    *          the workflow instance id
314    * @param locale
315    *          the language in which title and description shall be returned
316    * @param cascade
317    *          if true, return the incidents of the given job and those of of its descendants
318    * @return the list incidents as JSON array
319    * @throws JobEndpointException
320    * @throws NotFoundException
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    * Flatten a tree of incidents.
345    *
346    * @return a list of incidents
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    * Return localized title and description of an incident as JSON.
359    *
360    * @param incident
361    *          the incident to localize
362    * @param locale
363    *          the locale to be used to create title and description
364    * @return JSON object
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    * Return an incident serialized as JSON.
377    *
378    * @param id
379    *          incident id
380    * @param locale
381    *          the locale to be used to create title and description
382    * @return JSON object
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     return obj(f("id", v(incident.getId(), Jsons.BLANK)), f("job_id", v(incident.getJobId(), Jsons.BLANK)),
392             f("severity", v(incident.getSeverity(), Jsons.BLANK)),
393             f("timestamp", v(toUTC(incident.getTimestamp().getTime()), Jsons.BLANK)),
394             f("processing_host", v(incident.getProcessingHost(), Jsons.BLANK)), f("service_type", v(incident.getServiceType(), Jsons.BLANK)),
395             f("technical_details", v(incident.getDescriptionParameters(), Jsons.BLANK)),
396             f("details", arr($(incident.getDetails()).map(errorDetailToJson))))
397       .merge(localizeIncident(incident, locale));
398   }
399 
400   private final Fn<Tuple<String, String>, JObject> errorDetailToJson = new Fn<Tuple<String, String>, JObject>() {
401     @Override
402     public JObject apply(Tuple<String, String> detail) {
403       return obj(f("name", v(detail.getA(), Jsons.BLANK)), f("value", v(detail.getB(), Jsons.BLANK)));
404     }
405   };
406 
407   private class JobComparator implements Comparator<JobExtended> {
408 
409     private JobSort sortType;
410     private boolean ascending;
411 
412     JobComparator(JobSort sortType, boolean ascending) {
413       this.sortType = sortType;
414       this.ascending = ascending;
415     }
416 
417     @Override
418     public int compare(JobExtended jobEx1, JobExtended jobEx2) {
419       int result = 0;
420       Object value1 = null;
421       Object value2 = null;
422       Job job1 = jobEx1.getJob();
423       Job job2 = jobEx2.getJob();
424       switch (sortType) {
425         case CREATOR:
426           value1 = job1.getCreator();
427           value2 = job2.getCreator();
428           break;
429         case OPERATION:
430           value1 = job1.getOperation();
431           value2 = job2.getOperation();
432           break;
433         case PROCESSINGHOST:
434           value1 = job1.getProcessingHost();
435           value2 = job2.getProcessingHost();
436           break;
437         case PROCESSINGNODE:
438           value1 = jobEx1.getNodeName();
439           value2 = jobEx2.getNodeName();
440           break;        case STARTED:
441           value1 = job1.getDateStarted();
442           value2 = job2.getDateStarted();
443           break;
444         case STATUS:
445           value1 = job1.getStatus();
446           value2 = job2.getStatus();
447           break;
448         case SUBMITTED:
449           value1 = job1.getDateCreated();
450           value2 = job2.getDateCreated();
451           break;
452         case TYPE:
453           value1 = job1.getJobType();
454           value2 = job2.getJobType();
455           break;
456         case ID:
457           value1 = job1.getId();
458           value2 = job2.getId();
459           break;
460         default:
461       }
462 
463       if (value1 == null) {
464         return value2 == null ? 0 : 1;
465       }
466       if (value2 == null) {
467         return -1;
468       }
469       try {
470         result = ((Comparable)value1).compareTo(value2);
471       } catch (ClassCastException ex) {
472         logger.debug("Can not compare \"{}\" with \"{}\"",
473                 value1, value2, ex);
474       }
475 
476       return ascending ? result : -1 * result;
477     }
478   }
479 
480   /**
481    * @param hostname of server to find in list
482    * @param servers list of all host registrations
483    */
484   private Optional<HostRegistration> findServerByHost(String hostname, List<HostRegistration> servers) {
485     return servers.stream().filter(o -> o.getBaseUrl().equals(hostname)).findFirst();
486   }
487 }