ServiceRegistryEndpoint.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_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import org.opencastproject.job.api.JaxbJob;
import org.opencastproject.job.api.JaxbJobList;
import org.opencastproject.job.api.Job;
import org.opencastproject.job.api.JobParser;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.serviceregistry.api.HostRegistration;
import org.opencastproject.serviceregistry.api.JaxbHostRegistration;
import org.opencastproject.serviceregistry.api.JaxbHostRegistrationList;
import org.opencastproject.serviceregistry.api.JaxbServiceHealth;
import org.opencastproject.serviceregistry.api.JaxbServiceRegistration;
import org.opencastproject.serviceregistry.api.JaxbServiceRegistrationList;
import org.opencastproject.serviceregistry.api.JaxbServiceStatisticsList;
import org.opencastproject.serviceregistry.api.ServiceRegistration;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceRegistryException;
import org.opencastproject.serviceregistry.api.ServiceState;
import org.opencastproject.serviceregistry.api.SystemLoad;
import org.opencastproject.serviceregistry.impl.ServiceRegistryJpaImpl;
import org.opencastproject.systems.OpencastConstants;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.UrlSupport;
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.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONValue;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
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;

/**
 * Displays hosts and the service IDs they provide.
 */
@Path("/services")
@RestService(
    name = "serviceregistry",
    title = "Service Registry",
    notes = { "All paths above are relative to the REST endpoint base" },
    abstractText = "Provides registration and management functions for servers and services in this Opencast instance "
        + "or cluster.")
@Component(
  property = {
    "service.description=Service Registry REST Endpoint",
    "opencast.service.type=org.opencastproject.serviceregistry",
    "opencast.service.path=/services"
  },
  immediate = true,
  service = { ServiceRegistryEndpoint.class }
)
@JaxrsResource
public class ServiceRegistryEndpoint {

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

  private SecurityService securityService = null;

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

  /** The service path for this endpoint */
  protected String servicePath = "/";

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

  @Reference
  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  /**
   * Callback from OSGi that is called when this service is activated.
   *
   * @param cc
   *          OSGi component context
   */
  @Activate
  public void activate(ComponentContext cc) {
    serverUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
    servicePath = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
  }

