org.codehaus.mojo.cassandra.AbstractCassandraMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.mojo.cassandra.AbstractCassandraMojo.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.codehaus.mojo.cassandra;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.OS;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.project.MavenProject;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

/**
 * Base class for all the Cassandra Maven Plugin goals.
 *
 * @author stephenc
 */
public abstract class AbstractCassandraMojo extends AbstractMojo {
    /**
     * The directory to hold cassandra's database.
     *
     * @parameter default-value="${project.build.directory}/cassandra"
     * @required
     */
    protected File cassandraDir;

    /**
     * The enclosing project.
     *
     * @parameter default-value="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;

    /**
     * The directory containing generated classes.
     *
     * @parameter property="project.build.outputDirectory"
     * @required
     */
    private File classesDirectory;

    /**
     * The directory containing generated test classes.
     *
     * @parameter property="project.build.testOutputDirectory"
     * @required
     */
    private File testClassesDirectory;

    /**
     * Adds the test classpath to cassandra (for example you could use this when you have a custom comparator on your
     * test classpath.
     *
     * @parameter default-value="false"
     */
    protected boolean addTestClasspath;

    /**
     * Adds the main classpath to cassandra (for example you could use this when you have a custom comparator on your
     * main classpath.
     *
     * @parameter default-value="false"
     */
    protected boolean addMainClasspath;

    /**
     * Skip the execution.
     *
     * @parameter property="cassandra.skip" default-value="false"
     */
    protected boolean skip;

    /**
     * @parameter default-value="${plugin.artifacts}"
     * @readonly
     */
    private List<Artifact> pluginDependencies;

    /**
     * @parameter default-value="${plugin.pluginArtifact}"
     * @readonly
     */
    private Artifact pluginArtifact;

    /**
     * The current build session instance. This is used for toolchain manager API calls.
     *
     * @parameter default-value="${session}"
     * @required
     * @readonly
     */
    protected MavenSession session;

    /**
     * In yaml format any overrides or additional configuration that you want to apply to the server.
     *
     * @parameter
     */
    private String yaml;

    /**
     * Address to use for the RPC interface. Do not change this unless you really know what you are doing.
     *
     * @parameter default-value="127.0.0.1"
     */
    protected String rpcAddress;

    /**
     * Port to listen to for the RPC interface.
     *
     * @parameter property="cassandra.rpcPort" default-value="9160"
     */
    protected int rpcPort;

    /**
     * Port to listen to for the JMX interface.
     *
     * @parameter property="cassandra.jmxPort" default-value="7199"
     */
    protected int jmxPort;

    /**
     * Port on which the CQL native transport listens for clients.
     *
     * @parameter property="cassandra.nativeTransportPort" default-value="9042"
     * 
     * @since 2.0.0-1
     */
    protected int nativeTransportPort;

    /**
     * Enable or disable the native transport server. Currently, only the Thrift 
     * server is started by default because the native transport is considered beta.
     *
     * @parameter property="cassandra.startNativeTransport" default-value="false"
     * 
     * @since 2.0.0-1
     */
    protected boolean startNativeTransport;

    /**
     * Address to bind to and tell other Cassandra nodes to connect to. You
     * <strong>must</strong> change this if you want multiple nodes to be able to
     * communicate!
     * <p/>
     * Leaving it blank leaves it up to InetAddress.getLocalHost(). This
     * will always do the Right Thing <em>if</em> the node is properly configured
     * (hostname, name resolution, etc), and the Right Thing is to use the
     * address associated with the hostname (it might not be).
     * <p/>
     * Setting this to 0.0.0.0 is always wrong.
     * Do not change this unless you really know what you are doing.
     *
     * @parameter default-value="127.0.0.1"
     */
    protected String listenAddress;

    /**
     * Port to listen to for the Storage interface.
     *
     * @parameter property="cassandra.storagePort" default-value="7000"
     */
    protected int storagePort;

    /**
     * Port to listen to for receiving the stop command over
     *
     * @parameter property="cassandra.stopPort" default-value="8081"
     */
    protected int stopPort;

    /**
     * Key to be provided when stopping cassandra
     *
     * @parameter property="cassandra.stopKey" default-value="cassandra-maven-plugin"
     */
    protected String stopKey;

    /**
     * Number of megabytes to limit the cassandra JVM to.
     *
     * @parameter property="cassandra.maxMemory" default-value="512"
     */
    protected int maxMemory;

