edu.ucsf.vitro.opensocial.OpenSocialSmokeTests.java Source code

Java tutorial

Introduction

Here is the source code for edu.ucsf.vitro.opensocial.OpenSocialSmokeTests.java

Source

/* $This file is distributed under the terms of the license in /doc/license.txt$ */

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 org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.lang.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.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

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;

/**
 * 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<Warning> warnings = new ArrayList<Warning>();

    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);
                }
                return;
            }
        }
    }

    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 DefaultHttpClient httpClient = new DefaultHttpClient();

        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);
                statusCode = response.getStatusLine().getStatusCode();
                log.debug("HTTP status was " + statusCode);
            } 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);
                }
                return;
            }
        }
    }

    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);
        }
    }

}