code: /*
   * 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;
    }
  
  }
  