org.apache.usergrid.cassandra.CassandraResource.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.usergrid.cassandra.CassandraResource.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.apache.usergrid.cassandra;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.Properties;

import org.junit.rules.ExternalResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.yaml.snakeyaml.Yaml;

import org.apache.cassandra.service.CassandraDaemon;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.math.RandomUtils;

/**
 * A JUnit {@link org.junit.rules.ExternalResource} designed to start up Cassandra once in a TestSuite or test Class as
 * a shared external resource across test cases and shut it down after the TestSuite has completed.
 * <p/>
 * This external resource is completely isolated in terms of the files used and the ports selected if the {@link
 * AvailablePortFinder} is used with it.
 * <p/>
 * Note that for this resource to work properly, a project.properties file must be placed in the src/test/resources
 * directory with the following stanza in the project pom's build section:
 * <p/>
 * <testResources> <testResource> <directory>src/test/resources</directory> <filtering>true</filtering> <includes>
 * <include>**\/*.properties</include> <include>**\/*.xml</include> </includes> </testResource> </testResources>
 * <p/>
 * The following property expansion macro should be placed in this project.properties file:
 * <p/>
 * target.directory=${pom.build.directory}
 */
public class CassandraResource extends ExternalResource {
    public static final Logger LOG = LoggerFactory.getLogger(CassandraResource.class);
    public static final int DEFAULT_RPC_PORT = 9160;
    public static final int DEFAULT_STORAGE_PORT = 7000;
    public static final int DEFAULT_SSL_STORAGE_PORT = 7001;
    public static final int DEFAULT_NATIVE_TRANSPORT_PORT = 9042;

    public static final String PROPERTIES_FILE = "project.properties";
    public static final String TARGET_DIRECTORY_KEY = "target.directory";
    public static final String DATA_FILE_DIR_KEY = "data_file_directories";
    public static final String COMMIT_FILE_DIR_KEY = "commitlog_directory";
    public static final String SAVED_CACHES_DIR_KEY = "saved_caches_directory";

    public static final String NATIVE_TRANSPORT_PORT_KEY = "native_transport_port";
    public static final String RPC_PORT_KEY = "rpc_port";
    public static final String STORAGE_PORT_KEY = "storage_port";
    public static final String SSL_STORAGE_PORT_KEY = "ssl_storage_port";

    private static final Object lock = new Object();

    private final File tempDir;
    private final String schemaManagerName;

    private boolean initialized = false;
    private int rpcPort = DEFAULT_RPC_PORT;
    private int storagePort = DEFAULT_STORAGE_PORT;
    private int sslStoragePort = DEFAULT_SSL_STORAGE_PORT;
    private int nativeTransportPort = DEFAULT_NATIVE_TRANSPORT_PORT;

    private ConfigurableApplicationContext applicationContext;
    private CassandraDaemon cassandraDaemon;
    private SchemaManager schemaManager;

    private static CassandraResource instance;
    private Thread shutdown;

    /**
     * Creates a Cassandra starting ExternalResource for JUnit test cases which uses the default SchemaManager for
     * Cassandra.
     */
    @SuppressWarnings("UnusedDeclaration")
    CassandraResource() throws IOException {
        this(null, DEFAULT_RPC_PORT, DEFAULT_STORAGE_PORT, DEFAULT_SSL_STORAGE_PORT, DEFAULT_NATIVE_TRANSPORT_PORT);
    }

