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