All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencastproject.adminui.endpoint.JobEndpoint Maven / Gradle / Ivy

There is a newer version: 16.6
Show newest version
/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License
 * at:
 *
 *   http://opensource.org/licenses/ecl2.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */
package org.opencastproject.adminui.endpoint;

import static com.entwinemedia.fn.Stream.$;
import static com.entwinemedia.fn.data.json.Jsons.arr;
import static com.entwinemedia.fn.data.json.Jsons.f;
import static com.entwinemedia.fn.data.json.Jsons.obj;
import static com.entwinemedia.fn.data.json.Jsons.v;
import static org.opencastproject.util.DateTimeSupport.toUTC;

import org.opencastproject.adminui.exception.JobEndpointException;
import org.opencastproject.index.service.resources.list.query.JobsListQuery;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.job.api.Incident;
import org.opencastproject.job.api.IncidentTree;
import org.opencastproject.job.api.Job;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.serviceregistry.api.HostRegistration;
import org.opencastproject.serviceregistry.api.IncidentL10n;
import org.opencastproject.serviceregistry.api.IncidentService;
import org.opencastproject.serviceregistry.api.IncidentServiceException;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.RestUtil;
import org.opencastproject.util.SmartIterator;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.opencastproject.util.requests.SortCriterion;
import org.opencastproject.util.requests.SortCriterion.Order;
import org.opencastproject.workflow.api.WorkflowService;

import com.entwinemedia.fn.Fn;
import com.entwinemedia.fn.Stream;
import com.entwinemedia.fn.data.json.JObject;
import com.entwinemedia.fn.data.json.JValue;
import com.entwinemedia.fn.data.json.Jsons;
import com.entwinemedia.fn.data.json.SimpleSerializer;

import org.apache.commons.lang3.StringUtils;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/")
@RestService(name = "JobProxyService", title = "UI Jobs",
  abstractText = "This service provides the job data for the UI.",
  notes = { "These Endpoints deliver informations about the job required for the UI.",
            "Important: "
              + "This service is for exclusive use by the module admin-ui. Its API might change "
              + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
              + "DO NOT use this for integration of third-party applications."})
@Component(
  immediate = true,
  service = JobEndpoint.class,
  property = {
    "service.description=Admin UI - Job facade Endpoint",
    "opencast.service.type=org.opencastproject.adminui.endpoint.JobEndpoint",
    "opencast.service.path=/admin-ng/job"
  }
)
public class JobEndpoint {

  private static final Logger logger = LoggerFactory.getLogger(JobEndpoint.class);
  private static final SimpleSerializer serializer = new SimpleSerializer();

  public static final Response NOT_FOUND = Response.status(Response.Status.NOT_FOUND).build();

  private enum JobSort {
    CREATOR, OPERATION, PROCESSINGHOST, PROCESSINGNODE, STATUS, STARTED, SUBMITTED, TYPE, ID
  }

  private static final String NEGATE_PREFIX = "-";
  private static final String WORKFLOW_STATUS_TRANSLATION_PREFIX = "EVENTS.EVENTS.DETAILS.WORKFLOWS.OPERATION_STATUS.";
  private static final String JOB_STATUS_TRANSLATION_PREFIX = "SYSTEMS.JOBS.STATUS.";

  private WorkflowService workflowService;
  private ServiceRegistry serviceRegistry;
  private IncidentService incidentService;
  private UserDirectoryService userDirectoryService;

  /** OSGi callback for the workflow service. */
  @Reference
  public void setWorkflowService(WorkflowService workflowService) {
    this.workflowService = workflowService;
  }

  /** OSGi callback for the service registry. */
  @Reference
  public void setServiceRegistry(ServiceRegistry serviceRegistry) {
    this.serviceRegistry = serviceRegistry;
  }

  /** OSGi callback for the incident service. */
  @Reference
  public void setIncidentService(IncidentService incidentService) {
    this.incidentService = incidentService;
  }

