/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.adminui.endpoint;
import static com.entwinemedia.fn.data.json.Jsons.f;
import static com.entwinemedia.fn.data.json.Jsons.obj;
import static com.entwinemedia.fn.data.json.Jsons.v;
import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
import org.opencastproject.index.service.resources.list.provider.ServersListProvider;
import org.opencastproject.index.service.resources.list.query.ServersListQuery;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.matterhorn.search.SearchQuery;
import org.opencastproject.matterhorn.search.SortCriterion;
import org.opencastproject.serviceregistry.api.HostRegistration;
import org.opencastproject.serviceregistry.api.ServiceRegistry;
import org.opencastproject.serviceregistry.api.ServiceStatistics;
import org.opencastproject.util.SmartIterator;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import com.entwinemedia.fn.data.json.JValue;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/")
@RestService(name = "ServerProxyService", title = "UI Servers",
abstractText = "This service provides the server data for the UI.",
notes = { "These Endpoints deliver informations about the server required for the UI.",
"<strong>Important:</strong> "
+ "<em>This service is for exclusive use by the module matterhorn-admin-ui-ng. Its API might change "
+ "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
+ "DO NOT use this for integration of third-party applications.<em>"})
public class ServerEndpoint {
private enum Sort {
COMPLETED, CORES, HOSTNAME, MAINTENANCE, MEANQUEUETIME, MEANRUNTIME, ONLINE, QUEUED, RUNNING
}
// List of property keys for the JSON job object
private static final String KEY_ONLINE = "online";
private static final String KEY_MAINTENANCE = "maintenance";
private static final String KEY_HOSTNAME = "hostname";
private static final String KEY_CORES = "cores";
private static final String KEY_RUNNING = "running";
private static final String KEY_COMPLETED = "completed";
private static final String KEY_QUEUED = "queued";
private static final String KEY_MEAN_RUN_TIME = "meanRunTime";
private static final String KEY_MEAN_QUEUE_TIME = "meanQueueTime";
/**
* Comparator for the servers list
*/
private class ServerComparator implements Comparator<JSONObject> {
private Sort sortType;
private Boolean ascending = true;
ServerComparator(Sort sortType, Boolean ascending) {
this.sortType = sortType;
this.ascending = ascending;
}
@Override
public int compare(JSONObject host1, JSONObject host2) {
int result;
switch (sortType) {
case ONLINE:
Boolean status1 = (Boolean) host1.get(KEY_ONLINE);
Boolean status2 = (Boolean) host2.get(KEY_ONLINE);
result = status1.compareTo(status2);
break;
case CORES:
result = ((Integer) host1.get(KEY_CORES)).compareTo((Integer) host2.get(KEY_CORES));
break;
case COMPLETED:
result = ((Long) host1.get(KEY_COMPLETED)).compareTo((Long) host2.get(KEY_COMPLETED));
break;
case QUEUED:
result = ((Integer) host1.get(KEY_QUEUED)).compareTo((Integer) host2.get(KEY_QUEUED));
break;
case MAINTENANCE:
Boolean mtn1 = (Boolean) host1.get(KEY_MAINTENANCE);
Boolean mtn2 = (Boolean) host2.get(KEY_MAINTENANCE);
result = mtn1.compareTo(mtn2);
break;
case RUNNING:
result = ((Integer) host1.get(KEY_RUNNING)).compareTo((Integer) host2.get(KEY_RUNNING));
break;
case MEANQUEUETIME:
result = ((Long) host1.get(KEY_MEAN_QUEUE_TIME)).compareTo((Long) host2.get(KEY_MEAN_QUEUE_TIME));
break;
case MEANRUNTIME:
result = ((Long) host1.get(KEY_MEAN_RUN_TIME)).compareTo((Long) host2.get(KEY_MEAN_RUN_TIME));
break;
case HOSTNAME:
default:
String name1 = (String) host1.get(KEY_HOSTNAME);
String name2 = (String) host2.get(KEY_HOSTNAME);
result = name1.compareTo(name2);
}
return ascending ? result : -1 * result;
}
}
private static final Logger logger = LoggerFactory.getLogger(ServerEndpoint.class);
public static final Response UNAUTHORIZED = Response.status(Response.Status.UNAUTHORIZED).build();
public static final Response NOT_FOUND = Response.status(Response.Status.NOT_FOUND).build();
public static final Response SERVER_ERROR = Response.serverError().build();
private ServiceRegistry serviceRegistry;
/** OSGi callback for the service registry. */
public void setServiceRegistry(ServiceRegistry serviceRegistry) {
this.serviceRegistry = serviceRegistry;
}
protected void activate(BundleContext bundleContext) {
logger.info("Activate job endpoint");
}
@GET
@Path("servers.json")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(description = "Returns the list of servers", name = "servers", restParameters = {
@RestParameter(name = "limit", description = "The maximum number of items to return per page", isRequired = false, type = INTEGER),
@RestParameter(name = "offset", description = "The offset", isRequired = false, type = INTEGER),
@RestParameter(name = "filter", description = "Filter results by hostname, status or free text query", isRequired = false, type = STRING),
@RestParameter(name = "sort", description = "The sort order. May include any "
+ "of the following: COMPLETED (jobs), CORES, HOSTNAME, MAINTENANCE, MEANQUEUETIME (mean for jobs), "
+ "MEANRUNTIME (mean for jobs), ONLINE, QUEUED (jobs), RUNNING (jobs)."
+ "The suffix must be :ASC for ascending or :DESC for descending sort order (e.g. HOSTNAME:DESC).", isRequired = false, type = STRING) },
reponses = { @RestResponse(description = "Returns the list of jobs from Matterhorn", responseCode = HttpServletResponse.SC_OK) },
returnDescription = "The list of servers")
public Response getServers(@QueryParam("limit") final int limit, @QueryParam("offset") final int offset,
@QueryParam("filter") String filter, @QueryParam("sort") String sort)
throws Exception {
ServersListQuery query = new ServersListQuery();
EndpointUtil.addRequestFiltersToQuery(filter, query);
query.setLimit(limit);
query.setOffset(offset);
List<JSONObject> servers = new ArrayList<>();
// Get service statistics for all hosts and services
List<ServiceStatistics> servicesStatistics = serviceRegistry.getServiceStatistics();
for (HostRegistration server : serviceRegistry.getHostRegistrations()) {
// Calculate statistics per server
long jobsCompleted = 0;
int jobsRunning = 0;
int jobsQueued = 0;
long sumMeanRuntime = 0;
long sumMeanQueueTime = 0;
int totalServiceOnHost = 0;
int offlineJobProducerServices = 0;
int totalJobProducerServices = 0;
Set<String> serviceTypes = new HashSet<>();
for (ServiceStatistics serviceStat : servicesStatistics) {
if (server.getBaseUrl().equals(serviceStat.getServiceRegistration().getHost())) {
totalServiceOnHost++;
jobsCompleted += serviceStat.getFinishedJobs();
jobsRunning += serviceStat.getRunningJobs();
jobsQueued += serviceStat.getQueuedJobs();
// mean time values are given in milliseconds,
// we should convert them to seconds,
// because the adminNG UI expect it in this format
sumMeanRuntime += TimeUnit.MILLISECONDS.toSeconds(serviceStat.getMeanRunTime());
sumMeanQueueTime += TimeUnit.MILLISECONDS.toSeconds(serviceStat.getMeanQueueTime());
if (!serviceStat.getServiceRegistration().isOnline()
&& serviceStat.getServiceRegistration().isJobProducer()) {
offlineJobProducerServices++;
totalJobProducerServices++;
} else if (serviceStat.getServiceRegistration().isJobProducer()) {
totalJobProducerServices++;
}
serviceTypes.add(serviceStat.getServiceRegistration().getServiceType());
}
}
long meanRuntime = totalServiceOnHost > 0 ? Math.round((double)sumMeanRuntime / totalServiceOnHost) : 0L;
long meanQueueTime = totalServiceOnHost > 0 ? Math.round((double)sumMeanQueueTime / totalServiceOnHost) : 0L;
boolean vOnline = server.isOnline();
boolean vMaintenance = server.isMaintenanceMode();
String vHostname = server.getBaseUrl();
int vCores = server.getCores();
if (query.getHostname().isSome()
&& !StringUtils.equalsIgnoreCase(vHostname, query.getHostname().get()))
continue;
if (query.getStatus().isSome()) {
if (StringUtils.equalsIgnoreCase(
ServersListProvider.SERVER_STATUS_ONLINE,
query.getStatus().get())
&& !vOnline)
continue;
if (StringUtils.equalsIgnoreCase(
ServersListProvider.SERVER_STATUS_OFFLINE,
query.getStatus().get())
&& vOnline)
continue;
if (StringUtils.equalsIgnoreCase(
ServersListProvider.SERVER_STATUS_MAINTENANCE,
query.getStatus().get())
&& !vMaintenance)
continue;
}
if (query.getFreeText().isSome()
&& !StringUtils.containsIgnoreCase(vHostname, query.getFreeText().get())
&& !StringUtils.containsIgnoreCase(server.getIpAddress(), query.getFreeText().get()))
continue;
JSONObject jsonServer = new JSONObject();
jsonServer.put(KEY_ONLINE, vOnline && offlineJobProducerServices <= totalJobProducerServices / 2);
jsonServer.put(KEY_MAINTENANCE, vMaintenance);
jsonServer.put(KEY_HOSTNAME, vHostname);
jsonServer.put(KEY_CORES, vCores);
jsonServer.put(KEY_RUNNING, jobsRunning);
jsonServer.put(KEY_QUEUED, jobsQueued);
jsonServer.put(KEY_COMPLETED, jobsCompleted);
jsonServer.put(KEY_MEAN_RUN_TIME, meanRuntime);
jsonServer.put(KEY_MEAN_QUEUE_TIME, meanQueueTime);
servers.add(jsonServer);
}
// Sorting
Sort sortKey = Sort.HOSTNAME;
Boolean ascending = true;
if (StringUtils.isNotBlank(sort)) {
try {
SortCriterion sortCriterion = RestUtils.parseSortQueryParameter(sort).iterator().next();
sortKey = Sort.valueOf(sortCriterion.getFieldName().toUpperCase());
ascending = SearchQuery.Order.Ascending == sortCriterion.getOrder()
|| SearchQuery.Order.None == sortCriterion.getOrder();
} catch (WebApplicationException ex) {
logger.warn("Failed to parse sort criterion \"{}\", invalid format.", new Object[] { sort });
} catch (IllegalArgumentException ex) {
logger.warn("Can not apply sort criterion \"{}\", no field with this name.", new Object[] { sort });
}
}
JSONArray jsonList = new JSONArray();
if (!servers.isEmpty()) {
Collections.sort(servers, new ServerComparator(sortKey, ascending));
jsonList.addAll(new SmartIterator(
query.getLimit().getOrElse(0),
query.getOffset().getOrElse(0))
.applyLimitAndOffset(servers));
}
return RestUtils.okJsonList(
getServersListAsJson(jsonList),
query.getOffset().getOrElse(0),
query.getLimit().getOrElse(0),
servers.size());
}
/**
* Transform each list item to JValue representation.
* @param servers list with servers JSONObjects
* @return servers list
*/
private List<JValue> getServersListAsJson(List<JSONObject> servers) {
List<JValue> jsonServers = new ArrayList<JValue>();
for (JSONObject server : servers) {
Boolean vOnline = (Boolean) server.get(KEY_ONLINE);
Boolean vMaintenance = (Boolean) server.get(KEY_MAINTENANCE);
String vHostname = (String) server.get(KEY_HOSTNAME);
Integer vCores = (Integer) server.get(KEY_CORES);
Integer vRunning = (Integer) server.get(KEY_RUNNING);
Integer vQueued = (Integer) server.get(KEY_QUEUED);
Long vCompleted = (Long) server.get(KEY_COMPLETED);
Long vMeanRunTime = (Long) server.get(KEY_MEAN_RUN_TIME);
Long vMeanQueueTime = (Long) server.get(KEY_MEAN_QUEUE_TIME);
jsonServers.add(obj(f(KEY_ONLINE, v(vOnline)),
f(KEY_MAINTENANCE, v(vMaintenance)),
f(KEY_HOSTNAME, v(vHostname)),
f(KEY_CORES, v(vCores)),
f(KEY_RUNNING, v(vRunning)),
f(KEY_QUEUED, v(vQueued)),
f(KEY_COMPLETED, v(vCompleted)),
f(KEY_MEAN_RUN_TIME, v(vMeanRunTime)),
f(KEY_MEAN_QUEUE_TIME, v(vMeanQueueTime))));
}
return jsonServers;
}
}