THRIFT-1536: Maven thrift plugin
Client: build
Patch: David Trott

Maven thrift plugin
diff --git a/contrib/thrift-maven-plugin/pom.xml b/contrib/thrift-maven-plugin/pom.xml
new file mode 100644
index 0000000..5bc1004
--- /dev/null
+++ b/contrib/thrift-maven-plugin/pom.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ -->
+<project 
+  xmlns="http://maven.apache.org/POM/4.0.0" 
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.thrift</groupId>
+  <artifactId>thrift-maven-plugin</artifactId>
+  <packaging>maven-plugin</packaging>
+  <name>thrift-maven-plugin</name>
+  <version>1.0-SNAPSHOT</version>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>2.14.1</version>
+        <configuration>
+          <systemPropertyVariables>
+            <thriftExecutable>${thrift.compiler}</thriftExecutable>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+      <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.11</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-plugin-api</artifactId>
+      <version>3.1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-project</artifactId>
+      <version>2.2.1</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>14.0.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.plexus</groupId>
+      <artifactId>plexus-utils</artifactId>
+      <version>3.0.14</version>
+    </dependency>
+  </dependencies>
+  <properties>
+    <thrift.root>${basedir}/../..</thrift.root>
+    <thrift.compiler>${thrift.root}/compiler/cpp/thrift</thrift.compiler>
+    <thrift.test.home>${thrift.root}/test</thrift.test.home>
+  </properties>
+</project>
diff --git a/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/AbstractThriftMojo.java b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/AbstractThriftMojo.java
new file mode 100644
index 0000000..a3b5af6
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/AbstractThriftMojo.java
@@ -0,0 +1,377 @@
+/*
+ * 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.thrift.maven;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
+import org.codehaus.plexus.util.cli.CommandLineException;
+import org.codehaus.plexus.util.io.RawInputStreamFacade;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Sets.newHashSet;
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+import static java.util.Collections.list;
+import static org.codehaus.plexus.util.FileUtils.cleanDirectory;
+import static org.codehaus.plexus.util.FileUtils.copyStreamToFile;
+import static org.codehaus.plexus.util.FileUtils.getFiles;
+
+/**
+ * Abstract Mojo implementation.
+ * <p/>
+ * This class is extended by {@link org.apache.thrift.maven.ThriftCompileMojo} and
+ * {@link org.apache.thrift.maven.ThriftTestCompileMojo} in order to override the specific configuration for
+ * compiling the main or test classes respectively.
+ */
+abstract class AbstractThriftMojo extends AbstractMojo {
+
+    private static final String THRIFT_FILE_SUFFIX = ".thrift";
+
+    private static final String DEFAULT_INCLUDES = "**/*" + THRIFT_FILE_SUFFIX;
+
+    /**
+     * The current Maven project.
+     *
+     * @parameter default-value="${project}"
+     * @readonly
+     * @required
+     */
+    protected MavenProject project;
+
+    /**
+     * A helper used to add resources to the project.
+     *
+     * @component
+     * @required
+     */
+    protected MavenProjectHelper projectHelper;
+
+    /**
+     * This is the path to the {@code thrift} executable. By default it will search the {@code $PATH}.
+     *
+     * @parameter default-value="thrift"
+     * @required
+     */
+    private String thriftExecutable;
+
+    /**
+     * This string is passed to the {@code --gen} option of the {@code thrift} parameter. By default
+     * it will generate Java output. The main reason for this option is to be able to add options
+     * to the Java generator - if you generate something else, you're on your own.
+     *
+     * @parameter default-value="java:hashcode"
+     */
+    private String generator;
+
+    /**
+     * @parameter
+     */
+    private File[] additionalThriftPathElements = new File[]{};
+
+    /**
+     * Since {@code thrift} cannot access jars, thrift files in dependencies are extracted to this location
+     * and deleted on exit. This directory is always cleaned during execution.
+     *
+     * @parameter property="${project.build.directory}/thrift-dependencies"
+     * @required
+     */
+    private File temporaryThriftFileDirectory;
+
+    /**
+     * This is the path to the local maven {@code repository}.
+     *
+     * @parameter default-value="${localRepository}"
+     * @required
+     */
+    private ArtifactRepository localRepository;
+
+    /**
+     * Set this to {@code false} to disable hashing of dependent jar paths.
+     * <p/>
+     * This plugin expands jars on the classpath looking for embedded .thrift files.
+     * Normally these paths are hashed (MD5) to avoid issues with long file names on windows.
+     * However if this property is set to {@code false} longer paths will be used.
+     *
+     * @parameter default-value="true"
+     * @required
+     */
+    private boolean hashDependentPaths;
+
+    /**
+     * @parameter
+     */
+    private Set<String> includes = ImmutableSet.of(DEFAULT_INCLUDES);
+
+    /**
+     * @parameter
+     */
+    private Set<String> excludes = ImmutableSet.of();
+
+    /**
+     * @parameter
+     */
+    private long staleMillis = 0;
+
+    /**
+     * @parameter
+     */
+    private boolean checkStaleness = false;
+
+    /**
+     * Executes the mojo.
+     */
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        checkParameters();
+        final File thriftSourceRoot = getThriftSourceRoot();
+        if (thriftSourceRoot.exists()) {
+            try {
+                ImmutableSet<File> thriftFiles = findThriftFilesInDirectory(thriftSourceRoot);
+                final File outputDirectory = getOutputDirectory();
+                ImmutableSet<File> outputFiles = findGeneratedFilesInDirectory(getOutputDirectory());
+
+                if (thriftFiles.isEmpty()) {
+                    getLog().info("No thrift files to compile.");
+                } else if (checkStaleness && ((lastModified(thriftFiles) + staleMillis) < lastModified(outputFiles))) {
+                    getLog().info("Skipping compilation because target directory newer than sources.");
+                    attachFiles();
+                } else {
+                    ImmutableSet<File> derivedThriftPathElements =
+                            makeThriftPathFromJars(temporaryThriftFileDirectory, getDependencyArtifactFiles());
+                    outputDirectory.mkdirs();
+
+                    // Quick fix to fix issues with two mvn installs in a row (ie no clean)
+                    // cleanDirectory(outputDirectory);
+
+                    Thrift thrift = new Thrift.Builder(thriftExecutable, outputDirectory)
+                            .setGenerator(generator)
+                            .addThriftPathElement(thriftSourceRoot)
+                            .addThriftPathElements(derivedThriftPathElements)
+                            .addThriftPathElements(asList(additionalThriftPathElements))
+                            .addThriftFiles(thriftFiles)
+                            .build();
+                    final int exitStatus = thrift.compile();
+                    if (exitStatus != 0) {
+                        getLog().error("thrift failed output: " + thrift.getOutput());
+                        getLog().error("thrift failed error: " + thrift.getError());
+                        throw new MojoFailureException(
+                                "thrift did not exit cleanly. Review output for more information.");
+                    }
+                    attachFiles();
+                }
+            } catch (IOException e) {
+                throw new MojoExecutionException("An IO error occured", e);
+            } catch (IllegalArgumentException e) {
+                throw new MojoFailureException("thrift failed to execute because: " + e.getMessage(), e);
+            } catch (CommandLineException e) {
+                throw new MojoExecutionException("An error occurred while invoking thrift.", e);
+            }
+        } else {
+            getLog().info(format("%s does not exist. Review the configuration or consider disabling the plugin.",
+                    thriftSourceRoot));
+        }
+    }
+
+    ImmutableSet<File> findGeneratedFilesInDirectory(File directory) throws IOException {
+        if (directory == null || !directory.isDirectory())
+            return ImmutableSet.of();
+
+        List<File> javaFilesInDirectory = getFiles(directory, "**/*.java", null);
+        return ImmutableSet.copyOf(javaFilesInDirectory);
+    }
+
+    private long lastModified(ImmutableSet<File> files) {
+        long result = 0;
+        for (File file : files) {
+            if (file.lastModified() > result)
+                result = file.lastModified();
+        }
+        return result;
+    }
+
+    private void checkParameters() {
+        checkNotNull(project, "project");
+        checkNotNull(projectHelper, "projectHelper");
+        checkNotNull(thriftExecutable, "thriftExecutable");
+        checkNotNull(generator, "generator");
+        final File thriftSourceRoot = getThriftSourceRoot();
+        checkNotNull(thriftSourceRoot);
+        checkArgument(!thriftSourceRoot.isFile(), "thriftSourceRoot is a file, not a diretory");
+        checkNotNull(temporaryThriftFileDirectory, "temporaryThriftFileDirectory");
+        checkState(!temporaryThriftFileDirectory.isFile(), "temporaryThriftFileDirectory is a file, not a directory");
+        final File outputDirectory = getOutputDirectory();
+        checkNotNull(outputDirectory);
+        checkState(!outputDirectory.isFile(), "the outputDirectory is a file, not a directory");
+    }
+
+    protected abstract File getThriftSourceRoot();
+
+    protected abstract List<Artifact> getDependencyArtifacts();
+
+    protected abstract File getOutputDirectory();
+
+    protected abstract void attachFiles();
+
+    /**
+     * Gets the {@link File} for each dependency artifact.
+     *
+     * @return A set of all dependency artifacts.
+     */
+    private ImmutableSet<File> getDependencyArtifactFiles() {
+        Set<File> dependencyArtifactFiles = newHashSet();
+        for (Artifact artifact : getDependencyArtifacts()) {
+            dependencyArtifactFiles.add(artifact.getFile());
+        }
+        return ImmutableSet.copyOf(dependencyArtifactFiles);
+    }
+
+    /**
+     * @throws IOException
+     */
+    ImmutableSet<File> makeThriftPathFromJars(File temporaryThriftFileDirectory, Iterable<File> classpathElementFiles)
+            throws IOException, MojoExecutionException {
+        checkNotNull(classpathElementFiles, "classpathElementFiles");
+        // clean the temporary directory to ensure that stale files aren't used
+        if (temporaryThriftFileDirectory.exists()) {
+            cleanDirectory(temporaryThriftFileDirectory);
+        }
+        Set<File> thriftDirectories = newHashSet();
+        for (File classpathElementFile : classpathElementFiles) {
+            // for some reason under IAM, we receive poms as dependent files
+            // I am excluding .xml rather than including .jar as there may be other extensions in use (sar, har, zip)
+            if (classpathElementFile.isFile() && classpathElementFile.canRead() &&
+                    !classpathElementFile.getName().endsWith(".xml")) {
+
+                // create the jar file. the constructor validates.
+                JarFile classpathJar;
+                try {
+                    classpathJar = new JarFile(classpathElementFile);
+                } catch (IOException e) {
+                    throw new IllegalArgumentException(format(
+                            "%s was not a readable artifact", classpathElementFile));
+                }
+                for (JarEntry jarEntry : list(classpathJar.entries())) {
+                    final String jarEntryName = jarEntry.getName();
+                    if (jarEntry.getName().endsWith(THRIFT_FILE_SUFFIX)) {
+                        final File uncompressedCopy =
+                                new File(new File(temporaryThriftFileDirectory,
+                                        truncatePath(classpathJar.getName())), jarEntryName);
+                        uncompressedCopy.getParentFile().mkdirs();
+                        copyStreamToFile(new RawInputStreamFacade(classpathJar
+                                .getInputStream(jarEntry)), uncompressedCopy);
+                        thriftDirectories.add(uncompressedCopy.getParentFile());
+                    }
+                }
+            } else if (classpathElementFile.isDirectory()) {
+                File[] thriftFiles = classpathElementFile.listFiles(new FilenameFilter() {
+                    public boolean accept(File dir, String name) {
+                        return name.endsWith(THRIFT_FILE_SUFFIX);
+                    }
+                });
+
+                if (thriftFiles.length > 0) {
+                    thriftDirectories.add(classpathElementFile);
+                }
+            }
+        }
+        return ImmutableSet.copyOf(thriftDirectories);
+    }
+
+    ImmutableSet<File> findThriftFilesInDirectory(File directory) throws IOException {
+        checkNotNull(directory);
+        checkArgument(directory.isDirectory(), "%s is not a directory", directory);
+        List<File> thriftFilesInDirectory = getFiles(directory, 
+        		Joiner.on(",").join(includes),
+        		Joiner.on(",").join(excludes));
+        return ImmutableSet.copyOf(thriftFilesInDirectory);
+    }
+
+    ImmutableSet<File> findThriftFilesInDirectories(Iterable<File> directories) throws IOException {
+        checkNotNull(directories);
+        Set<File> thriftFiles = newHashSet();
+        for (File directory : directories) {
+            thriftFiles.addAll(findThriftFilesInDirectory(directory));
+        }
+        return ImmutableSet.copyOf(thriftFiles);
+    }
+
+    /**
+     * Truncates the path of jar files so that they are relative to the local repository.
+     *
+     * @param jarPath the full path of a jar file.
+     * @return the truncated path relative to the local repository or root of the drive.
+     */
+    String truncatePath(final String jarPath) throws MojoExecutionException {
+
+        if (hashDependentPaths) {
+            try {
+                return toHexString(MessageDigest.getInstance("MD5").digest(jarPath.getBytes()));
+            } catch (NoSuchAlgorithmException e) {
+                throw new MojoExecutionException("Failed to expand dependent jar", e);
+            }
+        }
+
+        String repository = localRepository.getBasedir().replace('\\', '/');
+        if (!repository.endsWith("/")) {
+            repository += "/";
+        }
+
+        String path = jarPath.replace('\\', '/');
+        int repositoryIndex = path.indexOf(repository);
+        if (repositoryIndex != -1) {
+            path = path.substring(repositoryIndex + repository.length());
+        }
+
+        // By now the path should be good, but do a final check to fix windows machines.
+        int colonIndex = path.indexOf(':');
+        if (colonIndex != -1) {
+            // 2 = :\ in C:\
+            path = path.substring(colonIndex + 2);
+        }
+
+        return path;
+    }
+
+    private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
+
+    public static String toHexString(byte[] byteArray) {
+        final StringBuilder hexString = new StringBuilder(2 * byteArray.length);
+        for (final byte b : byteArray) {
+            hexString.append(HEX_CHARS[(b & 0xF0) >> 4]).append(HEX_CHARS[b & 0x0F]);
+        }
+        return hexString.toString();
+    }
+}
diff --git a/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/Thrift.java b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/Thrift.java
new file mode 100644
index 0000000..6eea954
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/Thrift.java
@@ -0,0 +1,262 @@
+/*
+ * 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.thrift.maven;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.codehaus.plexus.util.cli.CommandLineException;
+import org.codehaus.plexus.util.cli.CommandLineUtils;
+import org.codehaus.plexus.util.cli.Commandline;
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Lists.newLinkedList;
+import static com.google.common.collect.Sets.newHashSet;
+
+/**
+ * This class represents an invokable configuration of the {@code thrift}
+ * compiler. The actual executable is invoked using the plexus
+ * {@link Commandline}.
+ * <p/>
+ * This class currently only supports generating java source files.
+ */
+final class Thrift {
+
+    final static String GENERATED_JAVA = "gen-java";
+
+    private final String executable;
+    private final String generator;
+    private final ImmutableSet<File> thriftPathElements;
+    private final ImmutableSet<File> thriftFiles;
+    private final File javaOutputDirectory;
+    private final CommandLineUtils.StringStreamConsumer output;
+    private final CommandLineUtils.StringStreamConsumer error;
+
+    /**
+     * Constructs a new instance. This should only be used by the {@link Builder}.
+     *
+     * @param executable          The path to the {@code thrift} executable.
+     * @param generator           The value for the {@code --gen} option.
+     * @param thriftPath          The directories in which to search for imports.
+     * @param thriftFiles         The thrift source files to compile.
+     * @param javaOutputDirectory The directory into which the java source files
+     *                            will be generated.
+     */
+    private Thrift(String executable, String generator, ImmutableSet<File> thriftPath,
+                   ImmutableSet<File> thriftFiles, File javaOutputDirectory) {
+        this.executable = checkNotNull(executable, "executable");
+        this.generator = checkNotNull(generator, "generator");
+        this.thriftPathElements = checkNotNull(thriftPath, "thriftPath");
+        this.thriftFiles = checkNotNull(thriftFiles, "thriftFiles");
+        this.javaOutputDirectory = checkNotNull(javaOutputDirectory, "javaOutputDirectory");
+        this.error = new CommandLineUtils.StringStreamConsumer();
+        this.output = new CommandLineUtils.StringStreamConsumer();
+    }
+
+    /**
+     * Invokes the {@code thrift} compiler using the configuration specified at
+     * construction.
+     *
+     * @return The exit status of {@code thrift}.
+     * @throws CommandLineException
+     */
+    public int compile() throws CommandLineException {
+
+        for (File thriftFile : thriftFiles) {
+            Commandline cl = new Commandline();
+            cl.setExecutable(executable);
+            cl.addArguments(buildThriftCommand(thriftFile).toArray(new String[]{}));
+            final int result = CommandLineUtils.executeCommandLine(cl, null, output, error);
+
+            if (result != 0) {
+                return result;
+            }
+        }
+
+        // result will always be 0 here.
+        return 0;
+    }
+
+    /**
+     * Creates the command line arguments.
+     * <p/>
+     * This method has been made visible for testing only.
+     *
+     * @param thriftFile
+     * @return A list consisting of the executable followed by any arguments.
+     */
+    ImmutableList<String> buildThriftCommand(final File thriftFile) {
+        final List<String> command = newLinkedList();
+        // add the executable
+        for (File thriftPathElement : thriftPathElements) {
+            command.add("-I");
+            command.add(thriftPathElement.toString());
+        }
+        command.add("-out");
+        command.add(javaOutputDirectory.toString());
+        command.add("--gen");
+        command.add(generator);
+        command.add(thriftFile.toString());
+        return ImmutableList.copyOf(command);
+    }
+
+    /**
+     * @return the output
+     */
+    public String getOutput() {
+        return output.getOutput();
+    }
+
+    /**
+     * @return the error
+     */
+    public String getError() {
+        return error.getOutput();
+    }
+
+    /**
+     * This class builds {@link Thrift} instances.
+     */
+    static final class Builder {
+        private final String executable;
+        private final File javaOutputDirectory;
+        private Set<File> thriftPathElements;
+        private Set<File> thriftFiles;
+        private String generator;
+
+        /**
+         * Constructs a new builder. The two parameters are present as they are
+         * required for all {@link Thrift} instances.
+         *
+         * @param executable          The path to the {@code thrift} executable.
+         * @param javaOutputDirectory The directory into which the java source files
+         *                            will be generated.
+         * @throws NullPointerException     If either of the arguments are {@code null}.
+         * @throws IllegalArgumentException If the {@code javaOutputDirectory} is
+         *                                  not a directory.
+         */
+        public Builder(String executable, File javaOutputDirectory) {
+            this.executable = checkNotNull(executable, "executable");
+            this.javaOutputDirectory = checkNotNull(javaOutputDirectory);
+            checkArgument(javaOutputDirectory.isDirectory());
+            this.thriftFiles = newHashSet();
+            this.thriftPathElements = newHashSet();
+        }
+
+        /**
+         * Adds a thrift file to be compiled. Thrift files must be on the thriftpath
+         * and this method will fail if a thrift file is added without first adding a
+         * parent directory to the thriftpath.
+         *
+         * @param thriftFile
+         * @return The builder.
+         * @throws IllegalStateException If a thrift file is added without first
+         *                               adding a parent directory to the thriftpath.
+         * @throws NullPointerException  If {@code thriftFile} is {@code null}.
+         */
+        public Builder addThriftFile(File thriftFile) {
+            checkNotNull(thriftFile);
+            checkArgument(thriftFile.isFile());
+            checkArgument(thriftFile.getName().endsWith(".thrift"));
+            checkThriftFileIsInThriftPath(thriftFile);
+            thriftFiles.add(thriftFile);
+            return this;
+        }
+
+        /**
+         * Adds the option string for the Thrift executable's {@code --gen} parameter.
+         *
+         * @param generator
+         * @return The builder
+         * @throws NullPointerException If {@code generator} is {@code null}.
+         */
+        public Builder setGenerator(String generator) {
+            checkNotNull(generator);
+            this.generator = generator;
+            return this;
+        }
+
+        private void checkThriftFileIsInThriftPath(File thriftFile) {
+            assert thriftFile.isFile();
+            checkState(checkThriftFileIsInThriftPathHelper(thriftFile.getParentFile()));
+        }
+
+        private boolean checkThriftFileIsInThriftPathHelper(File directory) {
+            assert directory.isDirectory();
+            if (thriftPathElements.contains(directory)) {
+                return true;
+            } else {
+                final File parentDirectory = directory.getParentFile();
+                return (parentDirectory == null) ? false
+                        : checkThriftFileIsInThriftPathHelper(parentDirectory);
+            }
+        }
+
+        /**
+         * @see #addThriftFile(File)
+         */
+        public Builder addThriftFiles(Iterable<File> thriftFiles) {
+            for (File thriftFile : thriftFiles) {
+                addThriftFile(thriftFile);
+            }
+            return this;
+        }
+
+        /**
+         * Adds the {@code thriftPathElement} to the thriftPath.
+         *
+         * @param thriftPathElement A directory to be searched for imported thrift message
+         *                          buffer definitions.
+         * @return The builder.
+         * @throws NullPointerException     If {@code thriftPathElement} is {@code null}.
+         * @throws IllegalArgumentException If {@code thriftPathElement} is not a
+         *                                  directory.
+         */
+        public Builder addThriftPathElement(File thriftPathElement) {
+            checkNotNull(thriftPathElement);
+            checkArgument(thriftPathElement.isDirectory());
+            thriftPathElements.add(thriftPathElement);
+            return this;
+        }
+
+        /**
+         * @see #addThriftPathElement(File)
+         */
+        public Builder addThriftPathElements(Iterable<File> thriftPathElements) {
+            for (File thriftPathElement : thriftPathElements) {
+                addThriftPathElement(thriftPathElement);
+            }
+            return this;
+        }
+
+        /**
+         * @return A configured {@link Thrift} instance.
+         * @throws IllegalStateException If no thrift files have been added.
+         */
+        public Thrift build() {
+            checkState(!thriftFiles.isEmpty());
+            return new Thrift(executable, generator, ImmutableSet.copyOf(thriftPathElements),
+                    ImmutableSet.copyOf(thriftFiles), javaOutputDirectory);
+        }
+    }
+}
diff --git a/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/ThriftCompileMojo.java b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/ThriftCompileMojo.java
new file mode 100644
index 0000000..b4f7571
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/ThriftCompileMojo.java
@@ -0,0 +1,78 @@
+/*
+ * 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.thrift.maven;
+
+import java.io.File;
+import java.util.List;
+import org.apache.maven.artifact.Artifact;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * This mojo executes the {@code thrift} compiler for generating java sources
+ * from thrift definitions. It also searches dependency artifacts for
+ * thrift files and includes them in the thriftPath so that they can be
+ * referenced. Finally, it adds the thrift files to the project as resources so
+ * that they are included in the final artifact.
+ *
+ * @phase generate-sources
+ * @goal compile
+ * @requiresDependencyResolution compile
+ */
+public final class ThriftCompileMojo extends AbstractThriftMojo {
+
+    /**
+     * The source directories containing the sources to be compiled.
+     *
+     * @parameter default-value="${basedir}/src/main/thrift"
+     * @required
+     */
+    private File thriftSourceRoot;
+
+    /**
+     * This is the directory into which the {@code .java} will be created.
+     *
+     * @parameter default-value="${project.build.directory}/generated-sources/thrift"
+     * @required
+     */
+    private File outputDirectory;
+
+    @Override
+    protected List<Artifact> getDependencyArtifacts() {
+        List<Artifact> compileArtifacts = project.getCompileArtifacts();
+        return compileArtifacts;
+    }
+
+    @Override
+    protected File getOutputDirectory() {
+        return outputDirectory;
+    }
+
+    @Override
+    protected File getThriftSourceRoot() {
+        return thriftSourceRoot;
+    }
+
+    @Override
+    protected void attachFiles() {
+        project.addCompileSourceRoot(outputDirectory.getAbsolutePath());
+        projectHelper.addResource(project, thriftSourceRoot.getAbsolutePath(),
+        		ImmutableList.of("**/*.thrift"), null);
+    }
+}
diff --git a/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/ThriftTestCompileMojo.java b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/ThriftTestCompileMojo.java
new file mode 100644
index 0000000..fb89d96
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/main/java/org/apache/thrift/maven/ThriftTestCompileMojo.java
@@ -0,0 +1,74 @@
+/*
+ * 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.thrift.maven;
+
+import java.io.File;
+import java.util.List;
+import org.apache.maven.artifact.Artifact;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * @phase generate-test-sources
+ * @goal testCompile
+ * @requiresDependencyResolution test
+ */
+public final class ThriftTestCompileMojo extends AbstractThriftMojo {
+
+    /**
+     * The source directories containing the sources to be compiled.
+     *
+     * @parameter default-value="${basedir}/src/test/thrift"
+     * @required
+     */
+    private File thriftTestSourceRoot;
+
+    /**
+     * This is the directory into which the {@code .java} will be created.
+     *
+     * @parameter default-value="${project.build.directory}/generated-test-sources/thrift"
+     * @required
+     */
+    private File outputDirectory;
+
+    @Override
+    protected void attachFiles() {
+        project.addTestCompileSourceRoot(outputDirectory.getAbsolutePath());
+        projectHelper.addTestResource(project, thriftTestSourceRoot.getAbsolutePath(),
+        		ImmutableList.of("**/*.thrift"), null);
+    }
+
+    @Override
+    protected List<Artifact> getDependencyArtifacts() {
+        // TODO(gak): maven-project needs generics
+        @SuppressWarnings("unchecked")
+        List<Artifact> testArtifacts = project.getTestArtifacts();
+        return testArtifacts;
+    }
+
+    @Override
+    protected File getOutputDirectory() {
+        return outputDirectory;
+    }
+
+    @Override
+    protected File getThriftSourceRoot() {
+        return thriftTestSourceRoot;
+    }
+}
diff --git a/contrib/thrift-maven-plugin/src/test/java/org/apache/thrift/maven/TestThrift.java b/contrib/thrift-maven-plugin/src/test/java/org/apache/thrift/maven/TestThrift.java
new file mode 100644
index 0000000..3ecd094
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/test/java/org/apache/thrift/maven/TestThrift.java
@@ -0,0 +1,163 @@
+/*
+ * 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.thrift.maven;
+
+import org.codehaus.plexus.util.FileUtils;
+import org.codehaus.plexus.util.cli.CommandLineException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class TestThrift {
+
+    private File testRootDir;
+    private File idlDir;
+    private File genJavaDir;
+    private Thrift.Builder builder;
+
+    @Before
+    public void setup() throws Exception {
+        final File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+        testRootDir = new File(tmpDir, "thrift-test");
+
+        if (testRootDir.exists()) {
+            FileUtils.cleanDirectory(testRootDir);
+        } else {
+            assertTrue("Failed to create output directory for test: " + testRootDir.getPath(), testRootDir.mkdir());
+        }
+
+        File testResourceDir = new File("src/test/resources");
+        assertTrue("Unable to find test resources", testRootDir.exists());
+
+        String thriftExecutable = System.getProperty("thriftExecutable", "thrift");
+        if (!(new File(thriftExecutable).exists())) {
+            thriftExecutable = "thrift";
+        }
+        System.out.println("Thrift compiler: " + thriftExecutable);
+
+        idlDir = new File(testResourceDir, "idl");
+        genJavaDir = new File(testRootDir, Thrift.GENERATED_JAVA);
+        builder = new Thrift.Builder(thriftExecutable, testRootDir);
+        builder
+            .setGenerator("java")
+            .addThriftPathElement(idlDir);
+    }
+
+    @Test
+    public void testThriftCompile() throws Exception {
+        executeThriftCompile();
+    }
+
+    @Test
+    public void testThriftCompileWithGeneratorOption() throws Exception {
+        builder.setGenerator("java:private-members,hashcode");
+        executeThriftCompile();
+    }
+
+    private void executeThriftCompile() throws CommandLineException {
+        final File thriftFile = new File(idlDir, "shared.thrift");
+
+        builder.addThriftFile(thriftFile);
+
+        final Thrift thrift = builder.build();
+
+        assertTrue("File not found: shared.thrift", thriftFile.exists());
+        assertFalse("gen-java directory should not exist", genJavaDir.exists());
+
+        // execute the compile
+        final int result = thrift.compile();
+        assertEquals(0, result);
+
+        assertFalse("gen-java directory was not removed", genJavaDir.exists());
+        assertTrue("generated java code doesn't exist",
+            new File(testRootDir, "shared/SharedService.java").exists());
+    }
+
+    @Test
+    public void testThriftMultipleFileCompile() throws Exception {
+        final File sharedThrift = new File(idlDir, "shared.thrift");
+        final File tutorialThrift = new File(idlDir, "tutorial.thrift");
+
+        builder.addThriftFile(sharedThrift);
+        builder.addThriftFile(tutorialThrift);
+
+        final Thrift thrift = builder.build();
+
+        assertTrue("File not found: shared.thrift", sharedThrift.exists());
+        assertFalse("gen-java directory should not exist", genJavaDir.exists());
+
+        // execute the compile
+        final int result = thrift.compile();
+        assertEquals(0, result);
+
+        assertFalse("gen-java directory was not removed", genJavaDir.exists());
+        assertTrue("generated java code doesn't exist",
+            new File(testRootDir, "shared/SharedService.java").exists());
+        assertTrue("generated java code doesn't exist",
+            new File(testRootDir, "tutorial/InvalidOperation.java").exists());
+    }
+
+    @Test
+    public void testBadCompile() throws Exception {
+        final File thriftFile = new File(testRootDir, "missing.thrift");
+        builder.addThriftPathElement(testRootDir);
+
+        // Hacking around checks in addThrift file.
+        assertTrue(thriftFile.createNewFile());
+        builder.addThriftFile(thriftFile);
+        assertTrue(thriftFile.delete());
+
+        final Thrift thrift = builder.build();
+
+        assertTrue(!thriftFile.exists());
+        assertFalse("gen-java directory should not exist", genJavaDir.exists());
+
+        // execute the compile
+        final int result = thrift.compile();
+        assertEquals(1, result);
+    }
+
+    @Test
+    public void testFileInPathPreCondition() throws Exception {
+        final File thriftFile = new File(testRootDir, "missing.thrift");
+
+        // Hacking around checks in addThrift file.
+        assertTrue(thriftFile.createNewFile());
+        try {
+            builder.addThriftFile(thriftFile);
+            fail("Expected IllegalStateException");
+        } catch (IllegalStateException e) {
+        }
+    }
+
+    @After
+    public void cleanup() throws Exception {
+        if (testRootDir.exists()) {
+            FileUtils.cleanDirectory(testRootDir);
+            assertTrue("Failed to delete output directory for test: " + testRootDir.getPath(), testRootDir.delete());
+        }
+    }
+}
diff --git a/contrib/thrift-maven-plugin/src/test/resources/idl/shared.thrift b/contrib/thrift-maven-plugin/src/test/resources/idl/shared.thrift
new file mode 100644
index 0000000..475e7f8
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/test/resources/idl/shared.thrift
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+/**
+ * This Thrift file can be included by other Thrift files that want to share
+ * these definitions.
+ */
+
+namespace cpp shared
+namespace java shared
+namespace perl shared
+
+struct SharedStruct {
+  1: i32 key
+  2: string value
+}
+
+service SharedService {
+  SharedStruct getStruct(1: i32 key)
+}
diff --git a/contrib/thrift-maven-plugin/src/test/resources/idl/tutorial.thrift b/contrib/thrift-maven-plugin/src/test/resources/idl/tutorial.thrift
new file mode 100644
index 0000000..86e433d
--- /dev/null
+++ b/contrib/thrift-maven-plugin/src/test/resources/idl/tutorial.thrift
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+# Thrift Tutorial
+# Mark Slee (mcslee@facebook.com)
+#
+# This file aims to teach you how to use Thrift, in a .thrift file. Neato. The
+# first thing to notice is that .thrift files support standard shell comments.
+# This lets you make your thrift file executable and include your Thrift build
+# step on the top line. And you can place comments like this anywhere you like.
+#
+# Before running this file, you will need to have installed the thrift compiler
+# into /usr/local/bin.
+
+/**
+ * The first thing to know about are types. The available types in Thrift are:
+ *
+ *  bool        Boolean, one byte
+ *  byte        Signed byte
+ *  i16         Signed 16-bit integer
+ *  i32         Signed 32-bit integer
+ *  i64         Signed 64-bit integer
+ *  double      64-bit floating point value
+ *  string      String
+ *  binary      Blob (byte array)
+ *  map<t1,t2>  Map from one type to another
+ *  list<t1>    Ordered list of one type
+ *  set<t1>     Set of unique elements of one type
+ *
+ * Did you also notice that Thrift supports C style comments?
+ */
+
+// Just in case you were wondering... yes. We support simple C comments too.
+
+/**
+ * Thrift files can reference other Thrift files to include common struct
+ * and service definitions. These are found using the current path, or by
+ * searching relative to any paths specified with the -I compiler flag.
+ *
+ * Included objects are accessed using the name of the .thrift file as a
+ * prefix. i.e. shared.SharedObject
+ */
+include "shared.thrift"
+
+/**
+ * Thrift files can namespace, package, or prefix their output in various
+ * target languages.
+ */
+namespace cpp tutorial
+namespace java tutorial
+namespace php tutorial
+namespace perl tutorial
+namespace smalltalk.category Thrift.Tutorial
+
+/**
+ * Thrift lets you do typedefs to get pretty names for your types. Standard
+ * C style here.
+ */
+typedef i32 MyInteger
+
+/**
+ * Thrift also lets you define constants for use across languages. Complex
+ * types and structs are specified using JSON notation.
+ */
+const i32 INT32CONSTANT = 9853
+const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}
+
+/**
+ * You can define enums, which are just 32 bit integers. Values are optional
+ * and start at 1 if not supplied, C style again.
+ */
+enum Operation {
+  ADD = 1,
+  SUBTRACT = 2,
+  MULTIPLY = 3,
+  DIVIDE = 4
+}
+
+/**
+ * Structs are the basic complex data structures. They are comprised of fields
+ * which each have an integer identifier, a type, a symbolic name, and an
+ * optional default value.
+ *
+ * Fields can be declared "optional", which ensures they will not be included
+ * in the serialized output if they aren't set.  Note that this requires some
+ * manual management in some languages.
+ */
+struct Work {
+  1: i32 num1 = 0,
+  2: i32 num2,
+  3: Operation op,
+  4: optional string comment,
+}
+
+/**
+ * Structs can also be exceptions, if they are nasty.
+ */
+exception InvalidOperation {
+  1: i32 what,
+  2: string why
+}
+
+/**
+ * Ahh, now onto the cool part, defining a service. Services just need a name
+ * and can optionally inherit from another service using the extends keyword.
+ */
+service Calculator extends shared.SharedService {
+
+  /**
+   * A method definition looks like C code. It has a return type, arguments,
+   * and optionally a list of exceptions that it may throw. Note that argument
+   * lists and exception lists are specified using the exact same syntax as
+   * field lists in struct or exception definitions.
+   */
+
+   void ping(),
+
+   i32 add(1:i32 num1, 2:i32 num2),
+
+   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),
+
+   /**
+    * This method has a oneway modifier. That means the client only makes
+    * a request and does not listen for any response at all. Oneway methods
+    * must be void.
+    */
+   oneway void zip()
+
+}
+
+/**
+ * That just about covers the basics. Take a look in the test/ folder for more
+ * detailed examples. After you run this file, your generated code shows up
+ * in folders with names gen-<language>. The generated code isn't too scary
+ * to look at. It even has pretty indentation.
+ */