IncidentServiceEndpoint.java

/*
 * 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.serviceregistry.impl.endpoint;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static org.opencastproject.util.EqualsUtil.eq;
import static org.opencastproject.util.RestUtil.R.badRequest;
import static org.opencastproject.util.RestUtil.R.ok;
import static org.opencastproject.util.RestUtil.getResponseType;

import org.opencastproject.job.api.Incident;
import org.opencastproject.job.api.Incident.Severity;
import org.opencastproject.job.api.IncidentTree;
import org.opencastproject.job.api.JaxbIncident;
import org.opencastproject.job.api.JaxbIncidentDigestList;
import org.opencastproject.job.api.JaxbIncidentFullList;
import org.opencastproject.job.api.JaxbIncidentFullTree;
import org.opencastproject.job.api.JaxbIncidentList;
import org.opencastproject.job.api.JaxbIncidentTree;
import org.opencastproject.job.api.Job;
import org.opencastproject.job.api.JobParser;
import org.opencastproject.rest.RestConstants;
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.systems.OpencastConstants;
import org.opencastproject.util.DateTimeSupport;
import org.opencastproject.util.LocalHashMap;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.UrlSupport;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestParameter.Type;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;

import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

/** REST endpoint for Incident Service. */
@Path("/incidents")
@RestService(name = "incidentservice", title = "Incident Service", abstractText = "This service creates, edits and retrieves and helps managing incidents.", notes = {
        "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
        "If the service is down or not working it will return a status 503, this means the the underlying service is "
                + "not working and is either restarting or has failed",
        "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
                + "other words, there is a bug! You should file an error report with your server logs from the time when the "
                + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"})
@Component(
  property = {
    "service.description=Incident Service REST Endpoint",
    "opencast.service.type=org.opencastproject.incident",
    "opencast.service.path=/incidents"
  },
  immediate = true,
  service = { IncidentServiceEndpoint.class }
)
@JaxrsResource
public class IncidentServiceEndpoint {
  /** Logging utility */
  private static final Logger logger = LoggerFactory.getLogger(IncidentServiceEndpoint.class);

  public static final String FMT_FULL = "full";
  public static final String FMT_DIGEST = "digest";
  public static final String FMT_SYS = "sys";
  private static final String FMT_DEFAULT = FMT_SYS;

  /** The incident service */
  protected IncidentService svc;

  /** The remote service manager */
  protected ServiceRegistry serviceRegistry = null;

  /** This server's base URL */
  protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;

  /** The REST endpoint's base URL */
  protected String serviceUrl = "/incidents";

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

  /**
   * The method that is called, when the service is activated
   *
   * @param cc
   *         The ComponentContext of this service
   */
  public void activate(ComponentContext cc) {
    // Get the configured server URL
    if (cc != null) {
      String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
      if (StringUtils.isNotBlank(ccServerUrl))
        serverUrl = ccServerUrl;
      serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
    }
  }

  @GET
  @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
  @Path("job/incidents.{type:xml|json}")
  @RestQuery(name = "incidentsofjobaslist",
             description = "Returns the job incidents with the given identifiers.",
             returnDescription = "Returns the job incidents.",
             pathParameters = {
                     @RestParameter(name = "type", isRequired = true,
                                    description = "The media type of the response [xml|json]",
                                    defaultValue = "xml",
                                    type = Type.STRING)},
             restParameters = {
                     @RestParameter(name = "id", isRequired = true, description = "The job identifiers.", type = Type.INTEGER),
                     @RestParameter(name = "format", isRequired = false,
                                    description = "The response format [full|digest|sys]. Defaults to sys",
                                    defaultValue = "sys",
                                    type = Type.STRING)},
             responses = {
                     @RestResponse(responseCode = SC_OK, description = "The job incidents.")})
  public Response getIncidentsOfJobAsList(
          @Context HttpServletRequest request,
          @QueryParam("id") final List<Long> jobIds,
          @QueryParam("format") @DefaultValue(FMT_DEFAULT) final String format,
          @PathParam("type") final String type) {
    try {
      final List<Incident> incidents = svc.getIncidentsOfJob(jobIds);
      final MediaType mt = getResponseType(type);
      if (eq(FMT_SYS, format)) {
        return ok(mt, new JaxbIncidentList(incidents));
      } else if (eq(FMT_DIGEST, format)) {
        return ok(mt, new JaxbIncidentDigestList(svc, request.getLocale(), incidents));
      } else if (eq(FMT_FULL, format)) {
        return ok(mt, new JaxbIncidentFullList(svc, request.getLocale(), incidents));
      } else {
        return unknownFormat();
      }
    } catch (NotFoundException e) {
      // should not happen
      logger.error("Unable to get job incident! Consistency issue!", e);
      throw new WebApplicationException(e, INTERNAL_SERVER_ERROR);
    } catch (IncidentServiceException e) {
      logger.warn("Unable to get job incident for id {}: {}", jobIds, e.getMessage());
      throw new WebApplicationException(INTERNAL_SERVER_ERROR);
    }
  }

  @GET
  @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
  @Path("job/{id}.{type:xml|json}")
  @RestQuery(name = "incidentsofjobastree",
             description = "Returns the job incident for the job with the given identifier.",
             returnDescription = "Returns the job incident.",
             pathParameters = {
                     @RestParameter(name = "id", isRequired = true, description = "The job identifier.", type = Type.INTEGER),
                     @RestParameter(name = "type", isRequired = true,
                                    description = "The media type of the response [xml|json]",
                                    defaultValue = "xml",
                                    type = Type.STRING)},
             restParameters = {
                     @RestParameter(name = "cascade", isRequired = false, description = "Whether to show the cascaded incidents.",
                                    type = Type.BOOLEAN, defaultValue = "false"),
                     @RestParameter(name = "format", isRequired = false,
                                    description = "The response format [full|digest|sys]. Defaults to sys",
                                    defaultValue = "sys",
                                    type = Type.STRING)},
             responses = {
                     @RestResponse(responseCode = SC_OK, description = "The job incident."),
                     @RestResponse(responseCode = SC_NOT_FOUND, description = "No job incident with this identifier was found.")})
  public Response getIncidentsOfJobAsTree(
          @Context HttpServletRequest request,
          @PathParam("id") final long jobId,
          @QueryParam("cascade") @DefaultValue("false") boolean cascade,
          @QueryParam("format") @DefaultValue(FMT_DEFAULT) final String format,
          @PathParam("type") final String type)
          throws NotFoundException {
    try {
      final IncidentTree tree = svc.getIncidentsOfJob(jobId, cascade);
      final MediaType mt = getResponseType(type);
      if (eq(FMT_SYS, format)) {
        return ok(mt, new JaxbIncidentTree(tree));
      } else if (eq(FMT_DIGEST, format)) {
        return ok(mt, new JaxbIncidentFullTree(svc, request.getLocale(), tree));
      } else if (eq(FMT_FULL, format)) {
        return ok(mt, new JaxbIncidentFullTree(svc, request.getLocale(), tree));
      } else {
        return unknownFormat();
      }
    } catch (IncidentServiceException e) {
      logger.warn("Unable to get job incident for id {}: {}", jobId, e.getMessage());
      throw new WebApplicationException(INTERNAL_SERVER_ERROR);
    }
  }

  @GET
  @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
  @Path("{id}.{type:xml|json}")
  @RestQuery(name = "incidentjson",
             description = "Returns the job incident by it's incident identifier.",
             returnDescription = "Returns the job incident.",
             pathParameters = {
                     @RestParameter(name = "id", isRequired = true, description = "The incident identifier.", type = Type.INTEGER),
                     @RestParameter(name = "type", isRequired = true,
                                    description = "The media type of the response [xml|json]",
                                    defaultValue = "xml",
                                    type = Type.STRING)},
             responses = {
                     @RestResponse(responseCode = SC_OK, description = "The job incident."),
                     @RestResponse(responseCode = SC_NOT_FOUND, description = "No job incident with this identifier was found.")})
  public Response getIncident(@PathParam("id") final long incidentId, @PathParam("type") final String type)
          throws NotFoundException {
    try {
      Incident incident = svc.getIncident(incidentId);
      return ok(getResponseType(type), new JaxbIncident(incident));
    } catch (IncidentServiceException e) {
      logger.warn("Unable to get job incident for incident id {}", incidentId, e);
      throw new WebApplicationException(INTERNAL_SERVER_ERROR);
    }
  }

  @GET
  @SuppressWarnings("unchecked")
  @Produces(MediaType.APPLICATION_JSON)
  @Path("localization/{id}")
  @RestQuery(name = "getlocalization",
             description = "Returns the localization of an incident by it's id as JSON",
             returnDescription = "The localization of the incident as JSON",
             pathParameters = {
                     @RestParameter(name = "id", isRequired = true, description = "The incident identifiers.", type = Type.INTEGER)},
             restParameters = {
                     @RestParameter(name = "locale", isRequired = true, description = "The locale.", type = Type.STRING)},
             responses = {
                     @RestResponse(responseCode = SC_OK, description = "The localization of the given job incidents."),
                     @RestResponse(responseCode = SC_NOT_FOUND, description = "No job incident with this incident identifier was found.")})
  public Response getLocalization(@PathParam("id") final long incidentId, @QueryParam("locale") String locale)
          throws NotFoundException {
    try {
      IncidentL10n localization = svc.getLocalization(incidentId, LocaleUtils.toLocale(locale));
      JSONObject json = new JSONObject();
      json.put("title", localization.getTitle());
      json.put("description", localization.getDescription());
      return Response.ok(json.toJSONString()).build();
    } catch (IncidentServiceException e) {
      logger.warn("Unable to get job localization of jo incident:", e);
      throw new WebApplicationException(INTERNAL_SERVER_ERROR);
    }
  }

  @POST
  @Produces(MediaType.APPLICATION_XML)
  @Path("/")
  @RestQuery(name = "postincident", description = "Creates a new job incident and returns it as XML", returnDescription = "Returns the created job incident as XML", restParameters = {
          @RestParameter(name = "job", isRequired = true, description = "The job on where to create the incident", type = Type.TEXT),
          @RestParameter(name = "date", isRequired = true, description = "The incident creation date", type = Type.STRING),
          @RestParameter(name = "code", isRequired = true, description = "The incident error code", type = Type.STRING),
          @RestParameter(name = "severity", isRequired = true, description = "The incident error code", type = Type.STRING),
          @RestParameter(name = "details", isRequired = false, description = "The incident details", type = Type.TEXT),
          @RestParameter(name = "params", isRequired = false, description = "The incident parameters", type = Type.TEXT)}, responses = {
          @RestResponse(responseCode = SC_CREATED, description = "New job incident has been created"),
          @RestResponse(responseCode = SC_BAD_REQUEST, description = "Unable to parse the one of the form params"),
          @RestResponse(responseCode = SC_CONFLICT, description = "No job incident related job exists")})
  public Response postIncident(@FormParam("job") String jobXml, @FormParam("date") String date,
                               @FormParam("code") String code, @FormParam("severity") String severityString,
                               @FormParam("details") String details, @FormParam("params") LocalHashMap params) {
    Job job;
    Date timestamp;
    Severity severity;
    Map<String, String> map = new HashMap<String, String>();
    List<Tuple<String, String>> list = new ArrayList<Tuple<String, String>>();
    try {
      job = JobParser.parseJob(jobXml);
      timestamp = new Date(DateTimeSupport.fromUTC(date));
      severity = Severity.valueOf(severityString);
      if (params != null)
        map = params.getMap();

      if (StringUtils.isNotBlank(details)) {
        final JSONArray array = (JSONArray) JSONValue.parse(details);
        for (int i = 0; i < array.size(); i++) {
          JSONObject tuple = (JSONObject) array.get(i);
          list.add(Tuple.tuple((String) tuple.get("title"), (String) tuple.get("content")));
        }
      }
    } catch (Exception e) {
      return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build();
    }

    try {
      Incident incident = svc.storeIncident(job, timestamp, code, severity, map, list);
      String uri = UrlSupport.concat(serverUrl, serviceUrl, Long.toString(incident.getId()), ".xml");
      return Response.created(new URI(uri)).entity(new JaxbIncident(incident)).build();
    } catch (IllegalStateException e) {
      return Response.status(Status.CONFLICT).build();
    } catch (Exception e) {
      logger.warn("Unable to post incident for job {}", job.getId(), e);
      throw new WebApplicationException(INTERNAL_SERVER_ERROR);
    }
  }

  private Response unknownFormat() {
    return badRequest("Unknown format");
  }
}