    /**
     * The keyspace against which individual operations will be executed
     *
     * @parameter property="cassandra.keyspace"
     */
    protected String keyspace;

    /**
     * List of System properties to pass to the JUnit tests.
     *
     * @parameter
     * @since 1.2.1-2
     */
    protected Map<String, String> systemPropertyVariables;

    /**
     * Log level of cassandra process. Logging is performed via log4j2.
     *
     * @parameter default-value="ERROR"
     * @since 3.5
     */
    protected String logLevel;

    /**
     * Create a jar with just a manifest containing a Main-Class entry for SurefireBooter and a Class-Path entry for
     * all classpath elements. Copied from surefire (ForkConfiguration#createJar())
     *
     * @param jarFile   The jar file to create/update
     * @param mainClass The main class to run.
     * @throws java.io.IOException if something went wrong.
     */
    protected void createCassandraJar(File jarFile, String mainClass) throws IOException {
        createCassandraJar(jarFile, mainClass, cassandraDir);
    }

    /**
     * Create a jar with just a manifest containing a Main-Class entry for SurefireBooter and a Class-Path entry for
     * all classpath elements. Copied from surefire (ForkConfiguration#createJar())
     *
     * @param jarFile   The jar file to create/update
     * @param mainClass The main class to run.
     * @throws java.io.IOException if something went wrong.
     */
    protected void createCassandraJar(File jarFile, String mainClass, File cassandraDir) throws IOException {
        File conf = new File(cassandraDir, "conf");
        FileOutputStream fos = null;
        JarOutputStream jos = null;
        try {
            fos = new FileOutputStream(jarFile);
            jos = new JarOutputStream(fos);
            jos.setLevel(JarOutputStream.STORED);
            jos.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));

            Manifest man = new Manifest();

            // we can't use StringUtils.join here since we need to add a '/' to
            // the end of directory entries - otherwise the jvm will ignore them.
            StringBuilder cp = new StringBuilder();
            cp.append(new URL(conf.toURI().toASCIIString()).toExternalForm());
            cp.append(' ');
            getLog().debug("Adding plugin artifact: " + ArtifactUtils.versionlessKey(pluginArtifact)
                    + " to the classpath");
            cp.append(new URL(pluginArtifact.getFile().toURI().toASCIIString()).toExternalForm());
            cp.append(' ');

            for (Artifact artifact : this.pluginDependencies) {
                getLog().debug("Adding plugin dependency artifact: " + ArtifactUtils.versionlessKey(artifact)
                        + " to the classpath");
                // NOTE: if File points to a directory, this entry MUST end in '/'.
                cp.append(new URL(artifact.getFile().toURI().toASCIIString()).toExternalForm());
                cp.append(' ');
            }

            if (addMainClasspath || addTestClasspath) {
                if (addTestClasspath) {
                    getLog().debug("Adding: " + testClassesDirectory + " to the classpath");
                    cp.append(new URL(testClassesDirectory.toURI().toASCIIString()).toExternalForm());
                    cp.append(' ');
                }
                if (addMainClasspath) {
                    getLog().debug("Adding: " + classesDirectory + " to the classpath");
                    cp.append(new URL(classesDirectory.toURI().toASCIIString()).toExternalForm());
                    cp.append(' ');
                }
                for (Artifact artifact : (Set<Artifact>) this.project.getArtifacts()) {
                    if ("jar".equals(artifact.getType()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope())
                            && (!Artifact.SCOPE_TEST.equals(artifact.getScope()) || addTestClasspath)) {
                        getLog().debug("Adding dependency: " + ArtifactUtils.versionlessKey(artifact)
                                + " to the classpath");
                        // NOTE: if File points to a directory, this entry MUST end in '/'.
                        cp.append(new URL(artifact.getFile().toURI().toASCIIString()).toExternalForm());
                        cp.append(' ');
                    }
                }
            }

            man.getMainAttributes().putValue("Manifest-Version", "1.0");
            man.getMainAttributes().putValue("Class-Path", cp.toString().trim());
            man.getMainAttributes().putValue("Main-Class", mainClass);

