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(
100     name = "serviceregistry",
101     title = "Service Registry",
102     notes = { "All paths above are relative to the REST endpoint base" },
103     abstractText = "Provides registration and management functions for servers and services in this Opencast instance "
104         + "or cluster.")
105 @Component(
106   property = {
107     "service.description=Service Registry REST Endpoint",
108     "opencast.service.type=org.opencastproject.serviceregistry",
109     "opencast.service.path=/services"
110   },
111   immediate = true,
112   service = { ServiceRegistryEndpoint.class }
113 )
114 @JaxrsResource
115 public class ServiceRegistryEndpoint {
116 
117   /** The remote service maanger */
118   protected ServiceRegistry serviceRegistry = null;
119 
120   private SecurityService securityService = null;
121 
122   /** This server's URL */
123   protected String serverUrl = UrlSupport.DEFAULT_BASE_URL;
124 
125   /** The service path for this endpoint */
126   protected String servicePath = "/";
127 
128   /** Sets the service registry instance for delegation */
129   @Reference
130   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
131     this.serviceRegistry = serviceRegistry;
132   }
133 
134   @Reference
135   public void setSecurityService(SecurityService securityService) {
136     this.securityService = securityService;
137   }
138 
139   /**
140    * Callback from OSGi that is called when this service is activated.
141    *
142    * @param cc
143    *          OSGi component context
144    */
145   @Activate
146   public void activate(ComponentContext cc) {
147     serverUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
148     servicePath = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
149   }
150 
151   @GET
152   @Path("statistics.json")
153   @Produces(MediaType.APPLICATION_JSON)
154   @RestQuery(
155       name = "statisticsasjson",
156       description = "List the service registrations in the cluster, along with some simple statistics",
157       returnDescription = "The service statistics.",
158       responses = {
159           @RestResponse(responseCode = SC_OK, description = "A JSON representation of the service statistics")
160       })
161   public Response getStatisticsAsJson() {
162     try {
163       return Response.ok(new JaxbServiceStatisticsList(serviceRegistry.getServiceStatistics())).build();
164     } catch (ServiceRegistryException e) {
165       throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
166     }
167   }
168 
169   @GET
170   @Path("statistics.xml")
171   @Produces(MediaType.TEXT_XML)
172   @RestQuery(
173       name = "statisticsasxml",
174       description = "List the service registrations in the cluster, along with some simple statistics",
175       returnDescription = "The service statistics.",
176       responses = {
177           @RestResponse(responseCode = SC_OK, description = "An XML representation of the service statistics")
178       })
179   public Response getStatisticsAsXml() throws ServiceRegistryException {
180     return getStatisticsAsJson();
181   }
182 
183   @POST
184   @Path("sanitize")
185   @RestQuery(
186       name = "sanitize",
187       description = "Sets the given service to NORMAL state",
188       returnDescription = "No content",
189       restParameters = {
190           @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier",
191               type = Type.STRING, defaultValue = ""),
192           @RestParameter(name = "host", isRequired = true, description = "The host providing the service, "
193               + "including the http(s) protocol", type = Type.STRING, defaultValue = "")
194       },
195       responses = {
196           @RestResponse(responseCode = SC_NO_CONTENT, description = "The service was successfully sanitized"),
197           @RestResponse(responseCode = SC_NOT_FOUND,
198               description = "No service of that type on that host is registered.")
199       })
200   public Response sanitize(@FormParam("serviceType") String serviceType, @FormParam("host") String host)
201           throws NotFoundException {
202     serviceRegistry.sanitize(serviceType, host);
203     return Response.status(Status.NO_CONTENT).build();
204   }
205 
206   @POST
207   @Path("register")
208   @Produces(MediaType.TEXT_XML)
209   @RestQuery(
210       name = "register",
211       description = "Add a new service registration to the cluster.",
212       returnDescription = "The service registration.",
213       restParameters = {
214           @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier",
215               type = Type.STRING, defaultValue = ""),
216           @RestParameter(name = "host", isRequired = true, description = "The host providing the service, "
217               + "including the http(s) protocol", type = Type.STRING, defaultValue = ""),
218           @RestParameter(name = "path", isRequired = true, description = "The service path on the host",
219               type = Type.STRING, defaultValue = ""),
220           @RestParameter(name = "jobProducer", isRequired = true, description = "Whether this service is a producer of "
221               + "long running jobs requiring dispatch", type = Type.STRING, defaultValue = "false")
222       },
223       responses = {
224           @RestResponse(responseCode = SC_OK, description = "An XML representation of the new service registration")
225       })
226   public JaxbServiceRegistration register(@FormParam("serviceType") String serviceType, @FormParam("host") String host,
227           @FormParam("path") String path, @FormParam("jobProducer") boolean jobProducer) {
228     try {
229       return new JaxbServiceRegistration(serviceRegistry.registerService(serviceType, host, path, jobProducer));
230     } catch (ServiceRegistryException e) {
231       throw new WebApplicationException(e);
232     }
233   }
234 
235   @POST
236   @Path("unregister")
237   @RestQuery(
238       name = "unregister",
239       description = "Removes a service registration.",
240       returnDescription = "No content",
241       restParameters = {
242           @RestParameter(name = "serviceType", isRequired = true, description = "The service type identifier",
243               type = Type.STRING),
244           @RestParameter(name = "host", isRequired = true, description = "The host providing the service, "
245               + "including the http(s) protocol", type = Type.STRING)
246       },
247       responses = {
248           @RestResponse(responseCode = SC_NO_CONTENT, description = "The service was unregistered successfully")
249       })
250   public Response unregister(@FormParam("serviceType") String serviceType, @FormParam("host") String host) {
251     try {
252       serviceRegistry.unRegisterService(serviceType, host);
253       return Response.status(Status.NO_CONTENT).build();
254     } catch (ServiceRegistryException e) {
255       throw new WebApplicationException(e);
256     }
257   }
258 
259   @POST
260   @Path("enablehost")
261   @RestQuery(
262       name = "enablehost",
263       description = "Enable a server from the cluster.",
264       returnDescription = "No content.",
265       restParameters = {
266           @RestParameter(name = "host", isRequired = true,
267               description = "The host name, including the http(s) protocol", type = Type.STRING)
268       },
269       responses = {
270           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was enabled successfully"),
271           @RestResponse(responseCode = SC_NOT_FOUND, description = "The host does not exist")
272       })
273   public Response enableHost(@FormParam("host") String host) throws NotFoundException {
274     try {
275       serviceRegistry.enableHost(host);
276       return Response.status(Status.NO_CONTENT).build();
277     } catch (ServiceRegistryException e) {
278       throw new WebApplicationException(e);
279     }
280   }
281 
282   @POST
283   @Path("disablehost")
284   @RestQuery(
285       name = "disablehost",
286       description = "Disable a server from the cluster.",
287       returnDescription = "No content.",
288       restParameters = {
289           @RestParameter(name = "host", isRequired = true,
290               description = "The host name, including the http(s) protocol", type = Type.STRING)
291       },
292       responses = {
293           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was disabled successfully"),
294           @RestResponse(responseCode = SC_NOT_FOUND, description = "The host does not exist")
295       })
296   public Response disableHost(@FormParam("host") String host) throws NotFoundException {
297     try {
298       serviceRegistry.disableHost(host);
299       return Response.status(Status.NO_CONTENT).build();
300     } catch (ServiceRegistryException e) {
301       throw new WebApplicationException(e);
302     }
303   }
304 
305   @POST
306   @Path("registerhost")
307   @RestQuery(
308       name = "registerhost",
309       description = "Add a new server to the cluster.",
310       returnDescription = "No content.",
311       restParameters = {
312           @RestParameter(name = "host", isRequired = true,
313               description = "The host name, including the http(s) protocol", type = Type.STRING),
314           @RestParameter(name = "address", isRequired = true, description = "The IP address", type = Type.STRING),
315           @RestParameter(name = "nodeName", isRequired = true,
316               description = "Descriptive node name", type = Type.STRING),
317           @RestParameter(name = "memory", isRequired = true, description = "The allocated memory", type = Type.STRING),
318           @RestParameter(name = "cores", isRequired = true, description = "The available cores", type = Type.STRING),
319           @RestParameter(name = "maxLoad", isRequired = true, description = "The maximum load this host support",
320               type = Type.STRING)
321       },
322       responses = {
323           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was registered successfully")
324       })
325   public void register(@FormParam("host") String host, @FormParam("address") String address,
326           @FormParam("nodeName") String nodeName, @FormParam("memory") long memory, @FormParam("cores") int cores,
327           @FormParam("maxLoad") float maxLoad) {
328     try {
329       serviceRegistry.registerHost(host, address, nodeName, memory, cores, maxLoad);
330     } catch (ServiceRegistryException e) {
331       throw new WebApplicationException(e);
332     }
333   }
334 
335   @POST
336   @Path("unregisterhost")
337   @RestQuery(
338       name = "unregisterhost",
339       description = "Removes a server from the cluster.",
340       returnDescription = "No content.",
341       restParameters = {
342         @RestParameter(name = "host", isRequired = true, description = "The host name, including the http(s) protocol",
343             type = Type.STRING)
344       },
345       responses = {
346         @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was removed successfully")
347       })
348   public Response unregister(@FormParam("host") String host) {
349     try {
350       serviceRegistry.unregisterHost(host);
351       return Response.status(Status.NO_CONTENT).build();
352     } catch (ServiceRegistryException e) {
353       if (e.getCause() instanceof IllegalArgumentException) {
354         return Response.status(Status.NOT_FOUND).entity(e.getMessage()).build();
355       }
356       throw new WebApplicationException(e);
357     }
358   }
359 
360   @POST
361   @Path("maintenance")
362   @RestQuery(
363       name = "maintenance",
364       description = "Sets the maintenance status for a server in the cluster.",
365       returnDescription = "No content.",
366       restParameters = {
367           @RestParameter(name = "host", isRequired = true, type = Type.STRING,
368               description = "The host name, including the http(s) protocol"),
369           @RestParameter(name = "maintenance", isRequired = true, type = Type.BOOLEAN,
370               description = "Whether this host should be put into maintenance mode (true) or not")
371       }, responses = {
372           @RestResponse(responseCode = SC_NO_CONTENT, description = "The host was registered successfully"),
373           @RestResponse(responseCode = SC_NOT_FOUND, description = "Host not found")
374       })
375   public Response setMaintenanceMode(@FormParam("host") String host, @FormParam("maintenance") boolean maintenance)
376           throws NotFoundException {
377     try {
378       serviceRegistry.setMaintenanceStatus(host, maintenance);
379       return Response.status(Status.NO_CONTENT).build();
380     } catch (ServiceRegistryException e) {
381       throw new WebApplicationException(e);
382     }
383   }
384 
385   @GET
386   @Path("available.xml")
387   @Produces(MediaType.TEXT_XML)
388   @RestQuery(
389       name = "availableasxml",
390       description = "Lists available services by service type identifier, ordered by load.",
391       returnDescription = "The services list as XML",
392       restParameters = {
393           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
394               description = "The service type identifier")
395       },
396       responses = {
397           @RestResponse(responseCode = SC_OK, description = "Returned the available services."),
398           @RestResponse(responseCode = SC_BAD_REQUEST, description = "No service type specified, bad request.")
399       })
400   public Response getAvailableServicesAsXml(@QueryParam("serviceType") String serviceType) {
401 
402     if (isBlank(serviceType)) {
403       throw new WebApplicationException(
404           Response.status(Status.BAD_REQUEST).entity("Service type must be specified").build());
405     }
406 
407     Map<String, String> properties = securityService.getOrganization().getProperties();
408 
409     JaxbServiceRegistrationList registrations = new JaxbServiceRegistrationList();
410     try {
411       for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByLoad(serviceType)) {
412         JaxbServiceRegistration jaxbReg = new JaxbServiceRegistration(reg);
413         URL internalHostUrl = new URL(jaxbReg.getHost());
414         String tenantSpecificHost = StringUtils.trimToNull(properties.get("org.opencastproject.host."
415             + internalHostUrl.getHost()));
416         if (StringUtils.isNotBlank(tenantSpecificHost)) {
417           jaxbReg.setHost(tenantSpecificHost);
418         }
419         registrations.add(jaxbReg);
420       }
421       return Response.ok(registrations).build();
422     } catch (ServiceRegistryException | MalformedURLException e) {
423       throw new WebApplicationException(e);
424     }
425   }
426 
427   @GET
428   @Path("available.json")
429   @Produces(MediaType.APPLICATION_JSON)
430   @RestQuery(
431       name = "availableasjson",
432       description = "Lists available services by service type identifier, ordered by load.",
433       returnDescription = "The services list as JSON",
434       restParameters = {
435           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
436               description = "The service type identifier")
437       },
438       responses = {
439           @RestResponse(responseCode = SC_OK, description = "Returned the available services."),
440           @RestResponse(responseCode = SC_BAD_REQUEST, description = "No service type specified, bad request.")
441       })
442   public Response getAvailableServicesAsJson(@QueryParam("serviceType") String serviceType) {
443     return getAvailableServicesAsXml(serviceType);
444   }
445 
446   @GET
447   @Path("health.json")
448   @Produces(MediaType.APPLICATION_JSON)
449   @RestQuery(
450       name = "health",
451       description = "Checks the status of the registered services",
452       returnDescription = "Returns NO_CONTENT if services are in a proper state",
453       restParameters = {
454           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
455               description = "The service type identifier"),
456           @RestParameter(name = "host", isRequired = false, type = Type.STRING,
457               description = "The host, including the http(s) protocol")
458       }, responses = {
459           @RestResponse(responseCode = SC_OK, description = "Service states returned"),
460           @RestResponse(responseCode = SC_NOT_FOUND,
461               description = "No service of that type on that host is registered."),
462           @RestResponse(responseCode = SC_SERVICE_UNAVAILABLE,
463               description = "An error has occurred during stats processing")
464       })
465   public Response getHealthStatusAsJson(@QueryParam("serviceType") String serviceType, @QueryParam("host") String host)
466           throws NotFoundException {
467     return getHealthStatus(serviceType, host);
468   }
469 
470   @GET
471   @Path("health.xml")
472   @Produces(MediaType.APPLICATION_XML)
473   @RestQuery(
474       name = "health",
475       description = "Checks the status of the registered services",
476       returnDescription = "Returns NO_CONTENT if services are in a proper state",
477       restParameters = {
478           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
479               description = "The service type identifier"),
480           @RestParameter(name = "host", isRequired = false, type = Type.STRING,
481               description = "The host, including the http(s) protocol")
482       },
483       responses = {
484           @RestResponse(responseCode = SC_OK, description = "Service states returned"),
485           @RestResponse(responseCode = SC_NOT_FOUND,
486               description = "No service of that type on that host is registered."),
487           @RestResponse(responseCode = SC_SERVICE_UNAVAILABLE,
488               description = "An error has occurred during stats processing")
489       })
490   public Response getHealthStatus(@QueryParam("serviceType") String serviceType, @QueryParam("host") String host)
491           throws NotFoundException {
492     try {
493       List<ServiceRegistration> services = null;
494       if (isNotBlank(serviceType) && isNotBlank(host)) {
495         // This is a request for one specific service. Return it, or SC_NOT_FOUND if not found
496         ServiceRegistration reg = serviceRegistry.getServiceRegistration(serviceType, host);
497         if (reg == null) {
498           throw new NotFoundException();
499         }
500 
501         services = new LinkedList<ServiceRegistration>();
502         services.add(reg);
503       } else if (isBlank(serviceType) && isBlank(host)) {
504         // This is a request for all service registrations
505         services = serviceRegistry.getServiceRegistrations();
506       } else if (isNotBlank(serviceType)) {
507         // This is a request for all service registrations of a particular type
508         services = serviceRegistry.getServiceRegistrationsByType(serviceType);
509       } else if (isNotBlank(host)) {
510         // This is a request for all service registrations of a particular host
511         services = serviceRegistry.getServiceRegistrationsByHost(host);
512       }
513 
514       int healthy = 0;
515       int warning = 0;
516       int error = 0;
517       for (ServiceRegistration reg : services) {
518         if (ServiceState.NORMAL == reg.getServiceState()) {
519           healthy++;
520         } else if (ServiceState.WARNING == reg.getServiceState()) {
521           warning++;
522         } else if (ServiceState.ERROR == reg.getServiceState()) {
523           error++;
524         } else {
525            error++;
526         }
527       }
528       JaxbServiceHealth stats = new JaxbServiceHealth(healthy, warning, error);
529       return Response.ok(stats).build();
530     } catch (ServiceRegistryException e) {
531       throw new WebApplicationException(e);
532     }
533   }
534 
535   @GET
536   @Path("services.xml")
537   @Produces(MediaType.TEXT_XML)
538   @RestQuery(
539       name = "servicesasxml",
540       description = "Returns a service registraton or list of available service registrations as XML.",
541       returnDescription = "The services list as XML",
542       restParameters = {
543           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
544               description = "The service type identifier"),
545           @RestParameter(name = "host", isRequired = false, type = Type.STRING,
546               description = "The host, including the http(s) protocol")
547       },
548       responses = {
549           @RestResponse(responseCode = SC_OK, description = "Returned the available service."),
550           @RestResponse(responseCode = SC_NOT_FOUND,
551               description = "No service of that type on that host is registered.")
552       })
553   public JaxbServiceRegistrationList getRegistrationsAsXml(@QueryParam("serviceType") String serviceType,
554           @QueryParam("host") String host) throws NotFoundException {
555     JaxbServiceRegistrationList registrations = new JaxbServiceRegistrationList();
556     try {
557       if (isNotBlank(serviceType) && isNotBlank(host)) {
558         // This is a request for one specific service. Return it, or SC_NOT_FOUND if not found
559         ServiceRegistration reg = serviceRegistry.getServiceRegistration(serviceType, host);
560         if (reg == null) {
561           throw new NotFoundException();
562         } else {
563           return new JaxbServiceRegistrationList(new JaxbServiceRegistration(reg));
564         }
565       } else if (isBlank(serviceType) && isBlank(host)) {
566         // This is a request for all service registrations
567         for (ServiceRegistration reg : serviceRegistry.getServiceRegistrations()) {
568           registrations.add(new JaxbServiceRegistration(reg));
569         }
570       } else if (isNotBlank(serviceType)) {
571         // This is a request for all service registrations of a particular type
572         for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByType(serviceType)) {
573           registrations.add(new JaxbServiceRegistration(reg));
574         }
575       } else if (isNotBlank(host)) {
576         // This is a request for all service registrations of a particular host
577         for (ServiceRegistration reg : serviceRegistry.getServiceRegistrationsByHost(host)) {
578           registrations.add(new JaxbServiceRegistration(reg));
579         }
580       }
581       return registrations;
582     } catch (ServiceRegistryException e) {
583       throw new WebApplicationException(e);
584     }
585   }
586 
587   @GET
588   @Path("services.json")
589   @Produces(MediaType.APPLICATION_JSON)
590   @RestQuery(
591       name = "servicesasjson",
592       description = "Returns a service registraton or list of available service registrations as JSON.",
593       returnDescription = "The services list as XML",
594       restParameters = {
595           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
596               description = "The service type identifier"),
597           @RestParameter(name = "host", isRequired = false, type = Type.STRING,
598               description = "The host, including the http(s) protocol")
599       },
600       responses = {
601           @RestResponse(responseCode = SC_OK, description = "Returned the available service."),
602           @RestResponse(responseCode = SC_NOT_FOUND,
603               description = "No service of that type on that host is registered.")
604       })
605   public JaxbServiceRegistrationList getRegistrationsAsJson(@QueryParam("serviceType") String serviceType,
606           @QueryParam("host") String host) throws NotFoundException {
607     return getRegistrationsAsXml(serviceType, host);
608   }
609 
610   @GET
611   @Path("hosts.xml")
612   @Produces(MediaType.TEXT_XML)
613   @RestQuery(
614       name = "hostsasxml",
615       description = "Returns a host registraton or list of available host registrations as XML.",
616       returnDescription = "The host list as XML",
617       responses = {
618           @RestResponse(responseCode = SC_OK, description = "Returned the available hosts.")
619       })
620   public JaxbHostRegistrationList getHostsAsXml() throws NotFoundException {
621     JaxbHostRegistrationList registrations = new JaxbHostRegistrationList();
622     try {
623       for (HostRegistration reg : serviceRegistry.getHostRegistrations()) {
624         registrations.add(new JaxbHostRegistration(reg));
625       }
626       return registrations;
627     } catch (ServiceRegistryException e) {
628       throw new WebApplicationException(e);
629     }
630   }
631 
632   @GET
633   @Path("hosts.json")
634   @Produces(MediaType.APPLICATION_JSON)
635   @RestQuery(
636       name = "hostsasjson",
637       description = "Returns a host registraton or list of available host registrations as JSON.",
638       returnDescription = "The host list as JSON",
639       responses = {
640           @RestResponse(responseCode = SC_OK, description = "Returned the available hosts.")
641       })
642   public JaxbHostRegistrationList getHostsAsJson() throws NotFoundException {
643     return getHostsAsXml();
644   }
645 
646   @POST
647   @Path("job")
648   @Produces(MediaType.TEXT_XML)
649   @RestQuery(
650       name = "createjob",
651       description = "Creates a new job.",
652       returnDescription = "An XML representation of the job.",
653       restParameters = {
654           @RestParameter(name = "jobType", isRequired = true, type = Type.STRING,
655               description = "The job type identifier"),
656           @RestParameter(name = "host", isRequired = true, type = Type.STRING,
657               description = "The creating host, including the http(s) protocol"),
658           @RestParameter(name = "operation", isRequired = true, type = Type.STRING,
659               description = "The operation this job should execute"),
660           @RestParameter(name = "payload", isRequired = false, type = Type.TEXT,
661               description = "The job type identifier"),
662           @RestParameter(name = "start", isRequired = false, type = Type.BOOLEAN,
663               description = "Whether the job should be queued for dispatch and execution"),
664           @RestParameter(name = "jobLoad", isRequired = false, type = Type.STRING,
665               description = "The load this job will incur on the system"),
666           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
667               description = "An argument for the operation"),
668           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
669               description = "An argument for the operation"),
670           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
671               description = "An argument for the operation"),
672           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
673               description = "An argument for the operation"),
674           @RestParameter(name = "arg", isRequired = false, type = Type.TEXT,
675               description = "An argument for the operation")
676       },
677       responses = {
678           @RestResponse(responseCode = SC_CREATED, description = "Job created."),
679           @RestResponse(responseCode = SC_BAD_REQUEST,
680               description = "The required parameters were not supplied, bad request.")
681       })
682   public Response createJob(@Context HttpServletRequest request) {
683     String[] argArray = request.getParameterValues("arg");
684     List<String> arguments = null;
685     if (argArray != null && argArray.length > 0) {
686       arguments = Arrays.asList(argArray);
687     }
688     String jobType = request.getParameter("jobType");
689     String operation = request.getParameter("operation");
690     String host = request.getParameter("host");
691     String payload = request.getParameter("payload");
692     boolean start = StringUtils.isBlank(request.getParameter("start"))
693             || Boolean.TRUE.toString().equalsIgnoreCase(request.getParameter("start"));
694     try {
695       Job job = null;
696       if (StringUtils.isNotBlank(request.getParameter("jobLoad"))) {
697         Float jobLoad = Float.parseFloat(request.getParameter("jobLoad"));
698         job = ((ServiceRegistryJpaImpl) serviceRegistry).createJob(host, jobType, operation, arguments, payload,
699               start, serviceRegistry.getCurrentJob(), jobLoad);
700       } else {
701         job = ((ServiceRegistryJpaImpl) serviceRegistry).createJob(host, jobType, operation, arguments, payload,
702                 start, serviceRegistry.getCurrentJob());
703       }
704       return Response.created(job.getUri()).entity(new JaxbJob(job)).build();
705     } catch (IllegalArgumentException e) {
706       throw new WebApplicationException(Status.BAD_REQUEST);
707     } catch (Exception e) {
708       throw new WebApplicationException(e);
709     }
710   }
711 
712   @PUT
713   @Path("job/{id}.xml")
714   @Produces(MediaType.TEXT_XML)
715   @RestQuery(
716       name = "updatejob",
717       description = "Updates an existing job",
718       returnDescription = "No content",
719       pathParameters = {
720           @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The job identifier")
721       },
722       restParameters = {
723           @RestParameter(name = "job", isRequired = true, type = Type.TEXT, description = "The updated job as XML")
724       },
725       responses = {
726           @RestResponse(responseCode = SC_NO_CONTENT, description = "Job updated."),
727           @RestResponse(responseCode = SC_NOT_FOUND, description = "Job not found.")
728       })
729   public Response updateJob(@PathParam("id") String id, @FormParam("job") String jobXml) throws NotFoundException {
730     try {
731       Job job = JobParser.parseJob(jobXml);
732       serviceRegistry.updateJob(job);
733       return Response.status(Status.NO_CONTENT).build();
734     } catch (Exception e) {
735       throw new WebApplicationException(e);
736     }
737   }
738 
739   @GET
740   @Path("job/{id}.xml")
741   @Produces(MediaType.TEXT_XML)
742   @RestQuery(
743       name = "jobasxml",
744       description = "Returns a job as XML.",
745       returnDescription = "The job as XML",
746       pathParameters = {
747           @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The job identifier")
748       },
749       responses = {
750           @RestResponse(responseCode = SC_OK, description = "Job found."),
751           @RestResponse(responseCode = SC_NOT_FOUND, description = "No job with that identifier exists.")
752       })
753   public JaxbJob getJobAsXml(@PathParam("id") long id) throws NotFoundException {
754     return getJobAsJson(id);
755   }
756 
757   @GET
758   @Path("job/{id}.json")
759   @Produces(MediaType.APPLICATION_JSON)
760   @RestQuery(
761       name = "jobasjson",
762       description = "Returns a job as JSON.",
763       returnDescription = "The job as JSON",
764       pathParameters = {
765           @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The job identifier")
766       },
767       responses = {
768           @RestResponse(responseCode = SC_OK, description = "Job found."),
769           @RestResponse(responseCode = SC_NOT_FOUND, description = "No job with that identifier exists.")
770       })
771   public JaxbJob getJobAsJson(@PathParam("id") long id) throws NotFoundException {
772     try {
773       return new JaxbJob(serviceRegistry.getJob(id));
774     } catch (ServiceRegistryException e) {
775       throw new WebApplicationException(e);
776     }
777   }
778 
779   @GET
780   @Path("job/{id}/children.xml")
781   @Produces(MediaType.TEXT_XML)
782   @RestQuery(
783       name = "childrenjobsasxml",
784       description = "Returns all children from a job as XML.",
785       returnDescription = "A list of children jobs as XML",
786       pathParameters = {
787           @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The parent job identifier")
788       },
789       responses = {
790           @RestResponse(responseCode = SC_OK, description = "Jobs found.")
791       })
792   public JaxbJobList getChildrenJobsAsXml(@PathParam("id") long id) {
793     return getChildrenJobsAsJson(id);
794   }
795 
796   @GET
797   @Path("job/{id}/children.json")
798   @Produces(MediaType.APPLICATION_JSON)
799   @RestQuery(
800       name = "childrenjobsasjson",
801       description = "Returns all children from a job as JSON.",
802       returnDescription = "A list of children jobs as JSON",
803       pathParameters = {
804           @RestParameter(name = "id", isRequired = true, type = Type.STRING, description = "The parent job identifier")
805       },
806       responses = {
807           @RestResponse(responseCode = SC_OK, description = "Jobs found.")
808       })
809   public JaxbJobList getChildrenJobsAsJson(@PathParam("id") long id) {
810     try {
811       return new JaxbJobList(serviceRegistry.getChildJobs(id));
812     } catch (ServiceRegistryException e) {
813       throw new WebApplicationException(e);
814     }
815   }
816 
817   @GET
818   @Path("jobs.xml")
819   @Produces(MediaType.TEXT_XML)
820   public JaxbJobList getJobsAsXml(@QueryParam("serviceType") String serviceType,
821       @QueryParam("status") Job.Status status) {
822     try {
823       return new JaxbJobList(serviceRegistry.getJobs(serviceType, status));
824     } catch (ServiceRegistryException e) {
825       throw new WebApplicationException(e);
826     }
827 
828   }
829 
830   @GET
831   @Path("activeJobs.xml")
832   @Produces(MediaType.TEXT_XML)
833   @RestQuery(
834       name = "activejobsasxml",
835       description = "Returns all active jobs as XML.",
836       returnDescription = "A list of active jobs as XML",
837       responses = {
838           @RestResponse(responseCode = SC_OK, description = "Active jobs found.")
839       })
840   public JaxbJobList getActiveJobsAsXml() {
841     try {
842       return new JaxbJobList(serviceRegistry.getActiveJobs());
843     } catch (ServiceRegistryException e) {
844       throw new WebApplicationException(e);
845     }
846   }
847 
848   @GET
849   @Path("activeJobs.json")
850   @Produces(MediaType.APPLICATION_JSON)
851   @RestQuery(
852       name = "activejobsasjson",
853       description = "Returns all active jobs as JSON.",
854       returnDescription = "A list of active jobs as JSON",
855       responses = {
856           @RestResponse(responseCode = SC_OK, description = "Active jobs found.")
857       })
858   public JaxbJobList getActiveJobsAsJson() {
859     try {
860       return new JaxbJobList(serviceRegistry.getActiveJobs());
861     } catch (ServiceRegistryException e) {
862       throw new WebApplicationException(e);
863     }
864   }
865 
866   @GET
867   @Path("count")
868   @Produces(MediaType.TEXT_PLAIN)
869   @RestQuery(
870       name = "count",
871       description = "Returns the number of jobs matching the query parameters as plain text.",
872       returnDescription = "The number of matching jobs",
873       restParameters = {
874           @RestParameter(name = "serviceType", isRequired = false, type = Type.STRING,
875               description = "The service type identifier"),
876           @RestParameter(name = "status", isRequired = false, type = Type.STRING, description = "The job status"),
877           @RestParameter(name = "host", isRequired = false, type = Type.STRING,
878               description = "The host executing the job"),
879           @RestParameter(name = "operation", isRequired = false, type = Type.STRING,
880               description = "The job's operation")
881       },
882       responses = {
883           @RestResponse(responseCode = SC_OK, description = "Job count returned.")
884       })
885   public long count(@QueryParam("serviceType") String serviceType, @QueryParam("status") Job.Status status,
886           @QueryParam("host") String host, @QueryParam("operation") String operation) {
887     try {
888       if (isNotBlank(host) && isNotBlank(operation)) {
889         if (isBlank(serviceType)) {
890           throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
891         }
892         return serviceRegistry.count(serviceType, host, operation, status);
893       } else if (isNotBlank(host)) {
894         if (isBlank(serviceType)) {
895           throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
896         }
897         return serviceRegistry.countByHost(serviceType, host, status);
898       } else if (isNotBlank(operation)) {
899         if (isBlank(serviceType)) {
900           throw new WebApplicationException(Response.serverError().entity("Service type must not be null").build());
901         }
902         return serviceRegistry.countByOperation(serviceType, operation, status);
903       } else {
904         return serviceRegistry.count(serviceType, status);
905       }
906     } catch (ServiceRegistryException e) {
907       throw new WebApplicationException(e);
908     }
909   }
910 
911   @GET
912   @Path("maxconcurrentjobs")
913   @Produces(MediaType.TEXT_PLAIN)
914   @RestQuery(
915       name = "maxconcurrentjobs",
916       description = "Returns the number of jobs that the servers in this service registry can execute concurrently. "
917           + "If there is only one server in this service registry this will be the number of jobs that one server is "
918           + "able to do at one time. If it is a distributed install across many servers then this number will be the "
919           + "total number of jobs the cluster can process concurrently.",
920       returnDescription = "The maximum number of concurrent jobs",
921       responses = {
922           @RestResponse(responseCode = SC_OK, description = "Maximum number of concurrent jobs returned.")
923       })
924   @Deprecated
925   public Response getMaximumConcurrentWorkflows() {
926     return Response.status(Status.MOVED_PERMANENTLY).type(MediaType.TEXT_PLAIN)
927             .header("Location", servicePath + "/maxload").build();
928   }
929 
930   @GET
931   @Path("maxload")
932   @Produces(MediaType.TEXT_XML)
933   @RestQuery(
934       name = "maxload",
935       description = "Returns the maximum load that servers in this service registry can execute concurrently.  "
936           + "If there is only one server in this service registry this will be the maximum load that one server is "
937           + "able to handle at one time. If it is a distributed install across many servers then this number will be "
938           + "the maximum load the cluster can process concurrently.",
939       returnDescription = "The maximum load of the cluster or server",
940       restParameters = {
941           @RestParameter(name = "host", isRequired = false, type = Type.STRING,
942               description = "The host you want to know the maximum load for.")
943       },
944       responses = {
945           @RestResponse(responseCode = SC_OK, description = "Maximum load for the cluster.")
946       })
947   public Response getMaxLoadOnNode(@QueryParam("host") String host) throws NotFoundException {
948     try {
949       if (StringUtils.isEmpty(host)) {
950         return Response.ok(serviceRegistry.getMaxLoads()).build();
951       } else {
952         SystemLoad systemLoad = new SystemLoad();
953         systemLoad.addNodeLoad(serviceRegistry.getMaxLoadOnNode(host));
954         return Response.ok(systemLoad).build();
955       }
956     } catch (ServiceRegistryException e) {
957       throw new WebApplicationException(e);
958     }
959   }
960 
961   @GET
962   @Path("currentload")
963   @Produces(MediaType.TEXT_XML)
964   @RestQuery(
965       name = "currentload",
966       description = "Returns the current load on the servers in this service registry.  "
967           + "If there is only one server in this service registry this will be the the load that one server.  "
968           + "If it is a distributed install across many servers then this number will be a dictionary of the load on "
969           + "all nodes in the cluster.",
970       returnDescription = "The current load across the cluster",
971       restParameters = {},
972       responses = {
973           @RestResponse(responseCode = SC_OK, description = "Current load for the cluster.")
974       })
975   public Response getCurrentLoad() {
976     try {
977       return Response.ok(serviceRegistry.getCurrentHostLoads()).build();
978     } catch (ServiceRegistryException e) {
979       throw new WebApplicationException(e);
980     }
981   }
982 
983   @GET
984   @Path("ownload")
985   @Produces(MediaType.TEXT_PLAIN)
986   @RestQuery(
987       name = "ownload",
988       description = "Returns the current load on this service registry's node.",
989       returnDescription = "The current load across the cluster",
990       restParameters = {},
991       responses = {
992           @RestResponse(responseCode = SC_OK, description = "Current load for the cluster.")
993       })
994   public Response getOwnLoad() {
995     try {
996       return Response.ok(serviceRegistry.getOwnLoad()).build();
997     } catch (ServiceRegistryException e) {
998       throw new WebApplicationException(e);
999     }
1000   }
1001 
1002 
1003   @DELETE
1004   @Path("job/{id}")
1005   @RestQuery(
1006       name = "deletejob",
1007       description = "Deletes a job from the service registry",
1008       returnDescription = "No data is returned, just the HTTP status code",
1009       pathParameters = {
1010           @RestParameter(isRequired = true, name = "id", type = Type.INTEGER, description = "ID of the job to delete")
1011       },
1012       responses = {
1013           @RestResponse(responseCode = SC_NO_CONTENT, description = "Job successfully deleted"),
1014           @RestResponse(responseCode = SC_NOT_FOUND, description = "Job with given id could not be found")
1015       })
1016   public Response deleteJob(@PathParam("id") long id) throws NotFoundException {
1017     try {
1018       serviceRegistry.removeJobs(Collections.singletonList(id));
1019       return Response.noContent().build();
1020     } catch (ServiceRegistryException e) {
1021       throw new WebApplicationException(e);
1022     }
1023   }
1024 
1025 
1026   @POST
1027   @Path("removejobs")
1028   @RestQuery(
1029       name = "removejobs",
1030       description = "Removes all given jobs and their child jobs",
1031       returnDescription = "No data is returned, just the HTTP status code",
1032       restParameters = {
1033           @RestParameter(name = "jobIds", isRequired = true, description = "The IDs of the jobs to delete",
1034               type = Type.TEXT),
1035       },
1036       responses = {
1037           @RestResponse(responseCode = SC_NO_CONTENT, description = "Jobs successfully removed"),
1038           @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
1039               description = "Error while removing jobs")
1040       })
1041   public Response removeParentlessJobs(@FormParam("jobIds") String jobIds) throws NotFoundException {
1042     try {
1043       final JSONArray array = (JSONArray) JSONValue.parse(jobIds);
1044       final List<Long> jobIdList = Arrays.asList((Long[]) array.toArray(new Long[0]));
1045       serviceRegistry.removeJobs(jobIdList);
1046       return Response.noContent().build();
1047     } catch (ServiceRegistryException e) {
1048       throw new WebApplicationException(e);
1049     }
1050   }
1051 
1052 
1053   @POST
1054   @Path("removeparentlessjobs")
1055   @RestQuery(
1056       name = "removeparentlessjobs",
1057       description = "Removes all jobs without a parent job which have passed their lifetime",
1058       returnDescription = "No data is returned, just the HTTP status code",
1059       restParameters = {
1060           @RestParameter(name = "lifetime", isRequired = true, type = Type.INTEGER,
1061               description = "Lifetime of parentless jobs")
1062       },
1063       responses = {
1064           @RestResponse(responseCode = SC_NO_CONTENT, description = "Parentless jobs successfully removed"),
1065           @RestResponse(responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
1066               description = "Error while removing parentless jobs")
1067       })
1068   public Response removeParentlessJobs(@FormParam("lifetime") int lifetime) {
1069     try {
1070       serviceRegistry.removeParentlessJobs(lifetime);
1071       return Response.noContent().build();
1072     } catch (ServiceRegistryException e) {
1073       throw new WebApplicationException(e);
1074     }
1075   }
1076 
1077 }