package org.apache.thrift.maven;
/*
* 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.
*/
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.
*
* @author Gregory Kick
* @author David Trott
* @author Brice Figureau
*/
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 expression="${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();
// TODO(gak): plexus-utils needs generics
@SuppressWarnings("unchecked")
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);
final Joiner joiner = Joiner.on(',');
// TODO(gak): plexus-utils needs generics
@SuppressWarnings("unchecked")
List<File> thriftFilesInDirectory = getFiles(directory, joiner.join(includes), joiner.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();
}
}