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.adminui.endpoint;
23  
24  import static org.opencastproject.index.service.util.JSONUtils.safeString;
25  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
26  
27  import org.opencastproject.index.service.resources.list.query.ServicesListQuery;
28  import org.opencastproject.index.service.util.RestUtils;
29  import org.opencastproject.serviceregistry.api.HostRegistration;
30  import org.opencastproject.serviceregistry.api.ServiceRegistry;
31  import org.opencastproject.serviceregistry.api.ServiceState;
32  import org.opencastproject.serviceregistry.api.ServiceStatistics;
33  import org.opencastproject.util.SmartIterator;
34  import org.opencastproject.util.doc.rest.RestParameter;
35  import org.opencastproject.util.doc.rest.RestQuery;
36  import org.opencastproject.util.doc.rest.RestResponse;
37  import org.opencastproject.util.doc.rest.RestService;
38  import org.opencastproject.util.requests.SortCriterion;
39  import org.opencastproject.util.requests.SortCriterion.Order;
40  
41  import com.google.gson.JsonObject;
42  
43  import org.apache.commons.lang3.StringUtils;
44  import org.json.simple.JSONAware;
45  import org.json.simple.JSONObject;
46  import org.osgi.service.component.annotations.Activate;
47  import org.osgi.service.component.annotations.Component;
48  import org.osgi.service.component.annotations.Reference;
49  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  import java.util.ArrayList;
54  import java.util.Collections;
55  import java.util.Comparator;
56  import java.util.HashMap;
57  import java.util.List;
58  import java.util.Map;
59  import java.util.Optional;
60  import java.util.concurrent.TimeUnit;
61  
62  import javax.servlet.http.HttpServletResponse;
63  import javax.ws.rs.GET;
64  import javax.ws.rs.Path;
65  import javax.ws.rs.Produces;
66  import javax.ws.rs.QueryParam;
67  import javax.ws.rs.core.MediaType;
68  import javax.ws.rs.core.Response;
69  
70  @Path("/admin-ng/services")
71  @RestService(
72      name = "ServicesProxyService",
73      title = "UI Services",
74      abstractText = "This service provides the services data for the UI.",
75      notes = { "These Endpoints deliver informations about the services required for the UI.",
76                "<strong>Important:</strong> "
77                  + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
78                  + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
79                  + "DO NOT use this for integration of third-party applications.<em>"})
80  @Component(
81      immediate = true,
82      service = ServicesEndpoint.class,
83      property = {
84          "service.description=Admin UI - Services facade Endpoint",
85          "opencast.service.type=org.opencastproject.adminui.endpoint.ServicesEndpoint",
86          "opencast.service.path=/admin-ng/services"
87      }
88  )
89  @JaxrsResource
90  public class ServicesEndpoint {
91    private static final Logger logger = LoggerFactory.getLogger(ServicesEndpoint.class);
92    private ServiceRegistry serviceRegistry;
93  
94    private static final String SERVICE_STATUS_TRANSLATION_PREFIX = "SYSTEMS.SERVICES.STATUS.";
95  
96  
97    @GET
98    @Path("services.json")
99    @Produces(MediaType.APPLICATION_JSON)
100   @RestQuery(
101       description = "Returns the list of services",
102       name = "services",
103       restParameters = {
104           @RestParameter(name = "limit", description = "The maximum number of items to return per page",
105               isRequired = false, type = RestParameter.Type.INTEGER),
106           @RestParameter(name = "offset", description = "The offset", isRequired = false,
107               type = RestParameter.Type.INTEGER),
108           @RestParameter(name = "filter", description = "Filter results by name, host, actions, status or free text "
109               + "query", isRequired = false, type = STRING),
110           @RestParameter(name = "sort", description = "The sort order.  May include any "
111               + "of the following: host, name, running, queued, completed,  meanRunTime, meanQueueTime, "
112               + "status. The sort suffix must be :asc for ascending sort order and :desc for descending.",
113               isRequired = false, type = STRING)
114       },
115       responses = {
116           @RestResponse(description = "Returns the list of services from Opencast",
117               responseCode = HttpServletResponse.SC_OK)
118       },
119       returnDescription = "The list of services")
120   public Response getServices(@QueryParam("limit") final int limit, @QueryParam("offset") final int offset,
121       @QueryParam("filter") String filter, @QueryParam("sort") String sort) throws Exception {
122 
123     Optional<String> sortOpt = Optional.ofNullable(StringUtils.trimToNull(sort));
124     ServicesListQuery query = new ServicesListQuery();
125     EndpointUtil.addRequestFiltersToQuery(filter, query);
126 
127     String fName = null;
128     if (query.getName().isPresent()) {
129       fName = StringUtils.trimToNull(query.getName().get());
130     }
131     String fHostname = null;
132     if (query.getHostname().isPresent()) {
133       fHostname = StringUtils.trimToNull(query.getHostname().get());
134     }
135     String fNodeName = null;
136     if (query.getNodeName().isPresent()) {
137       fNodeName = StringUtils.trimToNull(query.getNodeName().get());
138     }
139     String fStatus = null;
140     if (query.getStatus().isPresent()) {
141       fStatus = StringUtils.trimToNull(query.getStatus().get());
142     }
143     String fFreeText = null;
144     if (query.getFreeText().isPresent()) {
145       fFreeText = StringUtils.trimToNull(query.getFreeText().get());
146     }
147 
148     List<HostRegistration> servers = serviceRegistry.getHostRegistrations();
149     List<Service> services = new ArrayList<Service>();
150     for (ServiceStatistics stats : serviceRegistry.getServiceStatistics()) {
151       Service service = new Service(stats, findServerByHost(stats.getServiceRegistration().getHost(), servers));
152       if (fName != null && !StringUtils.equalsIgnoreCase(service.getName(), fName)) {
153         continue;
154       }
155 
156       if (fHostname != null && !StringUtils.equalsIgnoreCase(service.getHost(), fHostname)) {
157         continue;
158       }
159 
160       if (fNodeName != null && !StringUtils.equalsIgnoreCase(service.getNodeName(), fNodeName)) {
161         continue;
162       }
163 
164       if (fStatus != null && !StringUtils.equalsIgnoreCase(service.getStatus().toString(), fStatus)) {
165         continue;
166       }
167 
168       if (query.getActions().isPresent()) {
169         ServiceState serviceState = service.getStatus();
170 
171         if (query.getActions().get()) {
172           if (ServiceState.NORMAL == serviceState) {
173             continue;
174           }
175         } else {
176           if (ServiceState.NORMAL != serviceState) {
177             continue;
178           }
179         }
180       }
181 
182       if (fFreeText != null && !StringUtils.containsIgnoreCase(service.getName(), fFreeText)
183           && !StringUtils.containsIgnoreCase(service.getHost(), fFreeText)
184           && !StringUtils.containsIgnoreCase(service.getNodeName(), fFreeText)
185           && !StringUtils.containsIgnoreCase(service.getStatus().toString(), fFreeText)) {
186         continue;
187       }
188 
189       services.add(service);
190     }
191     int total = services.size();
192 
193     if (sortOpt.isPresent()) {
194       ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sortOpt.get());
195       if (!sortCriteria.isEmpty()) {
196         try {
197           SortCriterion sortCriterion = sortCriteria.iterator().next();
198           Collections.sort(services, new ServiceStatisticsComparator(
199                   sortCriterion.getFieldName(),
200                   sortCriterion.getOrder() == Order.Ascending));
201         } catch (Exception ex) {
202           logger.warn("Failed to sort services collection.", ex);
203         }
204       }
205     }
206 
207     List<JsonObject> jsonList = new ArrayList<>();
208     List<Service> limitedServices = new SmartIterator<Service>(limit, offset).applyLimitAndOffset(services);
209 
210     for (Service s : limitedServices) {
211       jsonList.add(s.toJSON());
212     }
213 
214     return RestUtils.okJsonList(jsonList, offset, limit, total);
215   }
216 
217   /**
218    * Service UI model. Wrapper class for a {@code ServiceStatistics} class.
219    */
220   class Service implements JSONAware {
221     /** Completed model field name. */
222     public static final String COMPLETED_NAME = "completed";
223     /** Host model field name. */
224     public static final String HOST_NAME = "hostname";
225     /** Node name model field name. */
226     public static final String NODE_NAME = "nodeName";
227     /** MeanQueueTime model field name. */
228     public static final String MEAN_QUEUE_TIME_NAME = "meanQueueTime";
229     /** MeanRunTime model field name. */
230     public static final String MEAN_RUN_TIME_NAME = "meanRunTime";
231     /** (Service-) Name model field name. */
232     public static final String NAME_NAME = "name";
233     /** Queued model field name. */
234     public static final String QUEUED_NAME = "queued";
235     /** Running model field name. */
236     public static final String RUNNING_NAME = "running";
237     /** Status model field name. */
238     public static final String STATUS_NAME = "status";
239     /** Online model field name. */
240     public static final String ONLINE_NAME = "online";
241     /** Maintenance model field name. */
242     public static final String MAINTENANCE_NAME = "maintenance";
243 
244     /** Wrapped {@code ServiceStatistics} instance. */
245     private final ServiceStatistics serviceStatistics;
246 
247     private final Optional<HostRegistration> server;
248 
249     /** Constructor, set {@code ServiceStatistics} instance to a final private property. */
250     Service(ServiceStatistics serviceStatistics, Optional<HostRegistration> server) {
251       this.serviceStatistics = serviceStatistics;
252       this.server = server;
253     }
254 
255     /**
256      * Returns completed jobs count.
257      * @return completed jobs count
258      */
259     public int getCompletedJobs() {
260       return serviceStatistics.getFinishedJobs();
261     }
262 
263     /**
264      * Returns service host name.
265      * @return service host name
266      */
267     public String getHost() {
268       return serviceStatistics.getServiceRegistration().getHost();
269     }
270 
271     /**
272      * Returns service host name.
273      * @return service host name
274      */
275     public String getNodeName() {
276       return server.isPresent() ? server.get().getNodeName() : "";
277     }
278 
279     /**
280      * Returns service mean queue time in seconds.
281      * @return service mean queue time in seconds
282      */
283     public long getMeanQueueTime() {
284       return TimeUnit.MILLISECONDS.toSeconds(serviceStatistics.getMeanQueueTime());
285     }
286 
287     /**
288      * Returns service mean run time in seconds.
289      * @return service mean run time in seconds
290      */
291     public long getMeanRunTime() {
292       return TimeUnit.MILLISECONDS.toSeconds(serviceStatistics.getMeanRunTime());
293     }
294 
295     /**
296      * Returns service name.
297      * @return service name
298      */
299     public String getName() {
300       return serviceStatistics.getServiceRegistration().getServiceType();
301     }
302 
303     /**
304      * Returns queued jobs count.
305      * @return queued jobs count
306      */
307     public int getQueuedJobs() {
308       return serviceStatistics.getQueuedJobs();
309     }
310 
311     /**
312      * Returns running jobs count.
313      * @return running jobs count
314      */
315     public int getRunningJobs() {
316       return serviceStatistics.getRunningJobs();
317     }
318 
319     /**
320      * Returns service status.
321      * @return service status
322      */
323     public ServiceState getStatus() {
324       return serviceStatistics.getServiceRegistration().getServiceState();
325     }
326 
327     /**
328      * Returns whether the service is online.
329      * @return online status
330      */
331     public boolean getIsOnline() {
332       return serviceStatistics.getServiceRegistration().isOnline();
333     }
334 
335     /**
336      * Returns whether the service is in maintenance.
337      * @return maintenance status
338      */
339     public boolean getisMaintenance() {
340       return serviceStatistics.getServiceRegistration().isInMaintenanceMode();
341     }
342 
343     /**
344      * Returns a map of all service fields.
345      * @return a map of all service fields
346      */
347     public Map<String, String> toMap() {
348       Map<String, String> serviceMap = new HashMap<String, String>();
349       serviceMap.put(COMPLETED_NAME, Integer.toString(getCompletedJobs()));
350       serviceMap.put(HOST_NAME, getHost());
351       serviceMap.put(NODE_NAME, getNodeName());
352       serviceMap.put(MEAN_QUEUE_TIME_NAME, Long.toString(getMeanQueueTime()));
353       serviceMap.put(MEAN_RUN_TIME_NAME, Long.toString(getMeanRunTime()));
354       serviceMap.put(NAME_NAME, getName());
355       serviceMap.put(QUEUED_NAME, Integer.toString(getQueuedJobs()));
356       serviceMap.put(RUNNING_NAME, Integer.toString(getRunningJobs()));
357       serviceMap.put(STATUS_NAME, getStatus().name());
358       serviceMap.put(ONLINE_NAME, Boolean.toString(getIsOnline()));
359       serviceMap.put(MAINTENANCE_NAME, Boolean.toString(getisMaintenance()));
360       return serviceMap;
361     }
362 
363     /**
364      * Returns a json representation of a service as {@code String}.
365      * @return a json representation of a service as {@code String}
366      */
367     @Override
368     public String toJSONString() {
369       return JSONObject.toJSONString(toMap());
370     }
371 
372     /**
373      * Returns a json representation of a service as {@code JValue}.
374      * @return a json representation of a service as {@code JValue}
375      */
376     public JsonObject toJSON() {
377       JsonObject json = new JsonObject();
378       json.addProperty(COMPLETED_NAME, getCompletedJobs());
379       json.addProperty(HOST_NAME, safeString(getHost()));
380       json.addProperty(NODE_NAME, safeString(getNodeName()));
381       json.addProperty(MEAN_QUEUE_TIME_NAME, getMeanQueueTime());
382       json.addProperty(MEAN_RUN_TIME_NAME, getMeanRunTime());
383       json.addProperty(NAME_NAME, safeString(getName()));
384       json.addProperty(QUEUED_NAME, getQueuedJobs());
385       json.addProperty(RUNNING_NAME, getRunningJobs());
386       json.addProperty(STATUS_NAME, SERVICE_STATUS_TRANSLATION_PREFIX + getStatus().name());
387       json.addProperty(ONLINE_NAME, getIsOnline());
388       json.addProperty(MAINTENANCE_NAME, getisMaintenance());
389       return json;
390     }
391   }
392 
393   /**
394    * {@code Service} comparator. Can compare service instances based on the given sort criterion and sort order.
395    */
396   class ServiceStatisticsComparator implements Comparator<Service> {
397 
398     /** Sort criterion. */
399     private final String sortBy;
400     /** Sort order (true if ascending, false otherwise). */
401     private final boolean ascending;
402 
403     /** Constructor. */
404     ServiceStatisticsComparator(String sortBy, boolean ascending) {
405       if (StringUtils.equalsIgnoreCase(Service.COMPLETED_NAME, sortBy)) {
406         this.sortBy = Service.COMPLETED_NAME;
407       } else if (StringUtils.equalsIgnoreCase(Service.HOST_NAME, sortBy)) {
408         this.sortBy = Service.HOST_NAME;
409       } else if (StringUtils.equalsIgnoreCase(Service.NODE_NAME, sortBy)) {
410         this.sortBy = Service.NODE_NAME;
411       } else if (StringUtils.equalsIgnoreCase(Service.MEAN_QUEUE_TIME_NAME, sortBy)) {
412         this.sortBy = Service.MEAN_QUEUE_TIME_NAME;
413       } else if (StringUtils.equalsIgnoreCase(Service.MEAN_RUN_TIME_NAME, sortBy)) {
414         this.sortBy = Service.MEAN_RUN_TIME_NAME;
415       } else if (StringUtils.equalsIgnoreCase(Service.NAME_NAME, sortBy)) {
416         this.sortBy = Service.NAME_NAME;
417       } else if (StringUtils.equalsIgnoreCase(Service.QUEUED_NAME, sortBy)) {
418         this.sortBy = Service.QUEUED_NAME;
419       } else if (StringUtils.equalsIgnoreCase(Service.RUNNING_NAME, sortBy)) {
420         this.sortBy = Service.RUNNING_NAME;
421       } else if (StringUtils.equalsIgnoreCase(Service.STATUS_NAME, sortBy)) {
422         this.sortBy = Service.STATUS_NAME;
423       } else {
424         throw new IllegalArgumentException(String.format("Can't sort services by %s.", sortBy));
425       }
426       this.ascending = ascending;
427     }
428 
429     /**
430      * Compare two service instances.
431      * @param s1 first {@code Service} instance to compare
432      * @param s2 second {@code Service} instance to compare
433      * @return
434      */
435     @Override
436     public int compare(Service s1, Service s2) {
437       int result = 0;
438       switch (sortBy) {
439         case Service.COMPLETED_NAME:
440           result = s1.getCompletedJobs() - s2.getCompletedJobs();
441           break;
442         case Service.HOST_NAME:
443           result = s1.getHost().compareToIgnoreCase(s2.getHost());
444           break;
445         case Service.NODE_NAME:
446           result = s1.getNodeName().compareToIgnoreCase(s2.getNodeName());
447           break;
448         case Service.MEAN_QUEUE_TIME_NAME:
449           result = (int) (s1.getMeanQueueTime() - s2.getMeanQueueTime());
450           break;
451         case Service.MEAN_RUN_TIME_NAME:
452           result = (int) (s1.getMeanRunTime() - s2.getMeanRunTime());
453           break;
454         case Service.QUEUED_NAME:
455           result = s1.getQueuedJobs() - s2.getQueuedJobs();
456           break;
457         case Service.RUNNING_NAME:
458           result = s1.getRunningJobs() - s2.getRunningJobs();
459           break;
460         case Service.STATUS_NAME:
461           result = s1.getStatus().compareTo(s2.getStatus());
462           break;
463         case Service.NAME_NAME: // default sorting criterium
464         default:
465           result = s1.getName().compareToIgnoreCase(s2.getName());
466       }
467       return ascending ? result : 0 - result;
468     }
469   }
470 
471   /** OSGI activate method. */
472   @Activate
473   public void activate() {
474     logger.info("ServicesEndpoint is activated!");
475   }
476 
477   /**
478    * @param serviceRegistry
479    *          the serviceRegistry to set
480    */
481   @Reference
482   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
483     this.serviceRegistry = serviceRegistry;
484   }
485 
486   /**
487    * @param hostname of server to find in list
488    * @param servers, list of known servers
489    */
490   private Optional<HostRegistration> findServerByHost(String hostname, List<HostRegistration> servers) {
491     return servers.stream().filter(o -> o.getBaseUrl().equals(hostname)).findFirst();
492   }
493 }