  @GET
  @Path("statistics.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "statisticsasjson",
      description = "List the service registrations in the cluster, along with some simple statistics",
      returnDescription = "The service statistics.",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "A JSON representation of the service statistics")
      })
  public Response getStatisticsAsJson() {
    try {
      return Response.ok(new JaxbServiceStatisticsList(serviceRegistry.getServiceStatistics())).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
    }
  }

  @GET
  @Path("statistics.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "statisticsasxml",
      description = "List the service registrations in the cluster, along with some simple statistics",
      returnDescription = "The service statistics.",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "An XML representation of the service statistics")
      })
  public Response getStatisticsAsXml() throws ServiceRegistryException {
    return getStatisticsAsJson();
  }

  @POST
  @Path("sanitize")
  @RestQuery(
      name = "sanitize",
      description = "Sets the given service to NORMAL state",
      returnDescription = "No content",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier",
              type = Type.STRING, defaultValue = ""),
          @RestParameter(name = "host", isRequired = true, description = "The host providing the service, "
              + "including the http(s) protocol", type = Type.STRING, defaultValue = "")
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "The service was successfully sanitized"),
          @RestResponse(responseCode = SC_NOT_FOUND,
              description = "No service of that type on that host is registered.")
      })
  public Response sanitize(@FormParam("serviceType") String serviceType, @FormParam("host") String host)
          throws NotFoundException {
    serviceRegistry.sanitize(serviceType, host);
    return Response.status(Status.NO_CONTENT).build();
  }

  @POST
  @Path("register")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "register",
      description = "Add a new service registration to the cluster.",
      returnDescription = "The service registration.",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier",
              type = Type.STRING, defaultValue = ""),
          @RestParameter(name = "host", isRequired = true, description = "The host providing the service, "
              + "including the http(s) protocol", type = Type.STRING, defaultValue = ""),
          @RestParameter(name = "path", isRequired = true, description = "The service path on the host",
              type = Type.STRING, defaultValue = ""),
          @RestParameter(name = "jobProducer", isRequired = true, description = "Whether this service is a producer of "
              + "long running jobs requiring dispatch", type = Type.STRING, defaultValue = "false")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "An XML representation of the new service registration")
      })
  public JaxbServiceRegistration register(@FormParam("serviceType") String serviceType, @FormParam("host") String host,
          @FormParam("path") String path, @FormParam("jobProducer") boolean jobProducer) {
    try {
      return new JaxbServiceRegistration(serviceRegistry.registerService(serviceType, host, path, jobProducer));
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @POST
  @Path("unregister")
  @RestQuery(
      name = "unregister",
      description = "Removes a service registration.",
      returnDescription = "No content",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier",
              type = Type.STRING),
          @RestParameter(name = "host", isRequired = true, description = "The host providing the service, "
              + "including the http(s) protocol", type = Type.STRING)
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "The service was unregistered successfully")
      })
  public Response unregister(@FormParam("serviceType") String serviceType, @FormParam("host") String host) {
    try {
      serviceRegistry.unRegisterService(serviceType, host);
      return Response.status(Status.NO_CONTENT).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @POST
  @Path("enablehost")
  @RestQuery(
      name = "enablehost",
      description = "Enable a server from the cluster.",
      returnDescription = "No content.",
      restParameters = {
          @RestParameter(name = "host", isRequired = true,
              description = "The host name, including the http(s) protocol", type = Type.STRING)
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was enabled successfully"),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "The host does not exist")
      })
  public Response enableHost(@FormParam("host") String host) throws NotFoundException {
    try {
      serviceRegistry.enableHost(host);
      return Response.status(Status.NO_CONTENT).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @POST
  @Path("disablehost")
  @RestQuery(
      name = "disablehost",
      description = "Disable a server from the cluster.",
      returnDescription = "No content.",
      restParameters = {
          @RestParameter(name = "host", isRequired = true,
              description = "The host name, including the http(s) protocol", type = Type.STRING)
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was disabled successfully"),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "The host does not exist")
      })
  public Response disableHost(@FormParam("host") String host) throws NotFoundException {
    try {
      serviceRegistry.disableHost(host);
      return Response.status(Status.NO_CONTENT).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @POST
  @Path("registerhost")
  @RestQuery(
      name = "registerhost",
      description = "Add a new server to the cluster.",
      returnDescription = "No content.",
      restParameters = {
          @RestParameter(name = "host", isRequired = true,
              description = "The host name, including the http(s) protocol", type = Type.STRING),
          @RestParameter(name = "address", isRequired = true, description = "The IP address", type = Type.STRING),
          @RestParameter(name = "nodeName", isRequired = true,
              description = "Descriptive node name", type = Type.STRING),
          @RestParameter(name = "memory", isRequired = true, description = "The allocated memory", type = Type.STRING),
          @RestParameter(name = "cores", isRequired = true, description = "The available cores", type = Type.STRING),
          @RestParameter(name = "maxLoad", isRequired = true, description = "The maximum load this host support",
              type = Type.STRING)
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was registered successfully")
      })
  public void register(@FormParam("host") String host, @FormParam("address") String address,
          @FormParam("nodeName") String nodeName, @FormParam("memory") long memory, @FormParam("cores") int cores,
          @FormParam("maxLoad") float maxLoad) {
    try {
      serviceRegistry.registerHost(host, address, nodeName, memory, cores, maxLoad);
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @POST
  @Path("unregisterhost")
  @RestQuery(
      name = "unregisterhost",
      description = "Removes a server from the cluster.",
      returnDescription = "No content.",
      restParameters = {
        @RestParameter(name = "host", isRequired = true, description = "The host name, including the http(s) protocol",
            type = Type.STRING)
      },
      responses = {
        @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was removed successfully")
      })
  public Response unregister(@FormParam("host") String host) {
    try {
      serviceRegistry.unregisterHost(host);
      return Response.status(Status.NO_CONTENT).build();
    } catch (ServiceRegistryException e) {
      if (e.getCause() instanceof IllegalArgumentException) {
        return Response.status(Status.NOT_FOUND).entity(e.getMessage()).build();
      }
      throw new WebApplicationException(e);
    }
  }

  @POST
  @Path("maintenance")
  @RestQuery(
      name = "maintenance",
      description = "Sets the maintenance status for a server in the cluster.",
      returnDescription = "No content.",
      restParameters = {
          @RestParameter(name = "host", isRequired = true, type = Type.STRING,
              description = "The host name, including the http(s) protocol"),
          @RestParameter(name = "maintenance", isRequired = true, type = Type.BOOLEAN,
              description = "Whether this host should be put into maintenance mode (true) or not")
      }, responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was registered successfully"),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "Host not found")
      })
  public Response setMaintenanceMode(@FormParam("host") String host, @FormParam("maintenance") boolean maintenance)
          throws NotFoundException {
    try {
      serviceRegistry.setMaintenanceStatus(host, maintenance);
      return Response.status(Status.NO_CONTENT).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("available.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "availableasxml",
      description = "Lists available services by service type identifier, ordered by load.",
      returnDescription = "The services list as XML",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Returned the available services."),
          @RestResponse(responseCode = SC_BAD_REQUEST, description = "No service type specified, bad request.")
      })
  public Response getAvailableServicesAsXml(@QueryParam("serviceType") String serviceType) {

    if (isBlank(serviceType)) {
      throw new WebApplicationException(
          Response.status(Status.BAD_REQUEST).entity("Service type must be specified").build());
    }

    Map<String, String> properties = securityService.getOrganization().getProperties();

    JaxbServiceRegistrationList registrations = new JaxbServiceRegistrationList();
    try {
      for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByLoad(serviceType)) {
        JaxbServiceRegistration jaxbReg = new JaxbServiceRegistration(reg);
        URL internalHostUrl = new URL(jaxbReg.getHost());
        String tenantSpecificHost = StringUtils.trimToNull(properties.get("org.opencastproject.host."
            + internalHostUrl.getHost()));
        if (StringUtils.isNotBlank(tenantSpecificHost)) {
          jaxbReg.setHost(tenantSpecificHost);
        }
        registrations.add(jaxbReg);
      }
      return Response.ok(registrations).build();
    } catch (ServiceRegistryException | MalformedURLException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("available.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "availableasjson",
      description = "Lists available services by service type identifier, ordered by load.",
      returnDescription = "The services list as JSON",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Returned the available services."),
          @RestResponse(responseCode = SC_BAD_REQUEST, description = "No service type specified, bad request.")
      })
  public Response getAvailableServicesAsJson(@QueryParam("serviceType") String serviceType) {
    return getAvailableServicesAsXml(serviceType);
  }

  @GET
  @Path("health.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "health",
      description = "Checks the status of the registered services",
      returnDescription = "Returns NO_CONTENT if services are in a proper state",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier"),
          @RestParameter(name = "host", isRequired = false, type = Type.STRING,
              description = "The host, including the http(s) protocol")
      }, responses = {
          @RestResponse(responseCode = SC_OK, description = "Service states returned"),
          @RestResponse(responseCode = SC_NOT_FOUND,
              description = "No service of that type on that host is registered."),
          @RestResponse(responseCode = SC_SERVICE_UNAVAILABLE,
              description = "An error has occurred during stats processing")
      })
  public Response getHealthStatusAsJson(@QueryParam("serviceType") String serviceType, @QueryParam("host") String host)
          throws NotFoundException {
    return getHealthStatus(serviceType, host);
  }

  @GET
  @Path("health.xml")
  @Produces(MediaType.APPLICATION_XML)
  @RestQuery(
      name = "health",
      description = "Checks the status of the registered services",
      returnDescription = "Returns NO_CONTENT if services are in a proper state",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier"),
          @RestParameter(name = "host", isRequired = false, type = Type.STRING,
              description = "The host, including the http(s) protocol")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Service states returned"),
          @RestResponse(responseCode = SC_NOT_FOUND,
              description = "No service of that type on that host is registered."),
          @RestResponse(responseCode = SC_SERVICE_UNAVAILABLE,
              description = "An error has occurred during stats processing")
      })
  public Response getHealthStatus(@QueryParam("serviceType") String serviceType, @QueryParam("host") String host)
          throws NotFoundException {
    try {
      List<ServiceRegistration> services = null;
      if (isNotBlank(serviceType) && isNotBlank(host)) {
        // This is a request for one specific service. Return it, or SC_NOT_FOUND if not found
        ServiceRegistration reg = serviceRegistry.getServiceRegistration(serviceType, host);
        if (reg == null) {
          throw new NotFoundException();
        }

        services = new LinkedList<ServiceRegistration>();
        services.add(reg);
      } else if (isBlank(serviceType) && isBlank(host)) {
        // This is a request for all service registrations
        services = serviceRegistry.getServiceRegistrations();
      } else if (isNotBlank(serviceType)) {
        // This is a request for all service registrations of a particular type
        services = serviceRegistry.getServiceRegistrationsByType(serviceType);
      } else if (isNotBlank(host)) {
        // This is a request for all service registrations of a particular host
        services = serviceRegistry.getServiceRegistrationsByHost(host);
      }

      int healthy = 0;
      int warning = 0;
      int error = 0;
      for (ServiceRegistration reg : services) {
        if (ServiceState.NORMAL == reg.getServiceState()) {
          healthy++;
        } else if (ServiceState.WARNING == reg.getServiceState()) {
          warning++;
        } else if (ServiceState.ERROR == reg.getServiceState()) {
          error++;
        } else {
           error++;
        }
      }
      JaxbServiceHealth stats = new JaxbServiceHealth(healthy, warning, error);
      return Response.ok(stats).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("services.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "servicesasxml",
      description = "Returns a service registraton or list of available service registrations as XML.",
      returnDescription = "The services list as XML",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier"),
          @RestParameter(name = "host", isRequired = false, type = Type.STRING,
              description = "The host, including the http(s) protocol")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Returned the available service."),
          @RestResponse(responseCode = SC_NOT_FOUND,
              description = "No service of that type on that host is registered.")
      })
  public JaxbServiceRegistrationList getRegistrationsAsXml(@QueryParam("serviceType") String serviceType,
          @QueryParam("host") String host) throws NotFoundException {
    JaxbServiceRegistrationList registrations = new JaxbServiceRegistrationList();
    try {
      if (isNotBlank(serviceType) && isNotBlank(host)) {
        // This is a request for one specific service. Return it, or SC_NOT_FOUND if not found
        ServiceRegistration reg = serviceRegistry.getServiceRegistration(serviceType, host);
        if (reg == null) {
          throw new NotFoundException();
        } else {
          return new JaxbServiceRegistrationList(new JaxbServiceRegistration(reg));
        }
      } else if (isBlank(serviceType) && isBlank(host)) {
        // This is a request for all service registrations
        for (ServiceRegistration reg : serviceRegistry.getServiceRegistrations()) {
          registrations.add(new JaxbServiceRegistration(reg));
        }
      } else if (isNotBlank(serviceType)) {
        // This is a request for all service registrations of a particular type
        for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByType(serviceType)) {
          registrations.add(new JaxbServiceRegistration(reg));
        }
      } else if (isNotBlank(host)) {
        // This is a request for all service registrations of a particular host
        for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByHost(host)) {
          registrations.add(new JaxbServiceRegistration(reg));
        }
      }
      return registrations;
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("services.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "servicesasjson",
      description = "Returns a service registraton or list of available service registrations as JSON.",
      returnDescription = "The services list as XML",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier"),
          @RestParameter(name = "host", isRequired = false, type = Type.STRING,
              description = "The host, including the http(s) protocol")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Returned the available service."),
          @RestResponse(responseCode = SC_NOT_FOUND,
              description = "No service of that type on that host is registered.")
      })
  public JaxbServiceRegistrationList getRegistrationsAsJson(@QueryParam("serviceType") String serviceType,
          @QueryParam("host") String host) throws NotFoundException {
    return getRegistrationsAsXml(serviceType, host);
  }

  @GET
  @Path("hosts.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "hostsasxml",
      description = "Returns a host registraton or list of available host registrations as XML.",
      returnDescription = "The host list as XML",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Returned the available hosts.")
      })
  public JaxbHostRegistrationList getHostsAsXml() throws NotFoundException {
    JaxbHostRegistrationList registrations = new JaxbHostRegistrationList();
    try {
      for (HostRegistration reg : serviceRegistry.getHostRegistrations()) {
        registrations.add(new JaxbHostRegistration(reg));
      }
      return registrations;
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("hosts.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "hostsasjson",
      description = "Returns a host registraton or list of available host registrations as JSON.",
      returnDescription = "The host list as JSON",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Returned the available hosts.")
      })
  public JaxbHostRegistrationList getHostsAsJson() throws NotFoundException {
    return getHostsAsXml();
  }

  @POST
  @Path("job")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "createjob",
      description = "Creates a new job.",
      returnDescription = "An XML representation of the job.",
      restParameters = {
          @RestParameter(name = "jobType", isRequired = true, type = Type.STRING,
              description = "The job type identifier"),
          @RestParameter(name = "host", isRequired = true, type = Type.STRING,
              description = "The creating host, including the http(s) protocol"),
          @RestParameter(name = "operation", isRequired = true, type = Type.STRING,
              description = "The operation this job should execute"),
          @RestParameter(name = "payload", isRequired = false, type = Type.TEXT,
              description = "The job type identifier"),
          @RestParameter(name = "start", isRequired = false, type = Type.BOOLEAN,
              description = "Whether the job should be queued for dispatch and execution"),
          @RestParameter(name = "jobLoad", isRequired = false, type = Type.STRING,
              description = "The load this job will incur on the system"),
          @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
              description = "An argument for the operation"),
          @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
              description = "An argument for the operation"),
          @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
              description = "An argument for the operation"),
          @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
              description = "An argument for the operation"),
          @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
              description = "An argument for the operation")
      },
      responses = {
          @RestResponse(responseCode = SC_CREATED, description = "Job created."),
          @RestResponse(responseCode = SC_BAD_REQUEST,
              description = "The required parameters were not supplied, bad request.")
      })
  public Response createJob(@Context HttpServletRequest request) {
    String[] argArray = request.getParameterValues("arg");
    List<String> arguments = null;
    if (argArray != null && argArray.length > 0) {
      arguments = Arrays.asList(argArray);
    }
    String jobType = request.getParameter("jobType");
    String operation = request.getParameter("operation");
    String host = request.getParameter("host");
    String payload = request.getParameter("payload");
    boolean start = StringUtils.isBlank(request.getParameter("start"))
            || Boolean.TRUE.toString().equalsIgnoreCase(request.getParameter("start"));
    try {
      Job job = null;
      if (StringUtils.isNotBlank(request.getParameter("jobLoad"))) {
        Float jobLoad = Float.parseFloat(request.getParameter("jobLoad"));
        job = ((ServiceRegistryJpaImpl) serviceRegistry).createJob(host, jobType, operation, arguments, payload,
              start, serviceRegistry.getCurrentJob(), jobLoad);
      } else {
        job = ((ServiceRegistryJpaImpl) serviceRegistry).createJob(host, jobType, operation, arguments, payload,
                start, serviceRegistry.getCurrentJob());
      }
      return Response.created(job.getUri()).entity(new JaxbJob(job)).build();
    } catch (IllegalArgumentException e) {
      throw new WebApplicationException(Status.BAD_REQUEST);
    } catch (Exception e) {
      throw new WebApplicationException(e);
    }
  }

  @PUT
  @Path("job/{id}.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "updatejob",
      description = "Updates an existing job",
      returnDescription = "No content",
      pathParameters = {
          @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The job identifier")
      },
      restParameters = {
          @RestParameter(name = "job", isRequired = true, type = Type.TEXT, description = "The updated job as XML")
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "Job updated."),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "Job not found.")
      })
  public Response updateJob(@PathParam("id") String id, @FormParam("job") String jobXml) throws NotFoundException {
    try {
      Job job = JobParser.parseJob(jobXml);
      serviceRegistry.updateJob(job);
      return Response.status(Status.NO_CONTENT).build();
    } catch (Exception e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("job/{id}.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "jobasxml",
      description = "Returns a job as XML.",
      returnDescription = "The job as XML",
      pathParameters = {
          @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The job identifier")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Job found."),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "No job with that identifier exists.")
      })
  public JaxbJob getJobAsXml(@PathParam("id") long id) throws NotFoundException {
    return getJobAsJson(id);
  }

  @GET
  @Path("job/{id}.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "jobasjson",
      description = "Returns a job as JSON.",
      returnDescription = "The job as JSON",
      pathParameters = {
          @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The job identifier")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Job found."),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "No job with that identifier exists.")
      })
  public JaxbJob getJobAsJson(@PathParam("id") long id) throws NotFoundException {
    try {
      return new JaxbJob(serviceRegistry.getJob(id));
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("job/{id}/children.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "childrenjobsasxml",
      description = "Returns all children from a job as XML.",
      returnDescription = "A list of children jobs as XML",
      pathParameters = {
          @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The parent job identifier")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Jobs found.")
      })
  public JaxbJobList getChildrenJobsAsXml(@PathParam("id") long id) {
    return getChildrenJobsAsJson(id);
  }

  @GET
  @Path("job/{id}/children.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "childrenjobsasjson",
      description = "Returns all children from a job as JSON.",
      returnDescription = "A list of children jobs as JSON",
      pathParameters = {
          @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The parent job identifier")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Jobs found.")
      })
  public JaxbJobList getChildrenJobsAsJson(@PathParam("id") long id) {
    try {
      return new JaxbJobList(serviceRegistry.getChildJobs(id));
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("jobs.xml")
  @Produces(MediaType.TEXT_XML)
  public JaxbJobList getJobsAsXml(@QueryParam("serviceType") String serviceType,
      @QueryParam("status") Job.Status status) {
    try {
      return new JaxbJobList(serviceRegistry.getJobs(serviceType, status));
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }

  }

  @GET
  @Path("activeJobs.xml")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "activejobsasxml",
      description = "Returns all active jobs as XML.",
      returnDescription = "A list of active jobs as XML",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Active jobs found.")
      })
  public JaxbJobList getActiveJobsAsXml() {
    try {
      return new JaxbJobList(serviceRegistry.getActiveJobs());
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("activeJobs.json")
  @Produces(MediaType.APPLICATION_JSON)
  @RestQuery(
      name = "activejobsasjson",
      description = "Returns all active jobs as JSON.",
      returnDescription = "A list of active jobs as JSON",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Active jobs found.")
      })
  public JaxbJobList getActiveJobsAsJson() {
    try {
      return new JaxbJobList(serviceRegistry.getActiveJobs());
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("count")
  @Produces(MediaType.TEXT_PLAIN)
  @RestQuery(
      name = "count",
      description = "Returns the number of jobs matching the query parameters as plain text.",
      returnDescription = "The number of matching jobs",
      restParameters = {
          @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
              description = "The service type identifier"),
          @RestParameter(name = "status", isRequired = false, type = Type.STRING, description = "The job status"),
          @RestParameter(name = "host", isRequired = false, type = Type.STRING,
              description = "The host executing the job"),
          @RestParameter(name = "operation", isRequired = false, type = Type.STRING,
              description = "The job's operation")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Job count returned.")
      })
  public long count(@QueryParam("serviceType") String serviceType, @QueryParam("status") Job.Status status,
          @QueryParam("host") String host, @QueryParam("operation") String operation) {
    try {
      if (isNotBlank(host) && isNotBlank(operation)) {
        if (isBlank(serviceType)) {
          throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
        }
        return serviceRegistry.count(serviceType, host, operation, status);
      } else if (isNotBlank(host)) {
        if (isBlank(serviceType)) {
          throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
        }
        return serviceRegistry.countByHost(serviceType, host, status);
      } else if (isNotBlank(operation)) {
        if (isBlank(serviceType)) {
          throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
        }
        return serviceRegistry.countByOperation(serviceType, operation, status);
      } else {
        return serviceRegistry.count(serviceType, status);
      }
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("maxconcurrentjobs")
  @Produces(MediaType.TEXT_PLAIN)
  @RestQuery(
      name = "maxconcurrentjobs",
      description = "Returns the number of jobs that the servers in this service registry can execute concurrently. "
          + "If there is only one server in this service registry this will be the number of jobs that one server is "
          + "able to do at one time. If it is a distributed install across many servers then this number will be the "
          + "total number of jobs the cluster can process concurrently.",
      returnDescription = "The maximum number of concurrent jobs",
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Maximum number of concurrent jobs returned.")
      })
  @Deprecated
  public Response getMaximumConcurrentWorkflows() {
    return Response.status(Status.MOVED_PERMANENTLY).type(MediaType.TEXT_PLAIN)
            .header("Location", servicePath + "/maxload").build();
  }

  @GET
  @Path("maxload")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "maxload",
      description = "Returns the maximum load that servers in this service registry can execute concurrently.  "
          + "If there is only one server in this service registry this will be the maximum load that one server is "
          + "able to handle at one time. If it is a distributed install across many servers then this number will be "
          + "the maximum load the cluster can process concurrently.",
      returnDescription = "The maximum load of the cluster or server",
      restParameters = {
          @RestParameter(name = "host", isRequired = false, type = Type.STRING,
              description = "The host you want to know the maximum load for.")
      },
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Maximum load for the cluster.")
      })
  public Response getMaxLoadOnNode(@QueryParam("host") String host) throws NotFoundException {
    try {
      if (StringUtils.isEmpty(host)) {
        return Response.ok(serviceRegistry.getMaxLoads()).build();
      } else {
        SystemLoad systemLoad = new SystemLoad();
        systemLoad.addNodeLoad(serviceRegistry.getMaxLoadOnNode(host));
        return Response.ok(systemLoad).build();
      }
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("currentload")
  @Produces(MediaType.TEXT_XML)
  @RestQuery(
      name = "currentload",
      description = "Returns the current load on the servers in this service registry.  "
          + "If there is only one server in this service registry this will be the the load that one server.  "
          + "If it is a distributed install across many servers then this number will be a dictionary of the load on "
          + "all nodes in the cluster.",
      returnDescription = "The current load across the cluster",
      restParameters = {},
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Current load for the cluster.")
      })
  public Response getCurrentLoad() {
    try {
      return Response.ok(serviceRegistry.getCurrentHostLoads()).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

  @GET
  @Path("ownload")
  @Produces(MediaType.TEXT_PLAIN)
  @RestQuery(
      name = "ownload",
      description = "Returns the current load on this service registry's node.",
      returnDescription = "The current load across the cluster",
      restParameters = {},
      responses = {
          @RestResponse(responseCode = SC_OK, description = "Current load for the cluster.")
      })
  public Response getOwnLoad() {
    try {
      return Response.ok(serviceRegistry.getOwnLoad()).build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }


  @DELETE
  @Path("job/{id}")
  @RestQuery(
      name = "deletejob",
      description = "Deletes a job from the service registry",
      returnDescription = "No data is returned, just the HTTP status code",
      pathParameters = {
          @RestParameter(isRequired = true, name = "id", type = Type.INTEGER, description = "ID of the job to delete")
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "Job successfully deleted"),
          @RestResponse(responseCode = SC_NOT_FOUND, description = "Job with given id could not be found")
      })
  public Response deleteJob(@PathParam("id") long id) throws NotFoundException {
    try {
      serviceRegistry.removeJobs(Collections.singletonList(id));
      return Response.noContent().build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }


  @POST
  @Path("removejobs")
  @RestQuery(
      name = "removejobs",
      description = "Removes all given jobs and their child jobs",
      returnDescription = "No data is returned, just the HTTP status code",
      restParameters = {
          @RestParameter(name = "jobIds", isRequired = true, description = "The IDs of the jobs to delete",
              type = Type.TEXT),
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "Jobs successfully removed"),
          @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
              description = "Error while removing jobs")
      })
  public Response removeParentlessJobs(@FormParam("jobIds") String jobIds) throws NotFoundException {
    try {
      final JSONArray array = (JSONArray) JSONValue.parse(jobIds);
      final List<Long> jobIdList = Arrays.asList((Long[]) array.toArray(new Long[0]));
      serviceRegistry.removeJobs(jobIdList);
      return Response.noContent().build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }


  @POST
  @Path("removeparentlessjobs")
  @RestQuery(
      name = "removeparentlessjobs",
      description = "Removes all jobs without a parent job which have passed their lifetime",
      returnDescription = "No data is returned, just the HTTP status code",
      restParameters = {
          @RestParameter(name = "lifetime", isRequired = true, type = Type.INTEGER,
              description = "Lifetime of parentless jobs")
      },
      responses = {
          @RestResponse(responseCode = SC_NO_CONTENT, description = "Parentless jobs successfully removed"),
          @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
              description = "Error while removing parentless jobs")
      })
  public Response removeParentlessJobs(@FormParam("lifetime") int lifetime) {
    try {
      serviceRegistry.removeParentlessJobs(lifetime);
      return Response.noContent().build();
    } catch (ServiceRegistryException e) {
      throw new WebApplicationException(e);
    }
  }

}