    /**
     * Creates a Cassandra starting ExternalResource for JUnit test cases which uses the specified SchemaManager for
     * Cassandra.
     */
    CassandraResource(String schemaManagerName, int rpcPort, int storagePort, int sslStoragePort,
            int nativeTransportPort) {
        LOG.info("Creating CassandraResource using {} for the ClassLoader.",
                Thread.currentThread().getContextClassLoader());

        this.schemaManagerName = schemaManagerName;
        this.rpcPort = rpcPort;
        this.storagePort = storagePort;
        this.sslStoragePort = sslStoragePort;
        this.nativeTransportPort = nativeTransportPort;

        try {
            this.tempDir = getTempDirectory();
        } catch (IOException e) {
            LOG.error("Failed to create temporary directory for Cassandra instance.", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates a Cassandra starting ExternalResource for JUnit test cases which uses the specified SchemaManager for
     * Cassandra.
     */
    CassandraResource(int rpcPort, int storagePort, int sslStoragePort, int nativeTransportPort)
            throws IOException {
        this(null, rpcPort, storagePort, sslStoragePort, nativeTransportPort);
    }

    /**
     * Gets the rpcPort for Cassandra.
     *
     * @return the rpc_port (in yaml file) used by Cassandra
     */
    public int getRpcPort() {
        return rpcPort;
    }

    /**
     * Gets the storagePort for Cassandra.
     *
     * @return the storage_port (in yaml file) used by Cassandra
     */
    public int getStoragePort() {
        return storagePort;
    }

    /**
     * Gets the sslStoragePort for Cassandra.
     *
     * @return the sslStoragePort
     */
    public int getSslStoragePort() {
        return sslStoragePort;
    }

    public int getNativeTransportPort() {
        return nativeTransportPort;
    }

    /**
     * Gets the temporary directory created for this CassandraResource.
     *
     * @return the temporary directory
     */
    @SuppressWarnings("UnusedDeclaration")
    public File getTemporaryDirectory() {
        return tempDir;
    }

    /**
     * Gets a bean from the application context.
     *
     * @param requiredType the type of the bean
     * @param <T> the type of the bean
     *
     * @return the bean
     */
    public <T> T getBean(String name, Class<T> requiredType) {
        protect();
        return applicationContext.getBean(name, requiredType);
    }

    /**
     * Gets a bean from the application context.
     *
     * @param requiredType the type of the bean
     * @param <T> the type of the bean
     *
     * @return the bean
     */
    public <T> T getBean(Class<T> requiredType) {
        protect();
        return applicationContext.getBean(requiredType);
    }

    /**
     * Gets whether this resource is ready to use.
     *
     * @return true if ready to use, false otherwise
     */
    public boolean isReady() {
        return initialized;
    }

    @Override
    public String toString() {

        return "\n" + "cassandra.yaml = " + new File(tempDir, "cassandra.yaml") + "\n" + RPC_PORT_KEY + " = "
                + rpcPort + "\n" + STORAGE_PORT_KEY + " = " + storagePort + "\n" + SSL_STORAGE_PORT_KEY + " = "
                + sslStoragePort + "\n" + NATIVE_TRANSPORT_PORT_KEY + " = " + nativeTransportPort + "\n"
                + DATA_FILE_DIR_KEY + " = " + new File(tempDir, "data").toString() + "\n" + COMMIT_FILE_DIR_KEY
                + " = " + new File(tempDir, "commitlog").toString() + "\n" + SAVED_CACHES_DIR_KEY + " = "
                + new File(tempDir, "saved_caches").toString() + "\n";
    }

    /**
     * Protects against IDE or command line runs of a unit test which when starting the test outside of the Suite will
     * not start the resource. This makes sure the resource is automatically started on a usage attempt.
     */
    private void protect() {
        if (!isReady()) {
            try {
                before();
            } catch (Throwable t) {
                LOG.error("Failed to start up Cassandra.", t);
                throw new RuntimeException(t);
            }
        }
    }

    /**
     * Starts up Cassandra before TestSuite or test Class execution.
     *
     * @throws Throwable if we cannot start up Cassandra
     */
    @Override
    protected void before() throws Throwable {
        /*
         * Note that the lock is static so it is JVM wide which prevents other
         * instances of this class from making changes to the Cassandra system
         * properties while we are initializing Cassandra with unique settings.
         */

        synchronized (lock) {
            super.before();

            if (isReady()) {
                return;
            }

            LOG.info("Initializing Cassandra at {} ...", tempDir.toString());

            // Create temp directory, setup to create new File configuration there
            File newYamlFile = new File(tempDir, "cassandra.yaml");
            URL newYamlUrl = FileUtils.toURLs(new File[] { newYamlFile })[0];

            // Read the original yaml file, make changes, and dump to new position in tmpdir
            Yaml yaml = new Yaml();
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) yaml
                    .load(ClassLoader.getSystemResourceAsStream("cassandra.yaml"));
            map.put(RPC_PORT_KEY, getRpcPort());
            map.put(STORAGE_PORT_KEY, getStoragePort());
            map.put(SSL_STORAGE_PORT_KEY, getSslStoragePort());
            map.put(NATIVE_TRANSPORT_PORT_KEY, getNativeTransportPort());
            map.put(COMMIT_FILE_DIR_KEY, new File(tempDir, "commitlog").toString());
            map.put(DATA_FILE_DIR_KEY, new String[] { new File(tempDir, "data").toString() });
            map.put(SAVED_CACHES_DIR_KEY, new File(tempDir, "saved_caches").toString());
            FileWriter writer = new FileWriter(newYamlFile);
            yaml.dump(map, writer);
            writer.flush();
            writer.close();

            // Fire up Cassandra by setting configuration to point to new yaml file
            System.setProperty("cassandra.url", "localhost:" + Integer.toString(rpcPort));
            System.setProperty("cassandra-foreground", "true");
            System.setProperty("log4j.defaultInitOverride", "true");
            System.setProperty("log4j.configuration", "log4j.properties");
            System.setProperty("cassandra.ring_delay_ms", "100");
            System.setProperty("cassandra.config", newYamlUrl.toString());

            //while ( !AvailablePortFinder.available( rpcPort ) || rpcPort == 9042 ) {
            // why previously has a or condition of rpc == 9042?
            while (!AvailablePortFinder.available(rpcPort)) {
                rpcPort++;
            }

            while (!AvailablePortFinder.available(storagePort)) {
                storagePort++;
            }

            while (!AvailablePortFinder.available(sslStoragePort)) {
                sslStoragePort++;
            }

            while (!AvailablePortFinder.available(nativeTransportPort)) {
                nativeTransportPort++;
            }

            System.setProperty("cassandra." + RPC_PORT_KEY, Integer.toString(rpcPort));
            System.setProperty("cassandra." + STORAGE_PORT_KEY, Integer.toString(storagePort));
            System.setProperty("cassandra." + SSL_STORAGE_PORT_KEY, Integer.toString(sslStoragePort));
            System.setProperty("cassandra." + NATIVE_TRANSPORT_PORT_KEY, Integer.toString(nativeTransportPort));

            LOG.info(
                    "before() test, setting system properties for ports : [rpc, storage, sslStoage, native] = [{}, {}, {}, {}]",
                    new Object[] { rpcPort, storagePort, sslStoragePort, nativeTransportPort });
            if (!newYamlFile.exists()) {
                throw new RuntimeException("Cannot find new Yaml file: " + newYamlFile);
            }

            cassandraDaemon = new CassandraDaemon();
            cassandraDaemon.activate();

            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    after();
                }
            });

            String[] locations = { "usergrid-test-context.xml" };
            applicationContext = new ClassPathXmlApplicationContext(locations);

            loadSchemaManager(schemaManagerName);
            initialized = true;
            LOG.info("External Cassandra resource at {} is ready!", tempDir.toString());
            lock.notifyAll();
        }
    }

    /** Stops Cassandra after a TestSuite or test Class executes. */
    @Override
    protected synchronized void after() {
        super.after();

        shutdown = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.currentThread().sleep(100L);
                } catch (InterruptedException e) {
                    e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
                }

                LOG.info("Shutting down Cassandra instance in {}", tempDir.toString());

                if (schemaManager != null) {
                    LOG.info("Destroying schemaManager...");
                    try {
                        schemaManager.destroy();
                    } catch (Exception e) {
                        LOG.error("Ignoring failures while dropping keyspaces: {}", e.getMessage());
                    }

                    LOG.info("SchemaManager destroyed...");
                }

                applicationContext.stop();
                LOG.info("ApplicationContext stopped...");

                try {
                    if (cassandraDaemon != null) {
                        LOG.info("Deactivating CassandraDaemon...");
                        cassandraDaemon.deactivate();
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        };

        shutdown.start();
    }

    /**
     * Loads the specified {@link SchemaManager} or the default manager if the manager name that is provided is null.
     *
     * @param schemaManagerName the name of the SchemaManager to load, or null
     */
    private void loadSchemaManager(String schemaManagerName) {
        if (!applicationContext.isActive()) {
            LOG.info("Restarting context...");
            applicationContext.refresh();
        }

        if (schemaManagerName != null) {
            LOG.info("Looking up SchemaManager impl: {}", schemaManagerName);
            this.schemaManager = applicationContext.getBean(schemaManagerName, SchemaManager.class);
        } else {
            LOG.info("The SchemaManager is not specified - using the default SchemaManager impl");
            this.schemaManager = applicationContext.getBean(SchemaManager.class);
        }

        schemaManager.create();
        schemaManager.populateBaseData();
    }

    /**
     * Creates a new instance of the CassandraResource with rpc and storage ports that may or may not be the default
     * ports. If either port is taken, an alternative free port is found.
     *
     * @param schemaManagerName the name of the schemaManager to use
     *
     * @return a new CassandraResource with possibly non-default ports
     */
    public static CassandraResource newWithAvailablePorts(String schemaManagerName) {
        // Uncomment to test for Surefire Failures
        // System.setSecurityManager( new NoExitSecurityManager( System.getSecurityManager() ) );

        synchronized (lock) {
            if (instance != null) {
                return instance;
            }

            int rpcPort = AvailablePortFinder
                    .getNextAvailable(CassandraResource.DEFAULT_RPC_PORT + RandomUtils.nextInt(1000));
            int storagePort = AvailablePortFinder
                    .getNextAvailable(CassandraResource.DEFAULT_STORAGE_PORT + RandomUtils.nextInt(1000));
            int sslStoragePort = AvailablePortFinder
                    .getNextAvailable(CassandraResource.DEFAULT_SSL_STORAGE_PORT + RandomUtils.nextInt(1000));
            int nativeTransportPort = AvailablePortFinder
                    .getNextAvailable(CassandraResource.DEFAULT_NATIVE_TRANSPORT_PORT + RandomUtils.nextInt(1000));

            if (rpcPort == storagePort) {
                storagePort++;
                storagePort = AvailablePortFinder.getNextAvailable(storagePort);
            }

            if (sslStoragePort == storagePort) {
                sslStoragePort++;
                sslStoragePort = AvailablePortFinder.getNextAvailable(sslStoragePort);
            }

            instance = new CassandraResource(schemaManagerName, rpcPort, storagePort, sslStoragePort,
                    nativeTransportPort);
            LOG.info("Created a new instance of CassandraResource: {}", instance);
            return instance;
        }
    }

    /**
     * Creates a new instance of the CassandraResource with rpc and storage ports that may or may not be the default
     * ports. If either port is taken, an alternative free port is found.
     *
     * @return a new CassandraResource with possibly non-default ports
     */
    public static CassandraResource newWithAvailablePorts() {
        return newWithAvailablePorts(null);
    }

    /**
     * Uses a project.properties file that Maven does substitution on to to replace the value of a property with the
     * path to the Maven build directory (a.k.a. target). It then uses this path to generate a random String which it
     * uses to append a path component to so a unique directory is selected. If already present it's deleted, then the
     * directory is created.
     *
     * @return a unique temporary directory
     *
     * @throws IOException if we cannot access the properties file
     */
    public static File getTempDirectory() throws IOException {
        File tmpdir;
        Properties props = new Properties();
        props.load(ClassLoader.getSystemResourceAsStream(PROPERTIES_FILE));
        File basedir = new File((String) props.get(TARGET_DIRECTORY_KEY));
        String comp = RandomStringUtils.randomAlphanumeric(7);
        tmpdir = new File(basedir, comp);

        if (tmpdir.exists()) {
            LOG.info("Deleting directory: {}", tmpdir);
            FileUtils.forceDelete(tmpdir);
        } else {
            LOG.info("Creating temporary directory: {}", tmpdir);
            FileUtils.forceMkdir(tmpdir);
        }

        return tmpdir;
    }
}