/*
* Copyright (C) 2013 Samuel Halliday
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see [http://www.gnu.org/licenses/].
*/
package com.github.fommil.netlib.generator;
import com.google.common.base.Charsets;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.thoughtworks.paranamer.*;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import javax.annotation.Nullable;
import java.io.File;
import java.lang.reflect.Method;
import java.util.List;
public abstract class AbstractNetlibGenerator extends AbstractMojo {
/**
* Location of the generated source files.
*/
@Parameter(defaultValue = "${project.build.directory}/generated-sources/netlib-java", required = true)
protected File outputDir;
@Parameter(required = true)
protected String outputName;
/**
* The artifact of the jar to generate from.
* Note that this must be listed as a <code>dependency</code>
* section of the calling module, not a plugin <code>dependency</code>.
*/
@Parameter(defaultValue = "net.sourceforge.f2j:arpack_combined_all:jar:0.1", required = true)
protected String input;
/**
* The artifact of the javadocs to extract parameter names.
* Note that this must be listed as a <code>dependency</code>
* section of the calling module, not a plugin <code>dependency</code>.
*/
@Parameter(defaultValue = "net.sourceforge.f2j:arpack_combined_all:jar:javadoc:0.1")
protected String javadoc;
/**
* The package to scan.
*/
@Parameter(required = true)
protected String scan;
/**
* Method names to exclude (regex);
*/
@Parameter
protected String exclude;
@Component
protected MavenProject project;
protected File getFile(String artifactName) {
// artifactMap is a bit too simplistic
for (Artifact artifact : project.getArtifacts())
if (artifact.toString().startsWith(artifactName))
return artifact.getFile();
throw new IllegalArgumentException("could not find " + artifactName + " in " + project.getArtifacts());
}
/**
* Implementation specific interpretation of the parameters.
*
* @param methods obtained from a scan of F2J public static methods.
* @return the file contents for the generated file associated to the parameters.
* @throws Exception
*/
abstract protected String generate(List<Method> methods) throws Exception;
protected Paranamer paranamer = new DefaultParanamer();
@Override
public void execute() throws MojoExecutionException {
try {
project.addCompileSourceRoot(outputDir.getAbsolutePath());
File output = new File(outputDir, outputName);
if (output.exists() && project.getFile().lastModified() < output.lastModified()) {
getLog().info("No changes detected, skipping: " + output);
return;
}
if (Strings.isNullOrEmpty(javadoc))
getLog().warn("Javadocs not attached for paranamer.");
else
paranamer = new CachingParanamer(new JavadocParanamer(getFile(javadoc)));
File jar = getFile(input);
JarMethodScanner scanner = new JarMethodScanner(jar);
List<Method> methods = Lists.newArrayList(
Iterables.filter(scanner.getStaticMethods(scan), new Predicate<Method>() {
@Override
public boolean apply(Method input) {
return exclude == null || !input.getName().matches(exclude);
}
}));
String generated = generate(methods);
output.getParentFile().mkdirs();
getLog().info("Generating " + output.getAbsoluteFile());
Files.write(generated, output, Charsets.UTF_8);
} catch (Exception e) {
throw new MojoExecutionException("java generation", e);
}
}
/**
* @param method
* @return parameters names for the netlib interface.
*/
protected List<String> getNetlibJavaParameterNames(Method method, boolean offsets) {
final List<String> params = Lists.newArrayList();
iterateRelevantParameters(method, offsets, new ParameterCallback() {
@Override
public void process(int i, Class<?> param, String name, String offsetName) {
params.add(name);
}
});
return params;
}
/**
* @param method
* @return canonical parameter types for the netlib interface.
*/
protected List<String> getNetlibJavaParameterTypes(Method method, boolean offsets) {
final List<String> types = Lists.newArrayList();
iterateRelevantParameters(method, offsets, new ParameterCallback() {
@Override
public void process(int i, Class<?> param, String name, String offsetName) {
types.add(param.getCanonicalName());
}
});
return types;
}
protected interface ParameterCallback {
void process(int i, Class<?> param, String name, @Nullable String offsetName);
}
/**
* Calls the callback with every parameter of the method, skipping out the offset parameter
* introduced by F2J for array arguments.
*
* @param method
* @param callback
*/
protected void iterateRelevantParameters(Method method, boolean offsets, ParameterCallback callback) {
if (method.getParameterTypes().length == 0)
return;
String[] names = new String[0];
try {
names = paranamer.lookupParameterNames(method, true);
} catch (ParameterNamesNotFoundException e) {
getLog().warn(e);
}
for (int i = 0; i < method.getParameterTypes().length; i++) {
Class<?> param = method.getParameterTypes()[i];
if (i > 0 && !offsets && param == Integer.TYPE && method.getParameterTypes()[i - 1].isArray()) {
continue;
}
String name;
if (names.length > 0)
name = names[i];
else
name = "arg" + i;
String offsetName = null;
if (i < method.getParameterTypes().length - 1
&& param.isArray()
&& method.getParameterTypes()[i + 1] == Integer.TYPE)
offsetName = names[i+1];
callback.process(i, param, name, offsetName);
}
}
public boolean hasOffsets(Method method) {
Class<?> last = null;
for (int i = 0; i < method.getParameterTypes().length; i++) {
Class<?> param = method.getParameterTypes()[i];
if (last != null && last.isArray() && param.equals(Integer.TYPE))
return true;
last = param;
}
return false;
}
}