            man.write(jos);
        } finally {
            IOUtil.close(jos);
            IOUtil.close(fos);
        }
    }

    /**
     * Creates the cassandra home directory.
     *
     * @throws IOException if something goes wrong.
     */
    protected void createCassandraHome() throws IOException {
        createCassandraHome(cassandraDir, listenAddress, rpcAddress, null, new String[] { listenAddress });
    }

    /**
     * Creates the cassandra home directory.
     *
     * @param cassandraDir the cassandra home directory.
     * @throws IOException if something goes wrong.
     */
    protected void createCassandraHome(File cassandraDir, String listenAddress, String rpcAddress,
            BigInteger initialToken, String[] seeds) throws IOException {
        File bin = new File(cassandraDir, "bin");
        File conf = new File(cassandraDir, "conf");
        File data = new File(cassandraDir, "data");
        File commitlog = new File(cassandraDir, "commitlog");
        File savedCaches = new File(cassandraDir, "saved_caches");
        for (File dir : Arrays.asList(cassandraDir, bin, conf, data, commitlog, savedCaches)) {
            if (dir.isFile()) {
                getLog().debug("Deleting file " + dir + " as we need to create a directory with the same name.");
                if (!dir.delete()) {
                    getLog().warn("Could not delete file " + dir);
                }
            }
            if (!dir.isDirectory()) {
                getLog().debug("Creating directory " + dir + " as it does not exist.");
                if (!dir.mkdirs()) {
                    getLog().warn("Could not create directory " + dir);
                }
            }
        }
        File cassandraYaml = new File(conf, "cassandra.yaml");
        if (Utils.shouldGenerateResource(project, cassandraYaml)) {
            getLog().debug((cassandraYaml.isFile() ? "Updating " : "Creating ") + cassandraYaml);
            createCassandraYaml(cassandraYaml, data, commitlog, savedCaches, listenAddress, rpcAddress,
                    initialToken, seeds);
        }
        File log4jServerConfig = new File(conf, "log4j-server.xml");
        if (Utils.shouldGenerateResource(project, log4jServerConfig)) {
            getLog().debug((log4jServerConfig.isFile() ? "Updating " : "Creating ") + log4jServerConfig);
            FileUtils.copyURLToFile(getClass().getResource("/log4j2.xml"), log4jServerConfig);
        }
        File log4jClientConfig = new File(conf, "log4j-client.xml");
        if (Utils.shouldGenerateResource(project, log4jClientConfig)) {
            getLog().debug((log4jClientConfig.isFile() ? "Updating " : "Creating ") + log4jClientConfig);
            FileUtils.copyURLToFile(getClass().getResource("/log4j2.xml"), log4jClientConfig);
        }
        File cassandraJar = new File(bin, "cassandra.jar");
        if (Utils.shouldGenerateResource(project, cassandraJar)) {
            getLog().debug((cassandraJar.isFile() ? "Updating " : "Creating ") + cassandraJar);
            createCassandraJar(cassandraJar, CassandraMonitor.class.getName(), cassandraDir);
        }
        /*
        File nodetoolJar = new File( bin, "nodetool.jar" );
        if ( Utils.shouldGenerateResource( project, nodetoolJar ) )
        {
        getLog().debug( ( nodetoolJar.isFile() ? "Updating " : "Creating " ) + nodetoolJar );
        createCassandraJar( nodetoolJar, NodeCmd.class.getName(), cassandraDir );
        }
        */
    }

    /**
     * Generates the {@code cassandra.yaml} file.
     *
     * @param cassandraYaml the {@code cassandra.yaml} file.
     * @param data          The data directory.
     * @param commitlog     The commitlog directory.
     * @param savedCaches   The saved caches directory.
     * @throws IOException If something went wrong.
     */
    private void createCassandraYaml(File cassandraYaml, File data, File commitlog, File savedCaches)
            throws IOException {
        createCassandraYaml(cassandraYaml, data, commitlog, savedCaches, listenAddress, rpcAddress, null,
                new String[] { listenAddress });
    }

    /**
     * Generates the {@code cassandra.yaml} file.
     *
     * @param cassandraYaml the {@code cassandra.yaml} file.
     * @param data          The data directory.
     * @param commitlog     The commitlog directory.
     * @param savedCaches   The saved caches directory.
     * @param listenAddress The address to listen on for storage and other cassandra servers.
     * @param rpcAddress    The address to listen on for clients.
     * @param seeds         The seeds.
     * @throws IOException If something went wrong.
     */
    private void createCassandraYaml(File cassandraYaml, File data, File commitlog, File savedCaches,
            String listenAddress, String rpcAddress, BigInteger initialToken, String[] seeds) throws IOException {
        String defaults = IOUtil.toString(getClass().getResourceAsStream("/cassandra.yaml"));
        StringBuilder config = new StringBuilder();
        config.append("data_file_directories:\n").append("    - ").append(data.getAbsolutePath()).append("\n");
        config.append("commitlog_directory: ").append(commitlog).append("\n");
        config.append("saved_caches_directory: ").append(savedCaches).append("\n");
        config.append("initial_token: ")
                .append(initialToken == null || "null".equals(initialToken) ? "" : initialToken.toString())
                .append("\n");
        config.append("listen_address: ").append(listenAddress).append("\n");
        config.append("storage_port: ").append(storagePort).append("\n");
        config.append("rpc_address: ").append(rpcAddress).append("\n");
        config.append("rpc_port: ").append(rpcPort).append("\n");
        config.append("native_transport_port: ").append(nativeTransportPort).append("\n");
        config.append("start_native_transport: ").append(startNativeTransport).append("\n");
        if (seeds != null) {
            config.append("seed_provider: ").append("\n");
            config.append("    - class_name: org.apache.cassandra.locator.SimpleSeedProvider").append("\n");
            config.append("      parameters:").append("\n");
            String sep = "          - seeds: \"";
            for (int i = 0; i < seeds.length; i++) {
                config.append(sep).append(seeds[i]);
                sep = ", ";
            }
            if (sep.length() == 2) {
                config.append("\"").append("\n");
            }
        }
        FileUtils.fileWrite(cassandraYaml.getAbsolutePath(),
                Utils.merge(Utils.merge(defaults, yaml), config.toString()));
    }

    /**
     * Gets the Java toolchain.
     *
     * @return the Java toolchain.
     */
    protected Toolchain getToolchain() {
        Toolchain tc = null;

        try {
            if (session != null) // session is null in tests..
            {
                ToolchainManager toolchainManager = (ToolchainManager) session.getContainer()
                        .lookup(ToolchainManager.ROLE);

                if (toolchainManager != null) {
                    tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
                }
            }
        } catch (ComponentLookupException componentLookupException) {
            // just ignore, could happen in pre-2.0.9 builds..
        }
        return tc;
    }

    /**
     * Create a {@link CommandLine} to launch Java.
     *
     * @return a {@link CommandLine} to launch Java.
     */
    protected CommandLine newJavaCommandLine() {
        String exec = null;
        Toolchain tc = getToolchain();

        // if the file doesn't exist & toolchain is null, java is probably in the PATH...
        // we should probably also test for isFile and canExecute, but the second one is only
        // available in SDK 6.
        if (tc != null) {
            getLog().info("Toolchain in cassandra-maven-plugin: " + tc);
            exec = tc.findTool("java");
        } else {
            if (OS.isFamilyWindows()) {
                String ex = "java.exe";
                // now try to figure the path from PATH, PATHEXT env vars
                // if bat file, wrap in cmd /c
                String path = System.getenv("PATH");
                if (path != null) {
                    for (String elem : StringUtils.split(path, File.pathSeparator)) {
                        File f = new File(new File(elem), ex);
                        if (f.exists()) {
                            exec = ex;
                            break;
                        }
                    }
                }
            }
        }

        if (exec == null) {
            exec = "java";
        }

        return new CommandLine(exec);
    }

    /**
     * Creates the environment required when launching Cassandra or the CLI tools.
     *
     * @return the environment required when launching Cassandra or the CLI tools.
     */
    protected Map<String, String> createEnvironmentVars() {
        Map<String, String> enviro = new HashMap<String, String>();
        try {
            Properties systemEnvVars = CommandLineUtils.getSystemEnvVars();
            for (Map.Entry entry : systemEnvVars.entrySet()) {
                enviro.put((String) entry.getKey(), (String) entry.getValue());
            }
        } catch (IOException x) {
            getLog().error("Could not assign default system enviroment variables.", x);
        }
        enviro.put("CASSANDRA_CONF", new File(cassandraDir, "conf").getAbsolutePath());
        return enviro;
    }

    /**
     * Creates the command line to launch the cassandra server.
     *
     * @return the command line to launch the cassandra server.
     * @throws IOException if there are issues creating the cassandra home directory.
     */
    protected CommandLine newServiceCommandLine() throws IOException {
        return newServiceCommandLine(cassandraDir, listenAddress, rpcAddress, null, new String[] { listenAddress },
                true, jmxPort);
    }

    /**
     * Creates the command line to launch the cassandra server.
     *
     * @return the command line to launch the cassandra server.
     * @throws IOException if there are issues creating the cassandra home directory.
     */
    protected CommandLine newServiceCommandLine(File cassandraDir, String listenAddress, String rpcAddress,
            BigInteger initialToken, String[] seeds, boolean jmxRemoteEnabled, int jmxPort) throws IOException {
        createCassandraHome(cassandraDir, listenAddress, rpcAddress, initialToken, seeds);
        CommandLine commandLine = newJavaCommandLine();
        commandLine.addArgument("-Xmx" + maxMemory + "m");
        //Only value should be quoted so we have to do it ourselves explicitly and disable additional quotation of whole
        //argument because it causes errors during launch. Also URLEncode.encode on value seems to work correctly too,
        //it is done for log4j.configuration during toURL().toString() conversion.
        commandLine.addArgument(
                "-Dcassandra.storagedir="
                        + org.apache.commons.exec.util.StringUtils.quoteArgument(cassandraDir.getAbsolutePath()),
                false);
        if (stopKey != null && stopPort > 0 && stopPort < 65536) {
            commandLine.addArgument("-D" + CassandraMonitor.KEY_PROPERTY_NAME + "=" + stopKey);
            commandLine.addArgument("-D" + CassandraMonitor.PORT_PROPERTY_NAME + "=" + stopPort);
            commandLine.addArgument("-D" + CassandraMonitor.HOST_PROPERTY_NAME + "=" + listenAddress);
        }
        commandLine.addArgument("-Dlog4j.configurationFile="
                + new File(new File(cassandraDir, "conf"), "log4j-server.xml").toURI().toURL().toString());
        commandLine.addArgument("-Dcom.sun.management.jmxremote=" + jmxRemoteEnabled);
        commandLine.addArgument("-DcassandraLogLevel=" + logLevel);
        if (jmxRemoteEnabled) {
            commandLine.addArgument("-Dcom.sun.management.jmxremote.port=" + jmxPort);
            commandLine.addArgument("-Dcom.sun.management.jmxremote.ssl=false");
            commandLine.addArgument("-Dcom.sun.management.jmxremote.authenticate=false");
        }

        if (systemPropertyVariables != null && !systemPropertyVariables.isEmpty()) {
            for (Map.Entry<String, String> entry : systemPropertyVariables.entrySet()) {
                commandLine.addArgument("-D" + entry.getKey() + "=" + entry.getValue());
            }
        }

        commandLine.addArgument("-jar");
        // It seems that java cannot handle quoted jar file names...
        commandLine.addArgument(new File(new File(cassandraDir, "bin"), "cassandra.jar").getAbsolutePath(), false);

        return commandLine;
    }

    /**
     * Creates the command line to launch the {@code nodetool} utility.
     *
     * @param args the command line arguments to pass to the {@code nodetool} utility.
     * @return the {@link CommandLine} to launch {@code nodetool} with the supplied arguments.
     * @throws IOException if there are issues creating the cassandra home directory.
     */
    protected CommandLine newNodetoolCommandLine(String... args) throws IOException {
        createCassandraHome();
        CommandLine commandLine = newJavaCommandLine();
        commandLine.addArgument("-jar");
        // It seems that java cannot handle quoted jar file names...
        commandLine.addArgument(new File(new File(cassandraDir, "bin"), "nodetool.jar").getAbsolutePath(), false);
        commandLine.addArgument("--host");
        commandLine.addArgument("127.0.0.1");
        commandLine.addArgument("--port");
        commandLine.addArgument(Integer.toString(jmxPort));
        commandLine.addArguments(args);
        return commandLine;
    }

    /**
     * Turns a file into a path string that is quoted (and escaped) if necessary
     *
     * @param file the file to convert to a path string.
     * @return the path string.
     */
    private static String toPathString(File file) {
        String path = file.getAbsolutePath();
        boolean hasSpaces = path.indexOf(' ') != -1;
        boolean hasDoubleQuotes = path.indexOf('\"') != -1;
        boolean hasSingleQuotes = path.indexOf('\'') != -1;
        if (!(hasSpaces || hasDoubleQuotes || hasSingleQuotes)) {
            return path;
        }
        if (!hasDoubleQuotes) {
            return '\"' + path + '\"';
        }
        if (!hasSingleQuotes) {
            return '\'' + path + '\'';
        }
        return '\"' + StringUtils.escape(path, new char[] { '\"' }, '\'') + '\"';
    }

}