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  
22  package org.opencastproject.serviceregistry.impl.endpoint;
23  
24  import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25  import static javax.servlet.http.HttpServletResponse.SC_CREATED;
26  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
27  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
28  import static javax.servlet.http.HttpServletResponse.SC_OK;
29  import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
30  import static org.apache.commons.lang3.StringUtils.isBlank;
31  import static org.apache.commons.lang3.StringUtils.isNotBlank;
32  
33  import org.opencastproject.job.api.JaxbJob;
34  import org.opencastproject.job.api.JaxbJobList;
35  import org.opencastproject.job.api.Job;
36  import org.opencastproject.job.api.JobParser;
37  import org.opencastproject.rest.RestConstants;
38  import org.opencastproject.security.api.SecurityService;
39  import org.opencastproject.serviceregistry.api.HostRegistration;
40  import org.opencastproject.serviceregistry.api.JaxbHostRegistration;
41  import org.opencastproject.serviceregistry.api.JaxbHostRegistrationList;
42  import org.opencastproject.serviceregistry.api.JaxbServiceHealth;
43  import org.opencastproject.serviceregistry.api.JaxbServiceRegistration;
44  import org.opencastproject.serviceregistry.api.JaxbServiceRegistrationList;
45  import org.opencastproject.serviceregistry.api.JaxbServiceStatisticsList;
46  import org.opencastproject.serviceregistry.api.ServiceRegistration;
47  import org.opencastproject.serviceregistry.api.ServiceRegistry;
48  import org.opencastproject.serviceregistry.api.ServiceRegistryException;
49  import org.opencastproject.serviceregistry.api.ServiceState;
50  import org.opencastproject.serviceregistry.api.SystemLoad;
51  import org.opencastproject.serviceregistry.impl.ServiceRegistryJpaImpl;
52  import org.opencastproject.systems.OpencastConstants;
53  import org.opencastproject.util.NotFoundException;
54  import org.opencastproject.util.UrlSupport;
55  import org.opencastproject.util.doc.rest.RestParameter;
56  import org.opencastproject.util.doc.rest.RestParameter.Type;
57  import org.opencastproject.util.doc.rest.RestQuery;
58  import org.opencastproject.util.doc.rest.RestResponse;
59  import org.opencastproject.util.doc.rest.RestService;
60  
61  import org.apache.commons.lang3.StringUtils;
62  import org.json.simple.JSONArray;
63  import org.json.simple.JSONValue;
64  import org.osgi.service.component.ComponentContext;
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  
70  import java.net.MalformedURLException;
71  import java.net.URL;
72  import java.util.Arrays;
73  import java.util.Collections;
74  import java.util.LinkedList;
75  import java.util.List;
76  import java.util.Map;
77  
78  import javax.servlet.http.HttpServletRequest;
79  import javax.servlet.http.HttpServletResponse;
80  import javax.ws.rs.DELETE;
81  import javax.ws.rs.FormParam;
82  import javax.ws.rs.GET;
83  import javax.ws.rs.POST;
84  import javax.ws.rs.PUT;
85  import javax.ws.rs.Path;
86  import javax.ws.rs.PathParam;
87  import javax.ws.rs.Produces;
88  import javax.ws.rs.QueryParam;
89  import javax.ws.rs.WebApplicationException;
90  import javax.ws.rs.core.Context;
91  import javax.ws.rs.core.MediaType;
92  import javax.ws.rs.core.Response;
93  import javax.ws.rs.core.Response.Status;
94  
95  /**
96   * Displays hosts and the service IDs they provide.
97   */
98  @Path("/services")
99  @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.")
100 @Component(
101   property = {
102     "service.description=Service Registry REST Endpoint",
103     "opencast.service.type=org.opencastproject.serviceregistry",
104     "opencast.service.path=/services"
105   },
106   immediate = true,
107   service = { ServiceRegistryEndpoint.class }
108 )
109 @JaxrsResource
110 public class ServiceRegistryEndpoint {
111 
112   /** The remote service maanger */
113   protected ServiceRegistry serviceRegistry = null;
114 
115   private SecurityService securityService = null;
116 
117   /** This server's URL */
118   protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
119 
120   /** The service path for this endpoint */
121   protected String servicePath = "/";
122 
123   /** Sets the service registry instance for delegation */
124   @Reference
125   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
126     this.serviceRegistry = serviceRegistry;
127   }
128 
129   @Reference
130   public void setSecurityService(SecurityService securityService) {
131     this.securityService = securityService;
132   }
133 
134   /**
135    * Callback from OSGi that is called when this service is activated.
136    *
137    * @param cc
138    *          OSGi component context
139    */
140   @Activate
141   public void activate(ComponentContext cc) {
142     serverUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
143     servicePath = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
144   }
145 
146   @GET
147   @Path("statistics.json")
148   @Produces(MediaType.APPLICATION_JSON)
149   @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") })
150   public Response getStatisticsAsJson() {
151     try {
152       return Response.ok(new JaxbServiceStatisticsList(serviceRegistry.getServiceStatistics())).build();
153     } catch (ServiceRegistryException e) {
154       throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
155     }
156   }
157 
158   @GET
159   @Path("statistics.xml")
160   @Produces(MediaType.TEXT_XML)
161   @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") })
162   public Response getStatisticsAsXml() throws ServiceRegistryException {
163     return getStatisticsAsJson();
164   }
165 
166   @POST
167   @Path("sanitize")
168   @RestQuery(name = "sanitize", description = "Sets the given service to NORMAL state", returnDescription = "No content", restParameters = {
169           @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier", type = Type.STRING, defaultValue = ""),
170           @RestParameter(name = "host", isRequired = true, description = "The host providing the service, including the http(s) protocol", type = Type.STRING, defaultValue = "") }, responses = {
171           @RestResponse(responseCode = SC_NO_CONTENT, description = "The service was successfully sanitized"),
172           @RestResponse(responseCode = SC_NOT_FOUND, description = "No service of that type on that host is registered.") })
173   public Response sanitize(@FormParam("serviceType") String serviceType, @FormParam("host") String host)
174           throws NotFoundException {
175     serviceRegistry.sanitize(serviceType, host);
176     return Response.status(Status.NO_CONTENT).build();
177   }
178 
179   @POST
180   @Path("register")
181   @Produces(MediaType.TEXT_XML)
182   @RestQuery(name = "register", description = "Add a new service registration to the cluster.", returnDescription = "The service registration.", restParameters = {
183           @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier", type = Type.STRING, defaultValue = ""),
184           @RestParameter(name = "host", isRequired = true, description = "The host providing the service, including the http(s) protocol", type = Type.STRING, defaultValue = ""),
185           @RestParameter(name = "path", isRequired = true, description = "The service path on the host", type = Type.STRING, defaultValue = ""),
186           @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") })
187   public JaxbServiceRegistration register(@FormParam("serviceType") String serviceType, @FormParam("host") String host,
188           @FormParam("path") String path, @FormParam("jobProducer") boolean jobProducer) {
189     try {
190       return new JaxbServiceRegistration(serviceRegistry.registerService(serviceType, host, path, jobProducer));
191     } catch (ServiceRegistryException e) {
192       throw new WebApplicationException(e);
193     }
194   }
195 
196   @POST
197   @Path("unregister")
198   @RestQuery(name = "unregister", description = "Removes a service registration.", returnDescription = "No content", restParameters = {
199           @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier", type = Type.STRING),
200           @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") })
201   public Response unregister(@FormParam("serviceType") String serviceType, @FormParam("host") String host) {
202     try {
203       serviceRegistry.unRegisterService(serviceType, host);
204       return Response.status(Status.NO_CONTENT).build();
205     } catch (ServiceRegistryException e) {
206       throw new WebApplicationException(e);
207     }
208   }
209 
210   @POST
211   @Path("enablehost")
212   @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 = {
213           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was enabled successfully"),
214           @RestResponse(responseCode = SC_NOT_FOUND, description = "The host does not exist") })
215   public Response enableHost(@FormParam("host") String host) throws NotFoundException {
216     try {
217       serviceRegistry.enableHost(host);
218       return Response.status(Status.NO_CONTENT).build();
219     } catch (ServiceRegistryException e) {
220       throw new WebApplicationException(e);
221     }
222   }
223 
224   @POST
225   @Path("disablehost")
226   @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 = {
227           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was disabled successfully"),
228           @RestResponse(responseCode = SC_NOT_FOUND, description = "The host does not exist") })
229   public Response disableHost(@FormParam("host") String host) throws NotFoundException {
230     try {
231       serviceRegistry.disableHost(host);
232       return Response.status(Status.NO_CONTENT).build();
233     } catch (ServiceRegistryException e) {
234       throw new WebApplicationException(e);
235     }
236   }
237 
238   @POST
239   @Path("registerhost")
240   @RestQuery(name = "registerhost", description = "Add a new server to the cluster.", returnDescription = "No content.", restParameters = {
241           @RestParameter(name = "host", isRequired = true, description = "The host name, including the http(s) protocol", type = Type.STRING),
242           @RestParameter(name = "address", isRequired = true, description = "The IP address", type = Type.STRING),
243           @RestParameter(name = "nodeName", isRequired = true, description = "Descriptive node name", type = Type.STRING),
244           @RestParameter(name = "memory", isRequired = true, description = "The allocated memory", type = Type.STRING),
245           @RestParameter(name = "cores", isRequired = true, description = "The available cores", type = Type.STRING),
246           @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") })
247   public void register(@FormParam("host") String host, @FormParam("address") String address, @FormParam("nodeName") String nodeName,
248           @FormParam("memory") long memory, @FormParam("cores") int cores, @FormParam("maxLoad") float maxLoad) {
249     try {
250       serviceRegistry.registerHost(host, address, nodeName, memory, cores, maxLoad);
251     } catch (ServiceRegistryException e) {
252       throw new WebApplicationException(e);
253     }
254   }
255 
256   @POST
257   @Path("unregisterhost")
258   @RestQuery(name = "unregisterhost", description = "Removes a server from the cluster.",
259           returnDescription = "No content.",
260           restParameters = {
261           @RestParameter(name = "host", isRequired = true, description = "The host name, including the http(s) protocol", type = Type.STRING)
262           }, responses = {
263           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was removed successfully") })
264   public Response unregister(@FormParam("host") String host) {
265     try {
266       serviceRegistry.unregisterHost(host);
267       return Response.status(Status.NO_CONTENT).build();
268     } catch (ServiceRegistryException e) {
269       if (e.getCause() instanceof IllegalArgumentException) {
270         return Response.status(Status.NOT_FOUND).entity(e.getMessage()).build();
271       }
272       throw new WebApplicationException(e);
273     }
274   }
275 
276   @POST
277   @Path("maintenance")
278   @RestQuery(name = "maintenance", description = "Sets the maintenance status for a server in the cluster.", returnDescription = "No content.", restParameters = {
279           @RestParameter(name = "host", isRequired = true, type = Type.STRING, description = "The host name, including the http(s) protocol"),
280           @RestParameter(name = "maintenance", isRequired = true, type = Type.BOOLEAN, description = "Whether this host should be put into maintenance mode (true) or not") }, responses = {
281           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was registered successfully"),
282           @RestResponse(responseCode = SC_NOT_FOUND, description = "Host not found") })
283   public Response setMaintenanceMode(@FormParam("host") String host, @FormParam("maintenance") boolean maintenance)
284           throws NotFoundException {
285     try {
286       serviceRegistry.setMaintenanceStatus(host, maintenance);
287       return Response.status(Status.NO_CONTENT).build();
288     } catch (ServiceRegistryException e) {
289       throw new WebApplicationException(e);
290     }
291   }
292 
293   @GET
294   @Path("available.xml")
295   @Produces(MediaType.TEXT_XML)
296   @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 = {
297           @RestResponse(responseCode = SC_OK, description = "Returned the available services."),
298           @RestResponse(responseCode = SC_BAD_REQUEST, description = "No service type specified, bad request.") })
299   public Response getAvailableServicesAsXml(@QueryParam("serviceType") String serviceType) {
300 
301     if (isBlank(serviceType))
302       throw new WebApplicationException(Response.status(Status.BAD_REQUEST).entity("Service type must be specified")
303               .build());
304 
305     Map<String, String> properties = securityService.getOrganization().getProperties();
306 
307     JaxbServiceRegistrationList registrations = new JaxbServiceRegistrationList();
308     try {
309       for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByLoad(serviceType)) {
310         JaxbServiceRegistration jaxbReg = new JaxbServiceRegistration(reg);
311         URL internalHostUrl = new URL(jaxbReg.getHost());
312         String tenantSpecificHost = StringUtils.trimToNull(properties.get("org.opencastproject.host." + internalHostUrl.getHost()));
313         if (StringUtils.isNotBlank(tenantSpecificHost)) {
314           jaxbReg.setHost(tenantSpecificHost);
315         }
316         registrations.add(jaxbReg);
317       }
318       return Response.ok(registrations).build();
319     } catch (ServiceRegistryException | MalformedURLException e) {
320       throw new WebApplicationException(e);
321     }
322   }
323 
324   @GET
325   @Path("available.json")
326   @Produces(MediaType.APPLICATION_JSON)
327   @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 = {
328           @RestResponse(responseCode = SC_OK, description = "Returned the available services."),
329           @RestResponse(responseCode = SC_BAD_REQUEST, description = "No service type specified, bad request.") })
330   public Response getAvailableServicesAsJson(@QueryParam("serviceType") String serviceType) {
331     return getAvailableServicesAsXml(serviceType);
332   }
333 
334   @GET
335   @Path("health.json")
336   @Produces(MediaType.APPLICATION_JSON)
337   @RestQuery(name = "health", description = "Checks the status of the registered services", returnDescription = "Returns NO_CONTENT if services are in a proper state", restParameters = {
338           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING, description = "The service type identifier"),
339           @RestParameter(name = "host", isRequired = false, type = Type.STRING, description = "The host, including the http(s) protocol") }, responses = {
340           @RestResponse(responseCode = SC_OK, description = "Service states returned"),
341           @RestResponse(responseCode = SC_NOT_FOUND, description = "No service of that type on that host is registered."),
342           @RestResponse(responseCode = SC_SERVICE_UNAVAILABLE, description = "An error has occurred during stats processing") })
343   public Response getHealthStatusAsJson(@QueryParam("serviceType") String serviceType, @QueryParam("host") String host)
344           throws NotFoundException {
345     return getHealthStatus(serviceType, host);
346   }
347 
348   @GET
349   @Path("health.xml")
350   @Produces(MediaType.APPLICATION_XML)
351   @RestQuery(name = "health", description = "Checks the status of the registered services", returnDescription = "Returns NO_CONTENT if services are in a proper state", restParameters = {
352           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING, description = "The service type identifier"),
353           @RestParameter(name = "host", isRequired = false, type = Type.STRING, description = "The host, including the http(s) protocol") }, responses = {
354           @RestResponse(responseCode = SC_OK, description = "Service states returned"),
355           @RestResponse(responseCode = SC_NOT_FOUND, description = "No service of that type on that host is registered."),
356           @RestResponse(responseCode = SC_SERVICE_UNAVAILABLE, description = "An error has occurred during stats processing") })
357   public Response getHealthStatus(@QueryParam("serviceType") String serviceType, @QueryParam("host") String host)
358           throws NotFoundException {
359     try {
360       List<ServiceRegistration> services = null;
361       if (isNotBlank(serviceType) && isNotBlank(host)) {
362         // This is a request for one specific service. Return it, or SC_NOT_FOUND if not found
363         ServiceRegistration reg = serviceRegistry.getServiceRegistration(serviceType, host);
364         if (reg == null)
365           throw new NotFoundException();
366 
367         services = new LinkedList<ServiceRegistration>();
368         services.add(reg);
369       } else if (isBlank(serviceType) && isBlank(host)) {
370         // This is a request for all service registrations
371         services = serviceRegistry.getServiceRegistrations();
372       } else if (isNotBlank(serviceType)) {
373         // This is a request for all service registrations of a particular type
374         services = serviceRegistry.getServiceRegistrationsByType(serviceType);
375       } else if (isNotBlank(host)) {
376         // This is a request for all service registrations of a particular host
377         services = serviceRegistry.getServiceRegistrationsByHost(host);
378       }
379 
380       int healthy = 0;
381       int warning = 0;
382       int error = 0;
383       for (ServiceRegistration reg : services) {
384         if (ServiceState.NORMAL == reg.getServiceState()) {
385           healthy++;
386         } else if (ServiceState.WARNING == reg.getServiceState()) {
387           warning++;
388         } else if (ServiceState.ERROR == reg.getServiceState()) {
389           error++;
390         } else {
391            error++;
392         }
393       }
394       JaxbServiceHealth stats = new JaxbServiceHealth(healthy, warning, error);
395       return Response.ok(stats).build();
396     } catch (ServiceRegistryException e) {
397       throw new WebApplicationException(e);
398     }
399   }
400 
401   @GET
402   @Path("services.xml")
403   @Produces(MediaType.TEXT_XML)
404   @RestQuery(name = "servicesasxml", description = "Returns a service registraton or list of available service registrations as XML.", returnDescription = "The services list as XML", restParameters = {
405           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING, description = "The service type identifier"),
406           @RestParameter(name = "host", isRequired = false, type = Type.STRING, description = "The host, including the http(s) protocol") }, responses = {
407           @RestResponse(responseCode = SC_OK, description = "Returned the available service."),
408           @RestResponse(responseCode = SC_NOT_FOUND, description = "No service of that type on that host is registered.") })
409   public JaxbServiceRegistrationList getRegistrationsAsXml(@QueryParam("serviceType") String serviceType,
410           @QueryParam("host") String host) throws NotFoundException {
411     JaxbServiceRegistrationList registrations = new JaxbServiceRegistrationList();
412     try {
413       if (isNotBlank(serviceType) && isNotBlank(host)) {
414         // This is a request for one specific service. Return it, or SC_NOT_FOUND if not found
415         ServiceRegistration reg = serviceRegistry.getServiceRegistration(serviceType, host);
416         if (reg == null) {
417           throw new NotFoundException();
418         } else {
419           return new JaxbServiceRegistrationList(new JaxbServiceRegistration(reg));
420         }
421       } else if (isBlank(serviceType) && isBlank(host)) {
422         // This is a request for all service registrations
423         for (ServiceRegistration reg : serviceRegistry.getServiceRegistrations()) {
424           registrations.add(new JaxbServiceRegistration(reg));
425         }
426       } else if (isNotBlank(serviceType)) {
427         // This is a request for all service registrations of a particular type
428         for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByType(serviceType)) {
429           registrations.add(new JaxbServiceRegistration(reg));
430         }
431       } else if (isNotBlank(host)) {
432         // This is a request for all service registrations of a particular host
433         for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByHost(host)) {
434           registrations.add(new JaxbServiceRegistration(reg));
435         }
436       }
437       return registrations;
438     } catch (ServiceRegistryException e) {
439       throw new WebApplicationException(e);
440     }
441   }
442 
443   @GET
444   @Path("services.json")
445   @Produces(MediaType.APPLICATION_JSON)
446   @RestQuery(name = "servicesasjson", description = "Returns a service registraton or list of available service registrations as JSON.", returnDescription = "The services list as XML", restParameters = {
447           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING, description = "The service type identifier"),
448           @RestParameter(name = "host", isRequired = false, type = Type.STRING, description = "The host, including the http(s) protocol") }, responses = {
449           @RestResponse(responseCode = SC_OK, description = "Returned the available service."),
450           @RestResponse(responseCode = SC_NOT_FOUND, description = "No service of that type on that host is registered.") })
451   public JaxbServiceRegistrationList getRegistrationsAsJson(@QueryParam("serviceType") String serviceType,
452           @QueryParam("host") String host) throws NotFoundException {
453     return getRegistrationsAsXml(serviceType, host);
454   }
455 
456   @GET
457   @Path("hosts.xml")
458   @Produces(MediaType.TEXT_XML)
459   @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.") })
460   public JaxbHostRegistrationList getHostsAsXml() throws NotFoundException {
461     JaxbHostRegistrationList registrations = new JaxbHostRegistrationList();
462     try {
463       for (HostRegistration reg : serviceRegistry.getHostRegistrations())
464         registrations.add(new JaxbHostRegistration(reg));
465       return registrations;
466     } catch (ServiceRegistryException e) {
467       throw new WebApplicationException(e);
468     }
469   }
470 
471   @GET
472   @Path("hosts.json")
473   @Produces(MediaType.APPLICATION_JSON)
474   @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.") })
475   public JaxbHostRegistrationList getHostsAsJson() throws NotFoundException {
476     return getHostsAsXml();
477   }
478 
479   @POST
480   @Path("job")
481   @Produces(MediaType.TEXT_XML)
482   @RestQuery(name = "createjob", description = "Creates a new job.", returnDescription = "An XML representation of the job.", restParameters = {
483           @RestParameter(name = "jobType", isRequired = true, type = Type.STRING, description = "The job type identifier"),
484           @RestParameter(name = "host", isRequired = true, type = Type.STRING, description = "The creating host, including the http(s) protocol"),
485           @RestParameter(name = "operation", isRequired = true, type = Type.STRING, description = "The operation this job should execute"),
486           @RestParameter(name = "payload", isRequired = false, type = Type.TEXT, description = "The job type identifier"),
487           @RestParameter(name = "start", isRequired = false, type = Type.BOOLEAN, description = "Whether the job should be queued for dispatch and execution"),
488           @RestParameter(name = "jobLoad", isRequired = false, type = Type.STRING, description = "The load this job will incur on the system"),
489           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT, description = "An argument for the operation"),
490           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT, description = "An argument for the operation"),
491           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT, description = "An argument for the operation"),
492           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT, description = "An argument for the operation"),
493           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT, description = "An argument for the operation") }, responses = {
494           @RestResponse(responseCode = SC_CREATED, description = "Job created."),
495           @RestResponse(responseCode = SC_BAD_REQUEST, description = "The required parameters were not supplied, bad request.") })
496   public Response createJob(@Context HttpServletRequest request) {
497     String[] argArray = request.getParameterValues("arg");
498     List<String> arguments = null;
499     if (argArray != null && argArray.length > 0) {
500       arguments = Arrays.asList(argArray);
501     }
502     String jobType = request.getParameter("jobType");
503     String operation = request.getParameter("operation");
504     String host = request.getParameter("host");
505     String payload = request.getParameter("payload");
506     boolean start = StringUtils.isBlank(request.getParameter("start"))
507             || Boolean.TRUE.toString().equalsIgnoreCase(request.getParameter("start"));
508     try {
509       Job job = null;
510       if (StringUtils.isNotBlank(request.getParameter("jobLoad"))) {
511         Float jobLoad = Float.parseFloat(request.getParameter("jobLoad"));
512         job = ((ServiceRegistryJpaImpl) serviceRegistry).createJob(host, jobType, operation, arguments, payload,
513               start, serviceRegistry.getCurrentJob(), jobLoad);
514       } else {
515         job = ((ServiceRegistryJpaImpl) serviceRegistry).createJob(host, jobType, operation, arguments, payload,
516                 start, serviceRegistry.getCurrentJob());
517       }
518       return Response.created(job.getUri()).entity(new JaxbJob(job)).build();
519     } catch (IllegalArgumentException e) {
520       throw new WebApplicationException(Status.BAD_REQUEST);
521     } catch (Exception e) {
522       throw new WebApplicationException(e);
523     }
524   }
525 
526   @PUT
527   @Path("job/{id}.xml")
528   @Produces(MediaType.TEXT_XML)
529   @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 = {
530           @RestResponse(responseCode = SC_NO_CONTENT, description = "Job updated."),
531           @RestResponse(responseCode = SC_NOT_FOUND, description = "Job not found.") })
532   public Response updateJob(@PathParam("id") String id, @FormParam("job") String jobXml) throws NotFoundException {
533     try {
534       Job job = JobParser.parseJob(jobXml);
535       serviceRegistry.updateJob(job);
536       return Response.status(Status.NO_CONTENT).build();
537     } catch (Exception e) {
538       throw new WebApplicationException(e);
539     }
540   }
541 
542   @GET
543   @Path("job/{id}.xml")
544   @Produces(MediaType.TEXT_XML)
545   @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 = {
546           @RestResponse(responseCode = SC_OK, description = "Job found."),
547           @RestResponse(responseCode = SC_NOT_FOUND, description = "No job with that identifier exists.") })
548   public JaxbJob getJobAsXml(@PathParam("id") long id) throws NotFoundException {
549     return getJobAsJson(id);
550   }
551 
552   @GET
553   @Path("job/{id}.json")
554   @Produces(MediaType.APPLICATION_JSON)
555   @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 = {
556           @RestResponse(responseCode = SC_OK, description = "Job found."),
557           @RestResponse(responseCode = SC_NOT_FOUND, description = "No job with that identifier exists.") })
558   public JaxbJob getJobAsJson(@PathParam("id") long id) throws NotFoundException {
559     try {
560       return new JaxbJob(serviceRegistry.getJob(id));
561     } catch (ServiceRegistryException e) {
562       throw new WebApplicationException(e);
563     }
564   }
565 
566   @GET
567   @Path("job/{id}/children.xml")
568   @Produces(MediaType.TEXT_XML)
569   @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.") })
570   public JaxbJobList getChildrenJobsAsXml(@PathParam("id") long id) {
571     return getChildrenJobsAsJson(id);
572   }
573 
574   @GET
575   @Path("job/{id}/children.json")
576   @Produces(MediaType.APPLICATION_JSON)
577   @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.") })
578   public JaxbJobList getChildrenJobsAsJson(@PathParam("id") long id) {
579     try {
580       return new JaxbJobList(serviceRegistry.getChildJobs(id));
581     } catch (ServiceRegistryException e) {
582       throw new WebApplicationException(e);
583     }
584   }
585 
586   @GET
587   @Path("jobs.xml")
588   @Produces(MediaType.TEXT_XML)
589   public JaxbJobList getJobsAsXml(@QueryParam("serviceType") String serviceType, @QueryParam("status") Job.Status status) {
590     try {
591       return new JaxbJobList(serviceRegistry.getJobs(serviceType, status));
592     } catch (ServiceRegistryException e) {
593       throw new WebApplicationException(e);
594     }
595 
596   }
597 
598   @GET
599   @Path("activeJobs.xml")
600   @Produces(MediaType.TEXT_XML)
601   @RestQuery(name = "activejobsasxml",
602           description = "Returns all active jobs as XML.",
603           returnDescription = "A list of active jobs as XML",
604           responses = { @RestResponse(responseCode = SC_OK, description = "Active jobs found.") })
605   public JaxbJobList getActiveJobsAsXml() {
606     try {
607       return new JaxbJobList(serviceRegistry.getActiveJobs());
608     } catch (ServiceRegistryException e) {
609       throw new WebApplicationException(e);
610     }
611   }
612 
613   @GET
614   @Path("activeJobs.json")
615   @Produces(MediaType.APPLICATION_JSON)
616   @RestQuery(name = "activejobsasjson",
617           description = "Returns all active jobs as JSON.",
618           returnDescription = "A list of active jobs as JSON",
619           responses = { @RestResponse(responseCode = SC_OK, description = "Active jobs found.") })
620   public JaxbJobList getActiveJobsAsJson() {
621     try {
622       return new JaxbJobList(serviceRegistry.getActiveJobs());
623     } catch (ServiceRegistryException e) {
624       throw new WebApplicationException(e);
625     }
626   }
627 
628   @GET
629   @Path("count")
630   @Produces(MediaType.TEXT_PLAIN)
631   @RestQuery(name = "count", description = "Returns the number of jobs matching the query parameters as plain text.", returnDescription = "The number of matching jobs", restParameters = {
632           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING, description = "The service type identifier"),
633           @RestParameter(name = "status", isRequired = false, type = Type.STRING, description = "The job status"),
634           @RestParameter(name = "host", isRequired = false, type = Type.STRING, description = "The host executing the job"),
635           @RestParameter(name = "operation", isRequired = false, type = Type.STRING, description = "The job's operation") }, responses = { @RestResponse(responseCode = SC_OK, description = "Job count returned.") })
636   public long count(@QueryParam("serviceType") String serviceType, @QueryParam("status") Job.Status status,
637           @QueryParam("host") String host, @QueryParam("operation") String operation) {
638     try {
639       if (isNotBlank(host) && isNotBlank(operation)) {
640         if (isBlank(serviceType))
641           throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
642         return serviceRegistry.count(serviceType, host, operation, status);
643       } else if (isNotBlank(host)) {
644         if (isBlank(serviceType))
645           throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
646         return serviceRegistry.countByHost(serviceType, host, status);
647       } else if (isNotBlank(operation)) {
648         if (isBlank(serviceType))
649           throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
650         return serviceRegistry.countByOperation(serviceType, operation, status);
651       } else {
652         return serviceRegistry.count(serviceType, status);
653       }
654     } catch (ServiceRegistryException e) {
655       throw new WebApplicationException(e);
656     }
657   }
658 
659   @GET
660   @Path("maxconcurrentjobs")
661   @Produces(MediaType.TEXT_PLAIN)
662   @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.") })
663   @Deprecated
664   public Response getMaximumConcurrentWorkflows() {
665     return Response.status(Status.MOVED_PERMANENTLY).type(MediaType.TEXT_PLAIN)
666             .header("Location", servicePath + "/maxload").build();
667   }
668 
669   @GET
670   @Path("maxload")
671   @Produces(MediaType.TEXT_XML)
672   @RestQuery(name = "maxload", description = "Returns the maximum load that servers in this service registry can execute concurrently.  "
673           + "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.  "
674           + "If it is a distributed install across many servers then this number will be the maximum load the cluster can process concurrently.",
675           returnDescription = "The maximum load of the cluster or server", restParameters = {
676               @RestParameter(name = "host", isRequired = false, type = Type.STRING, description = "The host you want to know the maximum load for.")
677           }, responses = { @RestResponse(responseCode = SC_OK, description = "Maximum load for the cluster.") })
678   public Response getMaxLoadOnNode(@QueryParam("host") String host) throws NotFoundException {
679     try {
680       if (StringUtils.isEmpty(host)) {
681         return Response.ok(serviceRegistry.getMaxLoads()).build();
682       } else {
683         SystemLoad systemLoad = new SystemLoad();
684         systemLoad.addNodeLoad(serviceRegistry.getMaxLoadOnNode(host));
685         return Response.ok(systemLoad).build();
686       }
687     } catch (ServiceRegistryException e) {
688       throw new WebApplicationException(e);
689     }
690   }
691 
692   @GET
693   @Path("currentload")
694   @Produces(MediaType.TEXT_XML)
695   @RestQuery(name = "currentload", description = "Returns the current load on the servers in this service registry.  "
696           + "If there is only one server in this service registry this will be the the load that one server.  "
697           + "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.",
698           returnDescription = "The current load across the cluster", restParameters = {},
699           responses = { @RestResponse(responseCode = SC_OK, description = "Current load for the cluster.") })
700   public Response getCurrentLoad() {
701     try {
702       return Response.ok(serviceRegistry.getCurrentHostLoads()).build();
703     } catch (ServiceRegistryException e) {
704       throw new WebApplicationException(e);
705     }
706   }
707 
708   @GET
709   @Path("ownload")
710   @Produces(MediaType.TEXT_PLAIN)
711   @RestQuery(name = "ownload", description = "Returns the current load on this service registry's node.",
712           returnDescription = "The current load across the cluster", restParameters = {},
713           responses = { @RestResponse(responseCode = SC_OK, description = "Current load for the cluster.") })
714   public Response getOwnLoad() {
715     try {
716       return Response.ok(serviceRegistry.getOwnLoad()).build();
717     } catch (ServiceRegistryException e) {
718       throw new WebApplicationException(e);
719     }
720   }
721 
722 
723   @DELETE
724   @Path("job/{id}")
725   @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 = {
726           @RestResponse(responseCode = SC_NO_CONTENT, description = "Job successfully deleted"),
727           @RestResponse(responseCode = SC_NOT_FOUND, description = "Job with given id could not be found") })
728   public Response deleteJob(@PathParam("id") long id) throws NotFoundException {
729     try {
730       serviceRegistry.removeJobs(Collections.singletonList(id));
731       return Response.noContent().build();
732     } catch (ServiceRegistryException e) {
733       throw new WebApplicationException(e);
734     }
735   }
736 
737 
738   @POST
739   @Path("removejobs")
740   @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 = {
741           @RestResponse(responseCode = SC_NO_CONTENT, description = "Jobs successfully removed"),
742           @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR, description = "Error while removing jobs") })
743   public Response removeParentlessJobs(@FormParam("jobIds") String jobIds) throws NotFoundException {
744     try {
745       final JSONArray array = (JSONArray) JSONValue.parse(jobIds);
746       final List<Long> jobIdList = Arrays.asList((Long[]) array.toArray(new Long[0]));
747       serviceRegistry.removeJobs(jobIdList);
748       return Response.noContent().build();
749     } catch (ServiceRegistryException e) {
750       throw new WebApplicationException(e);
751     }
752   }
753 
754 
755   @POST
756   @Path("removeparentlessjobs")
757   @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 = {
758           @RestResponse(responseCode = SC_NO_CONTENT, description = "Parentless jobs successfully removed"),
759           @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR, description = "Error while removing parentless jobs") })
760   public Response removeParentlessJobs(@FormParam("lifetime") int lifetime) {
761     try {
762       serviceRegistry.removeParentlessJobs(lifetime);
763       return Response.noContent().build();
764     } catch (ServiceRegistryException e) {
765       throw new WebApplicationException(e);
766     }
767   }
768 
769 }