package mandelbrot.ocamljava_maven_plugin;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import mandelbrot.dependency.analyzer.Analyzer;
import mandelbrot.dependency.data.DependencyGraph;
import mandelbrot.dependency.data.ModuleDescriptor;
import mandelbrot.ocamljava_maven_plugin.util.ArtifactDescriptor;
import mandelbrot.ocamljava_maven_plugin.util.FileMappings;
import mandelbrot.ocamljava_maven_plugin.util.JarExtractor;
import mandelbrot.ocamljava_maven_plugin.util.MapTransforms;
import mandelbrot.ocamljava_maven_plugin.util.StringTransforms;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.ocamljava.wrapper.ocamljavaMain;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
/**
* <p>
* This is a goal which wraps OCaml compiled source interfaces with code
* generated java equivalents. It is the same as executing something like
* </p>
* <p>
* <code>ocamlwrap -package com.mycomp lib.cmi</code>
* </p>
* from the command line but instead uses maven parameters to infer the compiled
* module interface locations. All parameters can be overridden. See the
* configuration section of the documentation for more information.</p>
*
* @since 1.0
*/
@Mojo(name = "wrap", defaultPhase = LifecyclePhase.GENERATE_SOURCES, threadSafe = true, requiresDependencyResolution = ResolutionScope.RUNTIME, requiresProject = true)
public class OcamlWrapMojo extends OcamlJavaWrapAbstractMojo {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String OCAMLJAVA_MAVEN_PLUGIN_COMMAND_LINE_ARGS = "ocamljava.maven.plugin.commandLineArgs";
/***
* The working directory where the generated Java source files are created.
*
*/
@Parameter(required = true, defaultValue = "${project.build.directory}/generated-sources/ocaml")
protected File generatedSourcesOutputDirectory;
/***
* <p>
* The artifacts to scan for ocaml compiled interfaces and then perform code
* generation on. Expressed with
* groupId:artifactId:version[:type[:classifier]] format.
* </p>
*
* <p>
* NOTE: The artifacts must have been packaged as either a jar or zip file.
* </p>
*/
@Parameter(required = true)
protected Set<String> targetArtifacts = ImmutableSet.of();
/***
* Prefix for names of generated classes. Default value is blank.
*
*/
@Parameter(defaultValue = "")
protected String classNamePrefix = "";
/***
* Suffix for names of generated classes.
*
*/
@Parameter(defaultValue = "Wrapper")
protected String classNameSuffix = "Wrapper";
/***
* Arguments passed for library initialization. Defaults to empty.
*
*/
@Parameter(defaultValue = "")
protected String libraryArgs = "";
public static enum LibraryInitMode {
STATIC, EXPLICIT;
public String toCommandLineValue() {
return name().toLowerCase();
}
}
/**
* Library initialization mode. One of <code>EXPLICIT</code> or
* <code>STATIC</code>.
*
*/
@Parameter(defaultValue = "EXPLICIT")
protected LibraryInitMode libraryInitMode = LibraryInitMode.EXPLICIT;
/***
* Library package.
*
*/
@Parameter(defaultValue = "")
protected String libaryPackage = "";
/***
* Whether to disable warnings during the code generation process.
*
*/
@Parameter(defaultValue = "false")
protected boolean noWarnings;
public static enum StringMapping {
JAVA_STRING, OCAMLSTRING, BYTE_ARRAY;
public String toCommandLineValue() {
return name().toLowerCase().replace("_", "-");
}
}
/***
* Determines the string mapping for the OCaml string type. One of
* <code>JAVA_STRING</code>, <code>OCAMLSTRING</code>, or
* <code>BYTE-ARRAY</code>.
*
*/
@Parameter(defaultValue = "JAVA_STRING")
protected StringMapping stringMapping = StringMapping.JAVA_STRING;
/***
* Whether to enable verbose mode.
*
**/
@Parameter(defaultValue = "false")
protected boolean verbose;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (hasCommandLineArgs()) {
final ocamljavaMain result = org.ocamljava.wrapper.ocamljavaMain
.mainWithReturn(getCommandLineArgs());
// Will never execute, because the above forks.
getLog().warn("process should have exited: " + result);
return;
}
if (targetArtifacts == null || targetArtifacts.isEmpty()) {
getLog().info("no artifacts to wrap");
return;
}
final Properties properties = System.getProperties();
final Object object = properties.get(FORK_PROPERTY_NAME);
if (Boolean.parseBoolean(Optional.fromNullable(object).or(Boolean.TRUE)
.toString())) {
getLog().info("forking process");
final boolean forkAgain = false;
invokePlugin(fullyQualifiedGoal(), forkAgain);
} else {
final Collection<String> artifactFiles = getArtifactFiles();
final Multimap<String, String> filesByExtension = extractFilesFromArtifacts(artifactFiles);
if (filesByExtension.isEmpty()) {
getLog().info(
"no relevant files found in " + getTargetJarFullPath());
return;
}
final DependencyGraph dependencyGraph = getDependendyGraph(filesByExtension);
getLog().info("dependencyGraph: " + dependencyGraph);
final ImmutableSet<String> compiledFiles = ImmutableSet
.<String> builder()
.addAll(filesByExtension
.get(OcamlJavaConstants.COMPILED_INTERFACE_EXTENSION))
.addAll(filesByExtension
.get(OcamlJavaConstants.COMPILED_IMPL_JAVA_EXTENSION))
.build();
if (compiledFiles.isEmpty()) {
getLog().info(
"no wrappable files found in " + getTargetJarFullPath());
return;
}
wrapInternal(dependencyGraph, compiledFiles);
}
}
private String[] getCommandLineArgs() throws MojoExecutionException {
try {
final List<String> arguments = OBJECT_MAPPER.readValue(
getCommandLineArgumentsFile(),
new TypeReference<List<String>>() {
});
return arguments.toArray(new String[] {});
} catch (final IOException e) {
throw new MojoExecutionException(
"failed to get command line arguments for ocamlwrap invocation",
e);
}
}
private File getCommandLineArgumentsFile() {
final File file = new File(getOcamlCompiledSourcesTargetFullPath()
+ File.separator + "commandLineArgs."
+ OcamlJavaConstants.JSON_EXTENSION);
file.getParentFile().mkdirs();
return file;
}
private boolean hasCommandLineArgs() {
final String property = System
.getProperty(OCAMLJAVA_MAVEN_PLUGIN_COMMAND_LINE_ARGS);
if (StringUtils.isBlank(property))
return false;
return Boolean.parseBoolean(property);
}
private void wrapInternal(final DependencyGraph dependencyGraph,
final Set<String> compiledFiles) throws MojoExecutionException {
getLog().info("infer package names based on directory structure");
final Multimap<String, String> filesByPackageName = FileMappings
.buildPackageMap(new File(
getOcamlCompiledSourcesTargetFullPath()), compiledFiles);
final Map<String, String> packageNamesByFile = MapTransforms.reverseIndex(filesByPackageName);
getLog().info("filesByPackageName: " + filesByPackageName);
final ImmutableSet.Builder<String> includeDirsBuilder = ImmutableSet
.builder();
// TODO inefficient - also should be factored into a util library module
for (final String file : packageNamesByFile.keySet()) {
try {
includeDirsBuilder.add(new File(file).getParent());
} catch (final Exception e) {
getLog().error("wrapping threw an exception", e);
throw new MojoExecutionException("wrapping threw an exception",
e);
}
}
final Set<ModuleDescriptor> concat = ImmutableSet.copyOf(Iterables
.concat(dependencyGraph.getDependencies().values()));
final Comparator<? super String> comparator = createComparator(concat);
final ImmutableSortedSet<String> sorted = ImmutableSortedSet.copyOf(
comparator, filterFiles(filesByPackageName.values()));
wrapFiles(includeDirsBuilder.build(), sorted, packageNamesByFile);
moveFiles(sorted);
}
private static Collection<String> filterFiles(
final Collection<String> filesInPackage) {
final Collection<String> cmiFiles = Collections2.filter(filesInPackage,
new Predicate<String>() {
@Override
public boolean apply(final String value) {
return OcamlJavaConstants.COMPILED_INTERFACE_EXTENSION
.equalsIgnoreCase(FileUtils.extension(value));
}
});
return cmiFiles;
}
public String fullyQualifiedGoal() {
return OcamlJavaConstants.wrapGoal();
}
private Comparator<? super String> createComparator(
final Collection<ModuleDescriptor> moduleDescriptors) {
final Comparator<? super String> comparator = new Comparator<String>() {
@Override
public int compare(final String paramT1, final String paramT2) {
if (Objects.equal(paramT1, paramT2)) {
return 0;
}
for (final ModuleDescriptor moduleDescriptor : moduleDescriptors) {
final Optional<String> moduleNameOfSource = Analyzer
.moduleNameOfSource(paramT1);
if (Objects.equal(moduleDescriptor.getModuleName(),
moduleNameOfSource.orNull())) {
return -1;
}
final Optional<String> moduleNameOfSource2 = Analyzer
.moduleNameOfSource(paramT2);
if (Objects.equal(moduleDescriptor.getModuleName(),
moduleNameOfSource2.orNull())) {
return 1;
}
}
return Optional.fromNullable(paramT1).or("").compareTo(paramT2);
}
};
return comparator;
}
private void wrapFiles(final Collection<String> includeDirs,
final Collection<String> cmiFiles, final Map<String, String> packageNamesByFile) throws MojoExecutionException {
final Optional<String[]> commandLineArguments = generateCommandLineArguments(
includeDirs, cmiFiles, packageNamesByFile);
if (commandLineArguments.isPresent()) {
final ImmutableList<String> argsAsList = ImmutableList
.copyOf(commandLineArguments.get());
if (getLog().isInfoEnabled())
getLog().info(
"command line arguments: "
+ Joiner.on(" ").join(argsAsList));
final File commandLineArgumentsFile = getCommandLineArgumentsFile();
try {
OBJECT_MAPPER.writeValue(commandLineArgumentsFile, argsAsList);
} catch (final IOException e) {
throw new MojoExecutionException(
"exception seriliazing command line arguments: "
+ argsAsList, e);
}
final String goal = fullyQualifiedGoal();
final boolean forkAgain = false;
final Properties properties = new Properties();
properties.put(OCAMLJAVA_MAVEN_PLUGIN_COMMAND_LINE_ARGS,
Boolean.TRUE.toString());
invokePlugin(goal, forkAgain, properties);
} else if (getLog().isInfoEnabled()) {
getLog().info(
"no compiled module interfaces to wrap for package \""
+ packageName + "\" in "
+ getOcamlCompiledSourcesTargetFullPath());
}
}
private void moveFiles(final Collection<String> cmiFiles) {
for (final String cmiFile : cmiFiles) {
try {
final String generatedSourceName = inferGeneratedSourceName(cmiFile);
final String packagePath = isDynamicPackageMode() ? FileMappings
.toPackagePath(getOcamlCompiledSourcesTargetFullPath(),
cmiFile) : packageName;
final String target =
// eg.
// target/generate-sources/ocaml/com/mycomp/FooWrapper.java
this.generatedSourcesOutputDirectory
+ (StringUtils.isNotBlank(packagePath) ? (File.separator + packagePath)
: "");
// TODO ..should I just scan for *.java instead, or some
// regex pattern specified in Maven project?
final File file = new File(project.getBasedir().getPath()
+ File.separator + generatedSourceName);
if (!file.exists()) {
getLog().warn(
"expected file " + file
+ " but it does not exist! skipping...");
continue;
}
final File targetDir = new File(target);
getLog().info("copying " + file + " to " + targetDir);
FileUtils.copyFileToDirectory(file, targetDir);
} catch (final IOException e) {
getLog().error("io exception", e);
}
}
return;
}
private String inferGeneratedSourceName(final String cmiFile) {
final String generatedSource = StringTransforms.trim(
FileUtils.basename(new File(cmiFile).getName()), ".");
return Optional.fromNullable(this.classNamePrefix).or("")
+ StringUtils.capitalizeFirstLetter(generatedSource)
+ Optional.fromNullable(this.classNameSuffix).or("")
+ OcamlJavaConstants.JAVA_EXTENSION;
}
private Multimap<String, String> extractFilesFromArtifacts(
final Collection<String> artifactFiles) {
final ImmutableMultimap.Builder<String, String> moduleFilesBuilder = ImmutableMultimap
.builder();
for (final String artifactFile : artifactFiles) {
final Collection<String> compiledModules = new JarExtractor(this)
.extractFiles(
artifactFile,
getOcamlCompiledSourcesTargetFullPath(),
ImmutableSet
.of(OcamlJavaConstants.COMPILED_INTERFACE_EXTENSION,
OcamlJavaConstants.COMPILED_IMPL_OCAML_EXTENSION,
OcamlJavaConstants.COMPILED_IMPL_JAVA_EXTENSION,
OcamlJavaConstants.JSON_EXTENSION));
for (final String compiledModule : compiledModules) {
moduleFilesBuilder.put(FileUtils.getExtension(compiledModule),
compiledModule);
}
}
return moduleFilesBuilder.build();
}
private Collection<String> getArtifactFiles() {
final ImmutableList.Builder<String> builder = ImmutableList.builder();
final Collection<Artifact> filter = Collections2.filter(
project.getArtifacts(), new Predicate<Artifact>() {
@Override
public boolean apply(final Artifact artifact) {
final String artifactDescription = ArtifactDescriptor
.toDescriptor(artifact);
getLog().info("artifact key: " + artifactDescription);
for (final String targetArtifact : targetArtifacts)
if (artifactDescription.startsWith(targetArtifact))
return true;
return false;
}
});
for (final Artifact artifact : filter) {
getLog().info("adding artifact: " + artifact);
builder.add(artifact.getFile().getPath());
}
final ImmutableList<String> artifactFiles = builder.build();
return artifactFiles;
}
private Optional<String[]> generateCommandLineArguments(
final Collection<String> includePaths,
final Collection<String> files,
final Map<String, String> packageNames) {
if (files == null || files.isEmpty())
return Optional.absent();
final ImmutableList.Builder<String> builder = ImmutableList.builder();
if (noWarnings)
builder.add(OcamlJavaConstants.NO_WARNINGS_OPTION);
if (verbose)
builder.add(OcamlJavaConstants.VERBOSE_OPTION);
for (final String includePath : includePaths) {
if (!StringUtils.isBlank(includePath)) {
builder.add(OcamlJavaConstants.INCLUDE_DIR_OPTION).add(
includePath);
}
}
if (!StringUtils.isBlank(classNamePrefix)) {
builder.add(OcamlJavaConstants.CLASS_NAME_PREFIX_OPTION);
builder.add(classNamePrefix);
}
if (!StringUtils.isBlank(classNameSuffix)) {
builder.add(OcamlJavaConstants.CLASS_NAME_SUFFIX_OPTION);
builder.add(classNameSuffix);
}
if (libraryInitMode != null) {
builder.add(OcamlJavaConstants.LIBRARY_INIT_OPTION);
builder.add(libraryInitMode.toCommandLineValue());
}
if (!StringUtils.isBlank(libraryArgs)) {
builder.add(OcamlJavaConstants.LIBRARY_ARGS_OPTION);
builder.add(String.format("\"%s\"", libraryArgs));
}
if (!StringUtils.isBlank(libaryPackage)) {
builder.add(OcamlJavaConstants.LIBRARY_PACKAGE_OPTION);
builder.add(libaryPackage);
}
if (!StringUtils.isBlank(packageName) && !isDynamicPackageMode()) {
builder.add(OcamlJavaConstants.PACKAGE_OPTION);
builder.add(this.packageName);
}
if (stringMapping != null) {
builder.add(OcamlJavaConstants.STRING_MAPPING_OPTION);
builder.add(stringMapping.toCommandLineValue());
}
if (isDynamicPackageMode()) {
builder.addAll(Collections2.transform(files,
new Function<String, String>() {
@Override
public String apply(final String file) {
return fileAtPackage(file, packageNames.get(file));
}
}));
} else {
builder.addAll(files);
}
return Optional.of(builder.build().toArray(new String[] {}));
}
@Override
protected String chooseTargetJar() {
return targetJar;
}
@Override
protected String chooseTargetOcamlJar() {
return targetOcamlJar;
}
@Override
protected File chooseOcamlSourcesDirectory() {
return ocamlSourceDirectory;
}
@Override
protected String chooseOcamlCompiledSourcesTarget() {
return ocamlCompiledSourcesTarget;
}
}