/* Copyright (c) 2011 Danish Maritime Authority.
*
* Licensed 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 net.maritimecloud.internal.msdl.compiler.mavenplugin;
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 java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.codehaus.plexus.util.FileUtils.cleanDirectory;
import static org.codehaus.plexus.util.FileUtils.copyStreamToFile;
import static org.codehaus.plexus.util.FileUtils.getFiles;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import net.maritimecloud.msdl.MsdlProcessor;
import net.maritimecloud.msdl.MsdlProcessorResult;
import net.maritimecloud.msdl.plugins.javagen.JavaGenPlugin;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.io.RawInputStreamFacade;
import org.sonatype.plexus.build.incremental.BuildContext;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
/**
* Abstract Mojo implementation.
*
*
*/
abstract class AbstractMsdlMojo extends AbstractMojo {
private static final String MSDL_FILE_SUFFIX = ".msdl";
private static final String DEFAULT_INCLUDES = "**/*" + MSDL_FILE_SUFFIX;
/** The current Maven project. */
@Component
protected MavenProject project;
/** The current Maven Session Object. */
@Component
private MavenSession session;
/** Build context that tracks changes to the source and target files. */
@Component
protected BuildContext buildContext;
/** A helper used to add resources to the project. */
@Component
protected MavenProjectHelper projectHelper;
/** This is the path to the local maven {@code repository}. */
@Parameter(required = true, readonly = true, property = "localRepository")
private ArtifactRepository localRepository;
/** This is the path to the local maven {@code repository}. */
@Parameter(required = false)
private boolean implementsSerializable;
/** Additional source paths for {@code .proto} definitions. */
@Parameter(required = false)
private File[] additionalProtoPathElements = {};
/**
* Since {@code protoc} cannot access jars, proto files in dependencies are extracted to this location and deleted
* on exit. This directory is always cleaned during execution.
*/
@Parameter(required = true, defaultValue = "${project.build.directory}/msdl-dependencies")
private File temporaryMSDLDirectory;
/**
* Set this to {@code false} to disable hashing of dependent jar paths.
* <p/>
* This plugin expands jars on the classpath looking for embedded {@code .proto} 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(required = true, defaultValue = "false")
private boolean hashDependentPaths;
/**
* A list of <include> elements specifying the protobuf definition files (by pattern) that should be included
* in compilation. When not specified, the default includes will be: <code><br/>
* <includes><br/>
* <include>**/*.proto</include><br/>
* </includes><br/>
* </code>
*/
@Parameter(required = false)
private Set<String> includes = ImmutableSet.of(DEFAULT_INCLUDES);
/**
* A list of <exclude> elements specifying the protobuf definition files (by pattern) that should be excluded
* from compilation. When not specified, the default excludes will be empty: <code><br/>
* <excludes><br/>
* </excludes><br/>
* </code>
*/
@Parameter(required = false)
private Set<String> excludes = ImmutableSet.of();
/** The descriptor set file name. Only used if {@code writeDescriptorSet} is set to {@code true}. */
@Parameter(required = true, defaultValue = "${project.build.finalName}.protobin")
private String descriptorSetFileName;
/**
* If set to {@code true}, the compiler will generate a binary descriptor set file for the specified {@code .msdl}
* files.
*/
@Parameter(required = true, defaultValue = "false")
private boolean writeDescriptorSet;
/**
* If {@code true} and {@code writeDescriptorSet} has been set, the compiler will include all dependencies in the
* descriptor set making it "self-contained".
*/
@Parameter(required = false, defaultValue = "false")
private boolean includeDependenciesInDescriptorSet;
/**
* Sets the granularity in milliseconds of the last modification date for testing whether source protobuf
* definitions need recompilation.
*
* <p>
* This parameter is only used when {@link #checkStaleness} parameter is set to {@code true}.
*
* <p>
* If the project is built on NFS it's recommended to set this parameter to {@code 10000}.
*/
@Parameter(required = false, defaultValue = "0")
private long staleMillis;
/**
* Normally {@code protoc} is invoked on every execution of the plugin. Setting this parameter to {@code true} will
* enable checking timestamps of source protobuf definitions vs. generated sources.
*
* @see #staleMillis
*/
@Parameter(required = false, defaultValue = "false")
private boolean checkStaleness;
/** When {@code true}, skip the execution. */
@Parameter(required = false, property = "msdl.skip", defaultValue = "false")
private boolean skip;
/** Specifies the location of the header or license file that can be appended to each generated source file. */
@Parameter(required = false)
protected String headerLocation;
// If we want to loader headers from jar files at some point. checkout
// https://github.com/apache/maven-plugins/blob/trunk/maven-checkstyle-plugin/src/main/java/org/apache/maven/plugin/checkstyle/DefaultCheckstyleExecutor.java
/**
* Usually most of protobuf mojos will not get executed on parent poms (i.e. projects with packaging type 'pom').
* Setting this parameter to {@code true} will force the execution of this mojo, even if it would usually get
* skipped in this case.
*/
@Parameter(required = false, property = "msdl.force", defaultValue = "false")
private boolean forceMojoExecution;
/** Executes this mojo. */
public void execute() throws MojoExecutionException, MojoFailureException {
if (skipMojo()) {
return;
}
checkParameters();
File msdlSourceRoot = getMsdlSourceRoot();
if (msdlSourceRoot.exists()) {
try {
Set<File> msdlFiles = findMsdlFilesInDirectory(msdlSourceRoot);
File outputDirectory = getOutputDirectory();
Set<File> outputFiles = findGeneratedFilesInDirectory(getOutputDirectory());
if (msdlFiles.isEmpty()) {
getLog().info("No msdl files to compile.");
} else if (!hasDelta(msdlFiles)) {
getLog().info("Skipping compilation because build context has no changes.");
attachFiles();
} else if (checkStaleness && checkFilesUpToDate(msdlFiles, outputFiles)) {
getLog().info("Skipping compilation because target directory newer than sources.");
attachFiles();
} else {
Set<File> derivedMsdlPathElements = makeMsdlPathFromJars(temporaryMSDLDirectory,
getDependencyArtifactFiles());
FileUtils.mkdir(outputDirectory.getAbsolutePath());
// Quick fix to fix issues with two mvn installs in a row (ie no clean)
cleanDirectory(outputDirectory);
if (writeDescriptorSet) {
File descriptorSetOutputDirectory = getDescriptorSetOutputDirectory();
FileUtils.mkdir(descriptorSetOutputDirectory.getAbsolutePath());
// See above
cleanDirectory(descriptorSetOutputDirectory);
}
MsdlProcessor g = new MsdlProcessor();
g.setSourceDirectory(msdlSourceRoot.toPath());
for (File f : derivedMsdlPathElements) {
g.addDependencyDirectory(f.toPath());
}
for (File f : msdlFiles) {
g.addFile(f.toPath());
}
JavaGenPlugin javaPlugin = JavaGenPlugin.create(outputDirectory.toPath());
javaPlugin.setImplementsSerializable(implementsSerializable);
getLog().debug("Setting implements serialiable = " + implementsSerializable);
g.addPlugin(javaPlugin);
if (headerLocation != null) {
Path headerPath = Paths.get(headerLocation).toAbsolutePath();
if (!Files.exists(headerPath)) {
getLog().warn("Could not find headerFile: " + headerPath);
} else {
javaPlugin.setHeader(headerPath);
}
}
if (getLog().isDebugEnabled()) {
getLog().debug("Msdl source root:");
getLog().debug(" " + msdlSourceRoot);
if (derivedMsdlPathElements != null && !derivedMsdlPathElements.isEmpty()) {
getLog().debug("Derived msdl paths:");
for (final File path : derivedMsdlPathElements) {
getLog().debug(" " + path);
}
}
if (additionalProtoPathElements != null && additionalProtoPathElements.length > 0) {
getLog().debug("Additional proto paths:");
for (final File path : additionalProtoPathElements) {
getLog().debug(" " + path);
}
}
}
g.setLogger(new AdaptedMsdlLogger(getLog()));
MsdlProcessorResult result = g.executePlugins();
if (result.isSuccesfull()) {
getLog().info("Files succesfully generated");
} else {
// for (Notification n : result) {
// if (n.isError()) {
// getLog().error(n.getMessage());
// } else {
// getLog().warn(n.getMessage());
// }
// }
throw new MojoFailureException("Compilation failed. Review output for more information.");
}
attachFiles();
}
} catch (IOException e) {
throw new MojoExecutionException("An IO error occured", e);
} catch (IllegalArgumentException e) {
throw new MojoFailureException("Compiler failed to execute because: " + e.getMessage(), e);
}
} else {
getLog().info(
format("%s does not exist. Review the configuration or consider disabling the plugin.",
msdlSourceRoot));
}
}
/**
* <p>
* Determine if the mojo execution should get skipped.
* </p>
* This is the case if:
* <ul>
* <li>{@link #skip} is <code>true</code></li>
* <li>if the mojo gets executed on a project with packaging type 'pom' and {@link #forceMojoExecution} is
* <code>false</code></li>
* </ul>
*
* @return <code>true</code> if the mojo execution should be skipped.
*/
protected boolean skipMojo() {
if (skip) {
getLog().info("Skipping msdl mojo execution");
return true;
}
if (!forceMojoExecution && "pom".equals(this.project.getPackaging())) {
getLog().info("Skipping protoc mojo execution for project with packaging type 'pom'");
return true;
}
return false;
}
@SuppressWarnings("unchecked")
protected static Set<File> findGeneratedFilesInDirectory(File directory) throws IOException {
if (directory == null || !directory.isDirectory()) {
return Collections.emptySet();
}
return new HashSet<>(getFiles(directory, "**/*.java", null));
}
/**
* Checks that the source files don't have modification time that is later than the target files.
*
* @param sourceFiles
* a set of source files.
* @param targetFiles
* a set of target files.
* @return {@code true}, if source files are not later than the target files; {@code false}, otherwise.
*/
protected boolean checkFilesUpToDate(final Set<File> sourceFiles, Set<File> targetFiles) {
return FileTools.lastModified(sourceFiles) + staleMillis < FileTools.lastModified(targetFiles);
}
/**
* Checks if the injected build context has changes in any of the specified files.
*
* @param files
* files to be checked for changes.
* @return {@code true}, if at least one file has changes; {@code false}, if no files have changes.
*/
protected boolean hasDelta(Set<File> files) {
for (File file : files) {
if (buildContext.hasDelta(file)) {
return true;
}
}
return false;
}
protected void checkParameters() {
checkNotNull(project, "project");
checkNotNull(projectHelper, "projectHelper");
File msdlSourceRoot = getMsdlSourceRoot();
checkNotNull(msdlSourceRoot);
checkArgument(!msdlSourceRoot.isFile(), "msdlSourceRoot is a file, not a directory");
checkNotNull(temporaryMSDLDirectory, "temporaryMsdlFileDirectory");
checkState(!temporaryMSDLDirectory.isFile(), "temporaryMsdlFileDirectory is a file, not a directory");
File outputDirectory = getOutputDirectory();
checkNotNull(outputDirectory);
checkState(!outputDirectory.isFile(), "the outputDirectory is a file, not a directory");
}
protected abstract File getMsdlSourceRoot();
// TODO add artifact filtering (inclusions and exclusions)
// TODO add filtering for proto definitions in included artifacts
protected abstract List<Artifact> getDependencyArtifacts();
/**
* Returns the output directory for generated sources. Depends on build phase so must be defined in concrete
* implementation.
*
* @return output directory for generated sources.
*/
protected abstract File getOutputDirectory();
/**
* Returns output directory for descriptor set file. Depends on build phase so must be defined in concrete
* implementation.
*
* @return output directory for generated descriptor set.
*
* @since 0.3.0
*/
protected abstract File getDescriptorSetOutputDirectory();
protected abstract void attachFiles();
/**
* Gets the {@link File} for each dependency artifact.
*
* @return A set of all dependency artifacts.
*/
protected Set<File> getDependencyArtifactFiles() {
final Set<File> dependencyArtifactFiles = new HashSet<>();
for (final Artifact artifact : getDependencyArtifacts()) {
dependencyArtifactFiles.add(artifact.getFile());
}
return dependencyArtifactFiles;
}
/**
* @throws IOException
*/
@SuppressWarnings("resource")
protected Set<File> makeMsdlPathFromJars(File temporaryProtoFileDirectory, Iterable<File> classpathElementFiles)
throws IOException, MojoExecutionException {
checkNotNull(classpathElementFiles, "classpathElementFiles");
if (!classpathElementFiles.iterator().hasNext()) {
return Collections.emptySet();
}
// clean the temporary directory to ensure that stale files aren't used
if (temporaryProtoFileDirectory.exists()) {
cleanDirectory(temporaryProtoFileDirectory);
}
Set<File> protoDirectories = new HashSet<>();
for (final 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.
final JarFile classpathJar;
try {
classpathJar = new JarFile(classpathElementFile);
} catch (IOException e) {
throw new IllegalArgumentException(format("%s was not a readable artifact", classpathElementFile),
e);
}
try {
final Enumeration<JarEntry> jarEntries = classpathJar.entries();
while (jarEntries.hasMoreElements()) {
final JarEntry jarEntry = jarEntries.nextElement();
final String jarEntryName = jarEntry.getName();
// TODO try using org.codehaus.plexus.util.SelectorUtils.matchPath() with DEFAULT_INCLUDES
if (jarEntryName.endsWith(MSDL_FILE_SUFFIX)) {
File jarDirectory = new File(temporaryProtoFileDirectory,
truncatePath(classpathJar.getName()));
File uncompressedCopy = new File(jarDirectory, jarEntryName);
FileUtils.mkdir(uncompressedCopy.getParentFile().getAbsolutePath());
copyStreamToFile(new RawInputStreamFacade(classpathJar.getInputStream(jarEntry)),
uncompressedCopy);
protoDirectories.add(jarDirectory);
}
}
} finally {
classpathJar.close();
}
} else if (classpathElementFile.isDirectory()) {
final List<?> protoFiles = getFiles(classpathElementFile, DEFAULT_INCLUDES, null);
if (!protoFiles.isEmpty()) {
protoDirectories.add(classpathElementFile);
}
}
}
return ImmutableSet.copyOf(protoDirectories);
}
@SuppressWarnings("unchecked")
protected Set<File> findMsdlFilesInDirectory(File directory) throws IOException {
requireNonNull(directory);
checkArgument(directory.isDirectory(), "%s is not a directory", directory);
Joiner joiner = Joiner.on(',');
List<File> protoFilesInDirectory = getFiles(directory, joiner.join(includes), joiner.join(excludes));
return ImmutableSet.copyOf(protoFilesInDirectory);
}
/**
* 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.
*/
protected 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('\\', '/');
final 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.
final 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();
protected 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();
}
}