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