/**
* 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.runtimeinfo;
import static org.opencastproject.rest.RestConstants.SERVICES_FILTER;
import org.opencastproject.rest.RestConstants;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.Role;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.systems.MatterhornConstants;
import org.opencastproject.userdirectory.UserIdRoleProvider;
import org.opencastproject.util.UrlSupport;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
/**
* This REST endpoint provides information about the runtime environment, including the services and user interfaces
* deployed and the current login context.
*
* If the 'org.opencastproject.anonymous.feedback.url' is set in config.properties, this service will also update the
* opencast project with the contents of the getRuntimeInfo() json feed.
*/
@Path("/")
@RestService(
name = "RuntimeInfo",
title = "Runtime Information",
abstractText = "This service provides information about the runtime environment, including the servives that are "
+ "deployed and the current user context.",
notes = {})
public class RuntimeInfo {
private static final Logger logger = LoggerFactory.getLogger(RuntimeInfo.class);
/** Configuration properties id */
private static final String ADMIN_URL_PROPERTY = "org.opencastproject.admin.ui.url";
private static final String ENGAGE_URL_PROPERTY = "org.opencastproject.engage.ui.url";
/**
* The rest publisher looks for any non-servlet with the 'opencast.service.path' property
*/
public static final String SERVICE_FILTER = "(&(!(objectClass=javax.servlet.Servlet))("
+ RestConstants.SERVICE_PATH_PROPERTY + "=*))";
private UserIdRoleProvider userIdRoleProvider;
private SecurityService securityService;
private BundleContext bundleContext;
private URL serverUrl;
protected void setUserIdRoleProvider(UserIdRoleProvider userIdRoleProvider) {
this.userIdRoleProvider = userIdRoleProvider;
}
protected void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
protected ServiceReference[] getRestServiceReferences() throws InvalidSyntaxException {
return bundleContext.getAllServiceReferences(null, SERVICES_FILTER);
}
protected ServiceReference[] getUserInterfaceServiceReferences() throws InvalidSyntaxException {
return bundleContext.getAllServiceReferences(Servlet.class.getName(), "(&(alias=*)(classpath=*))");
}
public void activate(ComponentContext cc) throws MalformedURLException {
logger.debug("start()");
this.bundleContext = cc.getBundleContext();
serverUrl = new URL(bundleContext.getProperty(MatterhornConstants.SERVER_URL_PROPERTY));
}
public void deactivate() {
// Nothing to do
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("components.json")
@RestQuery(name = "services", description = "List the REST services and user interfaces running on this host", reponses = { @RestResponse(description = "The components running on this host", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "")
@SuppressWarnings("unchecked")
public String getRuntimeInfo(@Context HttpServletRequest request) throws MalformedURLException {
Organization organization = securityService.getOrganization();
// Get request protocol and port
String targetScheme = request.getScheme();
// Create the engage target URL
URL targetEngageBaseUrl = null;
String orgEngageBaseUrl = organization.getProperties().get(ENGAGE_URL_PROPERTY);
if (StringUtils.isNotBlank(orgEngageBaseUrl)) {
try {
targetEngageBaseUrl = new URL(orgEngageBaseUrl);
} catch (MalformedURLException e) {
logger.warn("Engage url '{}' of organization '{}' is malformed", orgEngageBaseUrl, organization.getId());
}
}
if (targetEngageBaseUrl == null) {
logger.debug(
"Using 'org.opencastproject.server.url' as a fallback for the non-existing organization level key '{}' for the components.json response",
ENGAGE_URL_PROPERTY);
targetEngageBaseUrl = new URL(targetScheme, serverUrl.getHost(), serverUrl.getPort(), serverUrl.getFile());
}
// Create the admin target URL
URL targetAdminBaseUrl = null;
String orgAdminBaseUrl = organization.getProperties().get(ADMIN_URL_PROPERTY);
if (StringUtils.isNotBlank(orgAdminBaseUrl)) {
try {
targetAdminBaseUrl = new URL(orgAdminBaseUrl);
} catch (MalformedURLException e) {
logger.warn("Admin url '{}' of organization '{}' is malformed", orgAdminBaseUrl, organization.getId());
}
}
if (targetAdminBaseUrl == null) {
logger.debug(
"Using 'org.opencastproject.server.url' as a fallback for the non-existing organization level key '{}' for the components.json response",
ADMIN_URL_PROPERTY);
targetAdminBaseUrl = new URL(targetScheme, serverUrl.getHost(), serverUrl.getPort(), serverUrl.getFile());
}
JSONObject json = new JSONObject();
json.put("engage", targetEngageBaseUrl.toString());
json.put("admin", targetAdminBaseUrl.toString());
json.put("rest", getRestEndpointsAsJson(request));
json.put("ui", getUserInterfacesAsJson());
return json.toJSONString();
}
@GET
@Path("me.json")
@Produces(MediaType.APPLICATION_JSON)
@RestQuery(name = "me", description = "Information about the curent user", reponses = { @RestResponse(description = "Returns information about the current user", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "")
@SuppressWarnings("unchecked")
public String getMyInfo() {
JSONObject json = new JSONObject();
User user = securityService.getUser();
JSONObject jsonUser = new JSONObject();
jsonUser.put("username", user.getUsername());
jsonUser.put("name", user.getName());
jsonUser.put("email", user.getEmail());
jsonUser.put("provider", user.getProvider());
json.put("user", jsonUser);
if (userIdRoleProvider != null)
json.put("userRole", UserIdRoleProvider.getUserIdRole(user.getUsername()));
// Add the current user's roles
JSONArray roles = new JSONArray();
for (Role role : user.getRoles()) {
roles.add(role.getName());
}
json.put("roles", roles);
// Add the current user's organizational information
Organization org = securityService.getOrganization();
JSONObject jsonOrg = new JSONObject();
jsonOrg.put("id", org.getId());
jsonOrg.put("name", org.getName());
jsonOrg.put("adminRole", org.getAdminRole());
jsonOrg.put("anonymousRole", org.getAnonymousRole());
// and organization properties
JSONObject orgProps = new JSONObject();
jsonOrg.put("properties", orgProps);
for (Entry<String, String> entry : org.getProperties().entrySet()) {
orgProps.put(entry.getKey(), entry.getValue());
}
json.put("org", jsonOrg);
return json.toJSONString();
}
@SuppressWarnings("unchecked")
protected JSONArray getRestEndpointsAsJson(HttpServletRequest request) throws MalformedURLException {
JSONArray json = new JSONArray();
ServiceReference[] serviceRefs = null;
try {
serviceRefs = getRestServiceReferences();
} catch (InvalidSyntaxException e) {
e.printStackTrace();
}
if (serviceRefs == null)
return json;
for (ServiceReference servletRef : sort(serviceRefs)) {
String version = servletRef.getBundle().getVersion().toString();
String description = (String) servletRef.getProperty(Constants.SERVICE_DESCRIPTION);
String type = (String) servletRef.getProperty(RestConstants.SERVICE_TYPE_PROPERTY);
String servletContextPath = (String) servletRef.getProperty(RestConstants.SERVICE_PATH_PROPERTY);
JSONObject endpoint = new JSONObject();
endpoint.put("description", description);
endpoint.put("version", version);
endpoint.put("type", type);
URL url = new URL(request.getScheme(), request.getServerName(), request.getServerPort(), servletContextPath);
endpoint.put("path", servletContextPath);
endpoint.put("docs", UrlSupport.concat(url.toExternalForm(), "/docs")); // This is a Matterhorn convention
endpoint.put("wadl", UrlSupport.concat(url.toExternalForm(), "/?_wadl&_type=xml")); // This triggers a
json.add(endpoint);
}
return json;
}
@SuppressWarnings("unchecked")
protected JSONArray getUserInterfacesAsJson() {
JSONArray json = new JSONArray();
ServiceReference[] serviceRefs = null;
try {
serviceRefs = getUserInterfaceServiceReferences();
} catch (InvalidSyntaxException e) {
e.printStackTrace();
}
if (serviceRefs == null)
return json;
for (ServiceReference ref : sort(serviceRefs)) {
String description = (String) ref.getProperty(Constants.SERVICE_DESCRIPTION);
String version = ref.getBundle().getVersion().toString();
String alias = (String) ref.getProperty("alias");
String welcomeFile = (String) ref.getProperty("welcome.file");
String welcomePath = "/".equals(alias) ? alias + welcomeFile : alias + "/" + welcomeFile;
JSONObject endpoint = new JSONObject();
endpoint.put("description", description);
endpoint.put("version", version);
endpoint.put("welcomepage", serverUrl + welcomePath);
json.add(endpoint);
}
return json;
}
/**
* Returns the array of references sorted by their Constants.SERVICE_DESCRIPTION property.
*
* @param references
* the referencens
* @return the sorted set of references
*/
protected static SortedSet<ServiceReference> sort(ServiceReference[] references) {
// Sort the service references
SortedSet<ServiceReference> sortedServiceRefs = new TreeSet<ServiceReference>(new Comparator<ServiceReference>() {
@Override
public int compare(ServiceReference o1, ServiceReference o2) {
String o1Description = (String) o1.getProperty(Constants.SERVICE_DESCRIPTION);
if (StringUtils.isBlank(o1Description))
o1Description = o1.toString();
String o2Description = (String) o2.getProperty(Constants.SERVICE_DESCRIPTION);
if (StringUtils.isBlank(o2Description))
o2Description = o2.toString();
return o1Description.compareTo(o2Description);
}
});
sortedServiceRefs.addAll(Arrays.asList(references));
return sortedServiceRefs;
}
}