package mandelbrot.ocamljava_maven_plugin;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Properties;
import java.util.regex.Pattern;
import mandelbrot.dependency.data.DependencyGraph;
import mandelbrot.ocamljava_maven_plugin.util.FileGatherer;
import mandelbrot.ocamljava_maven_plugin.util.FileMappings;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationOutputHandler;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.Invoker;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.codehaus.plexus.util.StringUtils;
import org.ocamljava.runtime.kernel.AbstractNativeRunner;
import org.ocamljava.runtime.kernel.FalseExit;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
public abstract class OcamlJavaAbstractMojo extends AbstractMojo {
private static final String OFFLINE_MODE = "-o";
public static final String DEPENDENCIES_FILE_NAME = "dependencies.json";
public static final String FORK_PROPERTY_NAME = "ocamljava.maven.plugin.fork";
/***
* The name of the generated ocaml dependency graph file, to be place in the
* target directory (usually one of <code>target/ocaml-bin</code> or
* </code>target/ocaml-tests</code>)
*/
@Parameter(defaultValue=DEPENDENCIES_FILE_NAME)
protected String dependencyGraphTarget = DEPENDENCIES_FILE_NAME;
@Parameter(readonly=true, required=true, defaultValue="${project}")
protected MavenProject project;
/***
* The plugin descriptor.
*/
@Parameter(readonly=true, required=true, defaultValue="${descriptor}")
protected PluginDescriptor descriptor;
/***
* Project's output directory, usually <code>target</code>.
*
*/
@Parameter(required=true, property="project.build.directory")
protected final File outputDirectory = new File("");
/***
* The target subfolder to hold all compiled ocaml sources. This value is
* combined with the build output directory (usually <code>target</code>) to
* create an actual file path.
*/
@Parameter(defaultValue="ocaml-bin")
protected String ocamlCompiledSourcesTarget;
/***
* Project's source directory.
*/
@Parameter(defaultValue="src/main/ocaml")
protected File ocamlSourceDirectory = new File("src/main/ocaml");
/***
* The target jar to depend on and possibly replace with ocaml compiled
* sources depending on the value of the <code>replaceMainArtifact</code>
* parameter.
*
*/
@Parameter(required=true, defaultValue="${project.artifactId}-${project.version}.jar")
protected String targetJar;
/***
* The target jar created by the ocamljava jar creation tool. If
* <code>replaceMainArtifact</code> is set to <code>true</code>, then this
* jar will replace the contents of the <code>targetJar</code> parameter.
*
*/
@Parameter(required=true, defaultValue="${project.artifactId}-${project.version}-ocaml.jar")
protected String targetOcamlJar;
public String getOcamlCompiledSourcesTargetFullPath() {
return outputDirectory.getPath() + File.separator
+ chooseOcamlCompiledSourcesTarget();
}
protected abstract String chooseOcamlCompiledSourcesTarget();
/***
* The target test jar to depend on and possibly replace with ocaml compiled
* sources depending on the value of the <code>replaceMainArtifact</code>
* parameter.
*/
@Parameter(required=true, defaultValue="${project.artifactId}-${project.version}-tests.jar")
protected String targetTestJar;
/***
* The target test jar created by the ocamljava jar creation tool. If
* <code>replaceMainArtifact</code> is set to <code>true</code>, then this
* jar will replace the contents of the <code>targetTestJar</code>
* parameter.
*/
@Parameter(required=true, defaultValue="${project.artifactId}-${project.version}-ocaml-tests.jar")
protected String targetTestOcamlJar;
/***
* The target subfolder to hold all compiled ocaml test sources. This value
* is combined with the build output directory (usually <code>target</code>)
* to create an actual file path.
*
*/
@Parameter(defaultValue="ocaml-tests")
protected String ocamlCompiledTestsTarget;
/***
* Project's source directory as specified in the POM.
*/
@Parameter(defaultValue="src/test/java")
protected final File ocamlTestDirectory = new File("src/test/ocaml");
/***
* Sets how java packages are determined for the code generated classes.
* <p>
* If set to <code>DYNAMIC</ocde>, a java package will be inferred according to the folder
* structure of the modules. For instance,
* <code>"src/main/ocaml/foo/bar/lib.ml"</code> will generate
* <code>package foo.bar</code> at the top of <code>LibWrapper.java</code>.</p><p>
* To fix the package name for all compiled module interfaces, set this
* value to <code>FIXED</code> and fill in the {@link #packageName}
* parameter accordingly.
* </p>
*
**/
@Parameter(defaultValue="FIXED")
protected JavaPackageMode javaPackageMode = JavaPackageMode.FIXED;
public JavaPackageMode getJavaPackageMode() {
return javaPackageMode;
}
public void setJavaPackageMode(final JavaPackageMode javaPackageMode) {
this.javaPackageMode = javaPackageMode;
}
public static enum JavaPackageMode {
FIXED {
@Override
public String choosePackage(final String dynamicName, final String staticName) {
return staticName;
}
}, DYNAMIC {
@Override
public String choosePackage(final String dynamicName, final String staticName) {
return dynamicName;
}
};
public static JavaPackageMode getDefaultValue() {
return DYNAMIC;
}
public abstract String choosePackage(String dynamicName, String staticName);
}
/***
* Sets the java package name for each source file.
*
**/
@Parameter(defaultValue="")
protected String packageName;
protected String toPackage(final File prefixToTruncate, final String path) {
if (isDynamicPackageMode()) {
return FileMappings.toPackage(prefixToTruncate, path);
} else {
return packageName;
}
}
public boolean isDynamicPackageMode() {
return JavaPackageMode.DYNAMIC.equals(javaPackageMode);
}
/***
* Whether to enable extensions that allow ocaml modules to access plain
* java objects.
*
*/
@Parameter(defaultValue="true")
protected boolean javaExtensions;
protected Multimap<String, String> gatherOcamlSourceFiles(final File root) {
return new FileGatherer(this).gatherFiles(root,
OcamlJavaConstants.OCAML_SOURCE_FILE_EXTENSIONS);
}
protected void addIncludePaths(final Collection<String> includePaths,
final ImmutableCollection.Builder<String> builder) {
for (final String includePath : includePaths) {
if (!StringUtils.isBlank(includePath)) {
builder.add(OcamlJavaConstants.INCLUDE_DIR_OPTION).add(
includePath);
}
}
}
protected void checkForErrors(final String message,
final AbstractNativeRunner main) throws MojoExecutionException {
final Field declaredField;
try {
declaredField = getExceptionField();
} catch (final NoSuchFieldException e) {
throw new MojoExecutionException(message, e);
}
final boolean accessible = declaredField.isAccessible();
try {
declaredField.setAccessible(true);
final Throwable exception = (Throwable) declaredField.get(main);
if (exception != null) {
if (exception instanceof FalseExit) {
final FalseExit f = (FalseExit) exception;
switch (f.getExitCode()) {
case 0:
break;
default:
throw new MojoExecutionException(message
+ " (exit code = " + f.getExitCode() + ")");
}
} else
throw new MojoExecutionException(message, exception);
}
} catch (final MojoExecutionException e) {
throw e;
} catch (final Exception e) {
throw new MojoExecutionException(message, e);
} finally {
declaredField.setAccessible(accessible);
main.clearException();
}
}
// This seems to be only the way to access the exception protected field
// from the ocaml main object at this time.
private static Field getExceptionField() throws NoSuchFieldException {
return AbstractNativeRunner.class.getDeclaredField("exception");
}
public File chooseDependencyGraphTargetFullPath() {
return new File(getOcamlCompiledSourcesTargetFullPath()
+ File.separator + dependencyGraphTarget);
}
protected DependencyGraph getDependendyGraph(
final Multimap<String, String> filesByExtension)
throws MojoExecutionException {
final FileInputStream inputStream;
try {
inputStream = new FileInputStream(new File(
Collections2
.filter(filesByExtension.get(OcamlJavaConstants.JSON_EXTENSION),
dependencyGraphFileExists())
.iterator().next()));
} catch (final FileNotFoundException e) {
throw new MojoExecutionException(
"missing or corrupt dependency graph: "
+ dependencyGraphTarget + ", can't wrap!", e);
}
final DependencyGraph dependencyGraph = DependencyGraph
.read(inputStream);
return dependencyGraph;
}
protected Predicate<CharSequence> dependencyGraphFileExists() {
return Predicates.contains(Pattern
.compile(dependencyGraphTarget
.replace(".", "\\.")));
}
protected File chooseOcamlSourcesDirectory() {
return ocamlSourceDirectory;
}
/***
* <p>Invokes a goal on a separate process programmatically using the Maven invoker tool.</p>
* @param goal the maven goal, such as <code>jar:package</code> or <code>mandebrlot:ocamljava-maven-plugin:compile</code>.
* @param forkAgain sets a system property ({@value #FORK_PROPERTY_NAME}} as a hint to the invoking process on whether it should fork once more.
* @return the invocation result.
* @throws MojoExecutionException if an invocation exception occurs, or the invoked process did not exit with a return value of 0.
*/
protected InvocationResult invokePlugin(final String goal, final boolean forkAgain) throws MojoExecutionException {
return invokePlugin(goal, forkAgain, null, null);
}
protected InvocationResult invokePlugin(final String goal, final boolean forkAgain, final Properties properties) throws MojoExecutionException {
return invokePlugin(goal, forkAgain, properties, null);
}
protected InvocationResult invokePlugin(final String goal, final boolean forkAgain, final InvocationOutputHandler outputHandler) throws MojoExecutionException {
return invokePlugin(goal, forkAgain, null, outputHandler);
}
/***
* <p>Invokes a goal on a separate process programmatically using the Maven invoker tool.</p>
* @param goal the maven goal, such as <code>jar:package</code> or <code>mandebrlot:ocamljava-maven-plugin:compile</code>.
* @param forkAgain sets a system property ({@value #FORK_PROPERTY_NAME}} as a hint to the invoking process on whether it should fork once more.
* @param properties system properties to pass to the Maven command.
* @param outputHandler
* @return the invocation result.
* @throws MojoExecutionException if an invocation exception occurs, or the invoked process did not exit with a return value of 0.
*/
protected InvocationResult invokePlugin(final String goal, final boolean forkAgain, Properties properties, final InvocationOutputHandler
outputHandler) throws MojoExecutionException {
Preconditions.checkNotNull(project, "no project defined- this plugin must be invoked on a maven project!");
properties = properties == null ? new Properties() : (Properties)properties.clone();
properties.put(FORK_PROPERTY_NAME, Boolean.valueOf(forkAgain).toString());
final InvocationRequest defaultInvocationRequest = new DefaultInvocationRequest()
.setDebug(getLog().isDebugEnabled())
.setShellEnvironmentInherited(true)
.setMavenOpts(System.getenv("MAVEN_OPTS"))
.setGoals(ImmutableList.of(goal))
.setProperties(properties)
.setOffline(isOffline())
.setOutputHandler(outputHandler)
.setPomFile(project.getFile());
final Invoker invoker = new DefaultInvoker();
try {
final InvocationResult execution = invoker
.execute(defaultInvocationRequest);
switch (execution.getExitCode()) {
case 0:
return execution;
default:
throw new MojoExecutionException(
"process did not exit cleanly (exit code = " + execution.getExitCode() + ")");
}
} catch (final MavenInvocationException e) {
throw new MojoExecutionException("problem during fork operation", e);
}
}
protected boolean isOffline() {
final Optional<String> optional = Optional.fromNullable(System.getProperty("sun.java.command"));
if (optional.isPresent()) {
return ImmutableSet.copyOf(Splitter.on(" ").split(optional.get())).contains(OFFLINE_MODE);
}
return false;
}
protected static String fileAtPackage(final String fileName, final String packageName) {
if (StringUtils.isBlank(packageName))
return fileName;
return String.format("%s@%s", fileName, packageName);
}
}