edu.ucsf.vitro.opensocial.OpenSocialSmokeTests Maven / Gradle / Ivy
The newest version!
/* $This file is distributed under the terms of the license in LICENSE$ */
package edu.ucsf.vitro.opensocial;
import java.io.File;
import java.io.InputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import edu.cornell.mannlib.vitro.webapp.utils.http.HttpClientFactory;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus;
import edu.cornell.mannlib.vitro.webapp.utils.threads.VitroBackgroundThread;
import org.apache.http.util.EntityUtils;
/**
* Do some quick checks to see whether the OpenSocial stuff is configured and
* working.
*/
public class OpenSocialSmokeTests implements ServletContextListener {
private static final String PROPERTY_SHINDIG_URL = "OpenSocial.shindigURL";
private static final String PROPERTY_SHINDIG_TOKEN_KEY_FILE = "OpenSocial.tokenKeyFile";
private static final String PROPERTY_SHINDIG_TOKEN_SERVICE = "OpenSocial.tokenService";
private static final String PROPERTY_DB_DRIVER = "VitroConnection.DataSource.driver";
private static final String PROPERTY_DB_JDBC_URL = "VitroConnection.DataSource.url";
private static final String PROPERTY_DB_USERNAME = "VitroConnection.DataSource.username";
private static final String PROPERTY_DB_PASSWORD = "VitroConnection.DataSource.password";
private static final String FILENAME_SHINDIG_PROPERTIES = "shindigorng.properties";
/*
* If a connection fails in the tester thread, how long do we wait before
* trying again?
*/
private static final long SLEEP_INTERVAL = 20000; // 20 seconds
private ServletContext ctx;
private ConfigurationProperties configProps;
private List warnings = new ArrayList();
private String shindigBaseUrl;
private String tokenServiceHost;
private int tokenServicePort;
/**
* When the system starts up, run the tests.
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
ctx = sce.getServletContext();
StartupStatus ss = StartupStatus.getBean(ctx);
configProps = ConfigurationProperties.getBean(ctx);
/*
* If OpenSocial is not configured in runtime.properties, skip the
* tests.
*/
if (!configurationPresent()) {
ss.info(this, "The OpenSocial connection is not configured.");
return;
}
/*
* Run all of the non-threaded tests. If any fail, skip the threaded
* tests.
*/
checkDatabaseTables();
checkShindigConfigFile();
checkTokenKeyFile();
checkTokenServiceInfo();
if (!warnings.isEmpty()) {
for (Warning w : warnings) {
w.warn(ss);
}
return;
}
/*
* Run the threaded tests.
*/
ss.info(this, "Starting threads for OpenSocial smoke tests");
new ShindigTestThread(this, ss, shindigBaseUrl).start();
new TokenServiceTestThread(this, ss, tokenServiceHost, tokenServicePort)
.start();
}
/**
* Get the base URL for the Shindig server. If none, then the whole thing is
* disabled.
*/
private boolean configurationPresent() {
String shindigUrl = configProps.getProperty(PROPERTY_SHINDIG_URL);
if (StringUtils.isNotEmpty(shindigUrl)) {
this.shindigBaseUrl = shindigUrl;
return true;
} else {
return false;
}
}
/**
* Check that we can connect to the database, and query one of the Shindig
* tables.
*/
private void checkDatabaseTables() {
BasicDataSource dataSource = null;
Connection conn = null;
Statement stmt = null;
ResultSet rset = null;
try {
dataSource = new BasicDataSource();
dataSource.setDriverClassName(getProperty(PROPERTY_DB_DRIVER));
dataSource.setUrl(getProperty(PROPERTY_DB_JDBC_URL));
dataSource.setUsername(getProperty(PROPERTY_DB_USERNAME));
dataSource.setPassword(getProperty(PROPERTY_DB_PASSWORD));
conn = dataSource.getConnection();
stmt = conn.createStatement();
rset = stmt.executeQuery("select * from orng_apps");
} catch (NoSuchPropertyException e) {
warnings.add(new Warning(e.getMessage()));
} catch (SQLException e) {
if (e.getMessage().contains("doesn't exist")) {
warnings.add(new Warning("The Shindig tables don't exist "
+ "in the database. Was shindig_orng_tables.sql "
+ "run to set them up?", e));
} else {
warnings.add(new Warning(
"Can't access the Shindig database tables", e));
}
} finally {
try {
if (rset != null) {
rset.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (stmt != null) {
stmt.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Check that the Shindig configuration file is present in the classpath.
*/
private void checkShindigConfigFile() {
URL url = this.getClass()
.getResource("/" + FILENAME_SHINDIG_PROPERTIES);
if (url == null) {
String message = "Can't find the '" + FILENAME_SHINDIG_PROPERTIES
+ "' file in the classpath. ";
message += "Has the Tomcat classpath been set to include the "
+ "Shindig config directory? "
+ "(inside the Vitro home directory) ";
message += "Was the openSocial build script run? ('ant orng')";
warnings.add(new Warning(message));
}
}
/**
* Check that the Token Key file has been specified in runtime.properties,
* and that it actually does exist.
*/
private void checkTokenKeyFile() {
try {
String tokenFilename = getProperty(PROPERTY_SHINDIG_TOKEN_KEY_FILE);
File tokenFile = new File(tokenFilename);
if (!tokenFile.exists()) {
warnings.add(new Warning(
"Token key file for Shindig does not exist: '"
+ tokenFilename + "'"));
} else if (!tokenFile.isFile()) {
warnings.add(new Warning(
"Token key file for Shindig is not a file: '"
+ tokenFilename + "'"));
}
} catch (NoSuchPropertyException e) {
warnings.add(new Warning(e.getMessage()));
}
}
/**
* Get the Token Service info from runtime.properties. It must be in the
* form of host:port, and may not refer to localhost.
*/
private void checkTokenServiceInfo() {
String tsInfo = configProps.getProperty(PROPERTY_SHINDIG_TOKEN_SERVICE);
if (StringUtils.isEmpty(tsInfo)) {
warnings.add(new Warning("There is no value for '"
+ PROPERTY_SHINDIG_TOKEN_SERVICE
+ "' in runtime.properties"));
return;
}
/*
* If the parameter is invalid, use this message.
*/
String warningText = "The '" + PROPERTY_SHINDIG_TOKEN_SERVICE
+ "' parameter is set to \"" + tsInfo
+ "\". It must be in the form [hostname]:[port]. "
+ "For example, \"myhost.mydomain.edu:8777\". "
+ "The hostname may be an IP address, "
+ "but it may not be \"localhost\" or \"127.0.0.1\"";
int firstColon = tsInfo.indexOf(':');
if (firstColon <= 0) {
warnings.add(new Warning(warningText));
return;
}
int lastColon = tsInfo.lastIndexOf(':');
if (firstColon != lastColon) {
warnings.add(new Warning(warningText));
return;
}
tokenServiceHost = tsInfo.substring(0, firstColon);
if (("localhost".equals(tokenServiceHost))
|| ("127.0.0.1".equals(tokenServiceHost))) {
warnings.add(new Warning(warningText));
return;
}
try {
tokenServicePort = Integer.parseInt(tsInfo
.substring(firstColon + 1));
} catch (Exception e) { // probably a NumberFormatException
warnings.add(new Warning(warningText, e));
}
}
private String getProperty(String key) throws NoSuchPropertyException {
String value = configProps.getProperty(key);
if (StringUtils.isEmpty(value)) {
throw new NoSuchPropertyException(key);
} else {
return value;
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// nothing to destroy
}
// ----------------------------------------------------------------------
// Helper classes
// ----------------------------------------------------------------------
private static class NoSuchPropertyException extends Exception {
NoSuchPropertyException(String key) {
super("There is no value for '" + key + "' in build.properties");
}
}
private class Warning {
private final String message;
private final Throwable cause;
Warning(String message) {
this.message = message;
this.cause = null;
}
Warning(String message, Throwable cause) {
this.message = message;
this.cause = cause;
}
void warn(StartupStatus ss) {
if (cause == null) {
ss.warning(OpenSocialSmokeTests.this, message);
} else {
ss.warning(OpenSocialSmokeTests.this, message, cause);
}
}
}
private static class ShindigTestThread extends VitroBackgroundThread {
private final OpenSocialSmokeTests listener;
private final StartupStatus ss;
private final String shindigBaseUrl;
public ShindigTestThread(OpenSocialSmokeTests listener,
StartupStatus ss, String shindigBaseUrl) {
super("OpenSocialSmokeTest.ShindigTestThread");
this.listener = listener;
this.ss = ss;
this.shindigBaseUrl = shindigBaseUrl;
}
@Override
public void run() {
try {
new ShindigTester(shindigBaseUrl).connect();
ss.info(listener, "Shindig service responds to a REST query.");
} catch (ShindigTesterException e) {
String message = e.getMessage();
Throwable cause = e.getCause();
if (cause == null) {
ss.warning(listener, message);
} else {
ss.warning(listener, message, cause);
}
}
}
}
private static class ShindigTester {
// Use the parent's log
private static final Log log = LogFactory
.getLog(OpenSocialSmokeTests.class);
/** Pretend that there is an HTTP status code for this. */
private static final int SOCKET_TIMEOUT_STATUS = -500;
private final String shindigBaseUrl;
private final String shindigTestUrl;
private final HttpClient httpClient = HttpClientFactory.getHttpClient();
private int statusCode = Integer.MIN_VALUE;
public ShindigTester(String shindigBaseUrl) {
this.shindigBaseUrl = shindigBaseUrl;
this.shindigTestUrl = shindigBaseUrl + "/rest/appdata";
}
public void connect() throws ShindigTesterException {
testConnection();
if (!isDone()) {
sleep();
testConnection();
}
if (!isDone()) {
sleep();
testConnection();
}
if (statusCode != HttpStatus.SC_OK) {
throw new ShindigTesterException(statusCode, shindigBaseUrl,
shindigTestUrl);
}
}
private void testConnection() throws ShindigTesterException {
HttpGet method = new HttpGet(shindigTestUrl);
try {
log.debug("Trying to connect to Shindig");
HttpResponse response = httpClient.execute(method);
try {
statusCode = response.getStatusLine().getStatusCode();
log.debug("HTTP status was " + statusCode);
} finally {
EntityUtils.consume(response.getEntity());
}
} catch (SocketTimeoutException e) {
// Catch the exception so we can retry this.
// Save the status so we know why we failed.
statusCode = SOCKET_TIMEOUT_STATUS;
} catch (Exception e) {
throw new ShindigTesterException(e, shindigBaseUrl,
shindigTestUrl);
}
}
/**
* Stop trying to connect if we succeed, or if we receive an error that
* won't change on retry.
*/
private boolean isDone() {
return (statusCode == HttpStatus.SC_OK)
|| (statusCode == HttpStatus.SC_FORBIDDEN);
}
private void sleep() {
try {
Thread.sleep(SLEEP_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace(); // Should never happen
}
}
}
protected static class ShindigTesterException extends Exception {
private final int httpStatusCode;
protected ShindigTesterException(Integer httpStatusCode,
String baseUrl, String testUrl) {
super("Failed to connect to the Shindig service at '" + baseUrl
+ "' (tried for a response at '" + testUrl + "'). "
+ "status code was " + httpStatusCode);
this.httpStatusCode = httpStatusCode;
}
protected ShindigTesterException(Throwable cause, String baseUrl,
String testUrl) {
super("Failed to connect to the Shindig service at '" + baseUrl
+ "' (tried for a response at '" + testUrl + "').", cause);
this.httpStatusCode = Integer.MIN_VALUE;
}
protected int getHttpStatusCode() {
return httpStatusCode;
}
}
private static class TokenServiceTestThread extends VitroBackgroundThread {
private final OpenSocialSmokeTests listener;
private final StartupStatus ss;
private final String tokenServiceHost;
private final int tokenServicePort;
public TokenServiceTestThread(OpenSocialSmokeTests listener,
StartupStatus ss, String tokenServiceHost, int tokenServicePort) {
super("OpenSocialSmokeTest.TokenServiceTestThread");
this.listener = listener;
this.ss = ss;
this.tokenServiceHost = tokenServiceHost;
this.tokenServicePort = tokenServicePort;
}
@Override
public void run() {
try {
new TokenServiceTester(tokenServiceHost, tokenServicePort)
.connect();
ss.info(listener,
"Shindig security token service responds to a request.");
} catch (TokenServiceTesterException e) {
String message = e.getMessage();
Throwable cause = e.getCause();
if (cause == null) {
ss.warning(listener, message);
} else {
ss.warning(listener, message, cause);
}
}
}
}
protected static class TokenServiceTester {
// Use the parent's log
private static final Log log = LogFactory
.getLog(OpenSocialSmokeTests.class);
private final String host;
private final int port;
private Object problem;
public TokenServiceTester(String host, int port) {
this.host = host;
this.port = port;
}
public void connect() throws TokenServiceTesterException {
testConnection();
if (!isDone()) {
sleep();
testConnection();
}
if (!isDone()) {
sleep();
testConnection();
}
if (problem instanceof Throwable) {
throw new TokenServiceTesterException(
"Test of the Shindig token service failed.",
(Throwable) problem);
} else if (problem instanceof String) {
throw new TokenServiceTesterException((String) problem);
}
}
private void testConnection() {
try {
log.debug("Connecting to the token service");
Socket s = new Socket(host, port);
try {
s.getOutputStream().write("c=default\n".getBytes());
int byteCount = 0;
int totalBytecount = 0;
byte[] buffer = new byte[8192];
// The following will block until the page is transmitted.
InputStream inputStream = s.getInputStream();
while ((byteCount = inputStream.read(buffer)) > 0) {
totalBytecount += byteCount;
}
if (totalBytecount == 0) {
log.debug("Received an empty response.");
problem = "The Shindig security token service responded to a test, but the response was empty.";
} else {
log.debug("Recieved the token.");
problem = null;
}
} finally {
s.close();
}
} catch (Exception e) {
log.debug("Problem with the token service", e);
problem = e;
}
}
/**
* Stop trying to connect if we succeed, or if we receive an error that
* won't change on retry.
*/
private boolean isDone() {
return ((problem == null) || (problem instanceof String));
}
private void sleep() {
try {
Thread.sleep(SLEEP_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace(); // Should never happen
}
}
}
protected static class TokenServiceTesterException extends Exception {
protected TokenServiceTesterException(String message) {
super(message);
}
protected TokenServiceTesterException(String message, Throwable cause) {
super(message, cause);
}
}
}