  @Reference
  public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
    this.userDirectoryService = userDirectoryService;
  }

  @Activate
  protected void activate(BundleContext bundleContext) {
    logger.info("Activate job endpoint");
  }

  @GET
  @Path("jobs.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(description = "Returns the list of active jobs", name = "jobs", restParameters = {
          @RestParameter(name = "limit", description = "The maximum number of items to return per page", isRequired = false, type = RestParameter.Type.INTEGER),
          @RestParameter(name = "offset", description = "The offset", isRequired = false, type = RestParameter.Type.INTEGER),
          @RestParameter(name = "filter", description = "Filter results by hostname, status or free text query", isRequired = false, type = RestParameter.Type.STRING),
          @RestParameter(name = "sort", description = "The sort order. May include any of the following: CREATOR, OPERATION, PROCESSINGHOST, STATUS, STARTED, SUBMITTED or TYPE. "
                  + "The suffix must be :ASC for ascending or :DESC for descending sort order (e.g. OPERATION:DESC)", isRequired = false, type = RestParameter.Type.STRING)},
          responses = { @RestResponse(description = "Returns the list of active jobs from Opencast", responseCode = HttpServletResponse.SC_OK) },
          returnDescription = "The list of jobs as JSON")
  public Response getJobs(@QueryParam("limit") final int limit, @QueryParam("offset") final int offset,
          @QueryParam("filter") final String filter, @QueryParam("sort") final String sort) {
    JobsListQuery query = new JobsListQuery();
    EndpointUtil.addRequestFiltersToQuery(filter, query);
    query.setLimit(limit);
    query.setOffset(offset);

    String fHostname = null;
    if (query.getHostname().isSome())
      fHostname = StringUtils.trimToNull(query.getHostname().get());
    String fNodeName = null;
    if (query.getNodeName().isSome())
      fNodeName = StringUtils.trimToNull(query.getNodeName().get());
    String fStatus = null;
    if (query.getStatus().isSome())
      fStatus = StringUtils.trimToNull(query.getStatus().get());
    String fFreeText = null;
    if (query.getFreeText().isSome())
      fFreeText = StringUtils.trimToNull(query.getFreeText().get());

    List jobs = new ArrayList<>();
    try {
      String vNodeName;
      Optional server;
      List servers = serviceRegistry.getHostRegistrations();
      for (Job job : serviceRegistry.getActiveJobs()) {
        // filter workflow jobs
        if (StringUtils.equals(WorkflowService.JOB_TYPE, job.getJobType())
                && StringUtils.equals("START_WORKFLOW", job.getOperation()))
          continue;

        // filter by hostname
        if (fHostname != null && !StringUtils.equalsIgnoreCase(job.getProcessingHost(), fHostname))
          continue;

        server = findServerByHost(job.getProcessingHost(), servers);
        vNodeName = server.isPresent() ? server.get().getNodeName() : "";

        // filter by node name
        if (fNodeName != null && (server.isPresent()) && !StringUtils.equalsIgnoreCase(vNodeName, fNodeName))
          continue;

        // filter by status
        if (fStatus != null && !StringUtils.equalsIgnoreCase(job.getStatus().toString(), fStatus))
          continue;

        // fitler by user free text
        if (fFreeText != null
              && !StringUtils.equalsIgnoreCase(job.getProcessingHost(), fFreeText)
              && !StringUtils.equalsIgnoreCase(vNodeName, fFreeText)
              && !StringUtils.equalsIgnoreCase(job.getJobType(), fFreeText)
              && !StringUtils.equalsIgnoreCase(job.getOperation(), fFreeText)
              && !StringUtils.equalsIgnoreCase(job.getCreator(), fFreeText)
              && !StringUtils.equalsIgnoreCase(job.getStatus().toString(), fFreeText)
              && !StringUtils.equalsIgnoreCase(Long.toString(job.getId()), fFreeText)
              && (job.getRootJobId() != null && !StringUtils.equalsIgnoreCase(Long.toString(job.getRootJobId()), fFreeText)))
          continue;
        jobs.add(new JobExtended(job, vNodeName));
      }
    } catch (ServiceRegistryException ex) {
      logger.error("Failed to retrieve jobs list from service registry.", ex);
      return RestUtil.R.serverError();
    }

    JobSort sortKey = JobSort.SUBMITTED;
    boolean ascending = true;
    if (StringUtils.isNotBlank(sort)) {
      try {
        SortCriterion sortCriterion = RestUtils.parseSortQueryParameter(sort).iterator().next();
        sortKey = JobSort.valueOf(sortCriterion.getFieldName().toUpperCase());
        ascending = Order.Ascending == sortCriterion.getOrder()
                || Order.None == sortCriterion.getOrder();
      } catch (WebApplicationException ex) {
        logger.warn("Failed to parse sort criterion \"{}\", invalid format.", sort);
      } catch (IllegalArgumentException ex) {
        logger.warn("Can not apply sort criterion \"{}\", no field with this name.", sort);
      }
    }

    JobComparator comparator = new JobComparator(sortKey, ascending);
    Collections.sort(jobs, comparator);
    List json = getJobsAsJSON(new SmartIterator(
            query.getLimit().getOrElse(0),
            query.getOffset().getOrElse(0))
            .applyLimitAndOffset(jobs));

    return RestUtils.okJsonList(json, offset, limit, jobs.size());
  }

  /* Class to handle additional information related to a job */
  class JobExtended {

    private final Job job;
    private final String nodeName;

    JobExtended(Job job, String nodeName) {
      this.job = job;
      this.nodeName = nodeName;
    }

    public Job getJob() {
      return job;
    }

    public String getNodeName() {
      return nodeName;
    }
  }

  public List getJobsAsJSON(List jobs) {
    List jsonList = new ArrayList<>();
    for (JobExtended jobEx : jobs) {
      Job job = jobEx.getJob();
      long id = job.getId();
      String jobType = job.getJobType();
      String operation = job.getOperation();
      Job.Status status = job.getStatus();
      Date dateCreated = job.getDateCreated();
      String created = null;
      if (dateCreated != null)
        created = DateTimeSupport.toUTC(dateCreated.getTime());
      Date dateStarted = job.getDateStarted();
      String started = null;
      if (dateStarted != null)
        started = DateTimeSupport.toUTC(dateStarted.getTime());
      String creator = job.getCreator();
      String processingHost = job.getProcessingHost();
      String processingNode = jobEx.getNodeName();

      jsonList.add(obj(f("id", v(id)),
              f("type", v(jobType)),
              f("operation", v(operation)),
              f("status", v(JOB_STATUS_TRANSLATION_PREFIX + status.toString())),
              f("submitted", v(created, Jsons.BLANK)),
              f("started", v(started, Jsons.BLANK)),
              f("creator", v(creator, Jsons.BLANK)),
              f("processingHost", v(processingHost, Jsons.BLANK)),
              f("processingNode", v(processingNode, Jsons.BLANK))));
    }

    return jsonList;
  }

  /**
   * Returns the list of incidents for a given workflow instance
   *
   * @param jobId
   *          the workflow instance id
   * @param locale
   *          the language in which title and description shall be returned
   * @param cascade
   *          if true, return the incidents of the given job and those of of its descendants
   * @return the list incidents as JSON array
   * @throws JobEndpointException
   * @throws NotFoundException
   */
  public JValue getIncidentsAsJSON(long jobId, final Locale locale, boolean cascade)
          throws JobEndpointException, NotFoundException {
    final List incidents;
    try {
      final IncidentTree it = incidentService.getIncidentsOfJob(jobId, cascade);
      incidents = cascade ? flatten(it) : it.getIncidents();
    } catch (IncidentServiceException e) {
      throw new JobEndpointException(String.format(
              "Not able to get the incidents for the job %d from the incident service : %s", jobId, e), e.getCause());
    }
    final Stream json = $(incidents).map(new Fn() {
      @Override
      public JValue apply(Incident i) {
        return obj(f("id", v(i.getId())), f("severity", v(i.getSeverity(), Jsons.BLANK)),
                f("timestamp", v(toUTC(i.getTimestamp().getTime()), Jsons.BLANK))).merge(
                localizeIncident(i, locale));
      }
    });
    return arr(json);
  }

  /**
   * Flatten a tree of incidents.
   *
   * @return a list of incidents
   */
  private List flatten(IncidentTree incidentsTree) {
    final List incidents = new ArrayList<>();
    incidents.addAll(incidentsTree.getIncidents());
    for (IncidentTree descendantTree : incidentsTree.getDescendants()) {
      incidents.addAll(flatten(descendantTree));
    }
    return incidents;
  }

  /**
   * Return localized title and description of an incident as JSON.
   *
   * @param incident
   *          the incident to localize
   * @param locale
   *          the locale to be used to create title and description
   * @return JSON object
   */
  private JObject localizeIncident(Incident incident, Locale locale) {
    try {
      final IncidentL10n loc = incidentService.getLocalization(incident.getId(), locale);
      return obj(f("title", v(loc.getTitle(), Jsons.BLANK)), f("description", v(loc.getDescription(), Jsons.BLANK)));
    } catch (Exception e) {
      return obj(f("title", v("")), f("description", v("")));
    }
  }

  /**
   * Return an incident serialized as JSON.
   *
   * @param id
   *          incident id
   * @param locale
   *          the locale to be used to create title and description
   * @return JSON object
   */
  public JValue getIncidentAsJSON(long id, Locale locale) throws JobEndpointException, NotFoundException {
    final Incident incident;
    try {
      incident = incidentService.getIncident(id);
    } catch (IncidentServiceException e) {
      throw new JobEndpointException(String.format("Not able to get the incident %d: %s", id, e), e.getCause());
    }
    return obj(f("id", v(incident.getId(), Jsons.BLANK)), f("job_id", v(incident.getJobId(), Jsons.BLANK)),
            f("severity", v(incident.getSeverity(), Jsons.BLANK)),
            f("timestamp", v(toUTC(incident.getTimestamp().getTime()), Jsons.BLANK)),
            f("processing_host", v(incident.getProcessingHost(), Jsons.BLANK)), f("service_type", v(incident.getServiceType(), Jsons.BLANK)),
            f("technical_details", v(incident.getDescriptionParameters(), Jsons.BLANK)),
            f("details", arr($(incident.getDetails()).map(errorDetailToJson))))
      .merge(localizeIncident(incident, locale));
  }

  private final Fn, JObject> errorDetailToJson = new Fn, JObject>() {
    @Override
    public JObject apply(Tuple detail) {
      return obj(f("name", v(detail.getA(), Jsons.BLANK)), f("value", v(detail.getB(), Jsons.BLANK)));
    }
  };

  private class JobComparator implements Comparator {

    private JobSort sortType;
    private boolean ascending;

    JobComparator(JobSort sortType, boolean ascending) {
      this.sortType = sortType;
      this.ascending = ascending;
    }

    @Override
    public int compare(JobExtended jobEx1, JobExtended jobEx2) {
      int result = 0;
      Object value1 = null;
      Object value2 = null;
      Job job1 = jobEx1.getJob();
      Job job2 = jobEx2.getJob();
      switch (sortType) {
        case CREATOR:
          value1 = job1.getCreator();
          value2 = job2.getCreator();
          break;
        case OPERATION:
          value1 = job1.getOperation();
          value2 = job2.getOperation();
          break;
        case PROCESSINGHOST:
          value1 = job1.getProcessingHost();
          value2 = job2.getProcessingHost();
          break;
        case PROCESSINGNODE:
          value1 = jobEx1.getNodeName();
          value2 = jobEx2.getNodeName();
          break;        case STARTED:
          value1 = job1.getDateStarted();
          value2 = job2.getDateStarted();
          break;
        case STATUS:
          value1 = job1.getStatus();
          value2 = job2.getStatus();
          break;
        case SUBMITTED:
          value1 = job1.getDateCreated();
          value2 = job2.getDateCreated();
          break;
        case TYPE:
          value1 = job1.getJobType();
          value2 = job2.getJobType();
          break;
        case ID:
          value1 = job1.getId();
          value2 = job2.getId();
          break;
        default:
      }

      if (value1 == null) {
        return value2 == null ? 0 : 1;
      }
      if (value2 == null) {
        return -1;
      }
      try {
        result = ((Comparable)value1).compareTo(value2);
      } catch (ClassCastException ex) {
        logger.debug("Can not compare \"{}\" with \"{}\": {}",
                value1, value2, ex);
      }

      return ascending ? result : -1 * result;
    }
  }

  /**
   * @param hostname of server to find in list
   * @param servers list of all host registrations
   */
  private Optional findServerByHost(String hostname, List servers) {
    return servers.stream().filter(o -> o.getBaseUrl().equals(hostname)).findFirst();
  }
}