Monday, 7 February 2011

Maven Dependency Finder

My friend Mark had an idea for a utility to search a list of your favourite repositories for a given dependency. So I thought I'd code up a version.

Maven Dependency Finder is a command line utility that takes a list of repositories and a dependency group id,artifact id and version, and returns the repositories which contain the JAR you want.

The utility reads a list of repositories from a file, and uses multiple threads to check for the required JARs, outputting the URLs where the JARs are hosted.

MavenDependencyFinder.java

package org.adrianwalker.maven;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public final class MavenDependencyFinder {

  private static final int EXIT_FAILURE = -1;
  private static final int EXIT_SUCCESS = 0;
  private static final String FLAG_START = "-";
  private static final String GROUP_FLAG = FLAG_START + "g";
  private static final String ARTIFACT_FLAG = FLAG_START + "a";
  private static final String VERSION_FLAG = FLAG_START + "v";
  private static final String MAX_THREADS_FLAG = FLAG_START + "t";
  private static final String REPOSITORY_FILE_FLAG = FLAG_START + "r";
  private static final String HELP_FLAG = FLAG_START + "h";
  private static final String DEFAULT_VERSION = "";
  private static final List<String> DEFAULT_REPOSITORIES = Arrays.asList(new String[]{
            "http://mirrors.ibiblio.org/pub/mirrors/maven2/"
          });
  private static final int DEFAULT_MAX_THREADS = 2;
  private static final String ARTIFACT_VERSION_SEPERATOR = "-";
  private static final String EMPTY_STRING = "";
  private static final String HTTP_METHOD = "GET";
  private static final String JAR_FILE_EXTENSION = ".jar";
  private static final String PACKAGE_SEPERATOR = "\\.";
  private static final int TIMEOUT_MILLISECONDS = 10000;
  private static final String URL_SEPERATOR = "/";

  public static void main(final String[] args) {


    Map<String, String> parameters = readParameters(args);

    if (parameters.isEmpty() || parameters.containsKey(HELP_FLAG)) {
      printHelp();
      System.exit(EXIT_SUCCESS);
    }

    if (!parameters.containsKey(GROUP_FLAG)) {
      printError("Required parameter '-g' missing");
      System.exit(EXIT_FAILURE);
    }

    if (!parameters.containsKey(ARTIFACT_FLAG)) {
      printError("Required parameter '-a' missing");
      System.exit(EXIT_FAILURE);
    }

    String groupId = parameters.get(GROUP_FLAG);
    String artifactId = parameters.get(ARTIFACT_FLAG);

    String version = DEFAULT_VERSION;
    if (parameters.containsKey(VERSION_FLAG)) {
      version = parameters.get(VERSION_FLAG);
    }

    int maxThreads = DEFAULT_MAX_THREADS;
    if (parameters.containsKey(MAX_THREADS_FLAG)) {
      maxThreads = Integer.parseInt(parameters.get(MAX_THREADS_FLAG));
    }

    List<String> repositories = DEFAULT_REPOSITORIES;
    if (parameters.containsKey(REPOSITORY_FILE_FLAG)) {
      String fileName = parameters.get(REPOSITORY_FILE_FLAG);
      try {
        repositories = readRepositoriesFile(fileName);
      } catch (Throwable t) {
        printError(String.format("Error reading file '%s'", fileName));
        System.exit(EXIT_FAILURE);
      }
    }

    findDependency(maxThreads, repositories, groupId, artifactId, version);

    System.exit(EXIT_SUCCESS);
  }

  private MavenDependencyFinder() {
  }

  private static void findDependency(final int maxThreads, final List<String> repositories, final String groupId, final String artifactId, final String version) {

    ExecutorService threadPool = Executors.newFixedThreadPool(maxThreads);
    Map<String, Future<Boolean>> results = new HashMap<String, Future<Boolean>>();
    for (String repository : repositories) {
      Future<Boolean> isHostingDependancy = threadPool.submit(new CallableUrlFinder(repository, groupId, artifactId, version));
      results.put(repository, isHostingDependancy);
    }
    Set<String> responses = new HashSet<String>();
    while (results.size() > responses.size()) {

      for(Entry<String, Future<Boolean>> entry : results.entrySet()) {
          
        String repository = entry.getKey();
        Future<Boolean> isHostingDependancy = entry.getValue();
          
        if (!responses.contains(repository) && isHostingDependancy.isDone()) {
          Boolean found;
          try {
            found = isHostingDependancy.get();
            responses.add(repository);
          } catch (Throwable t) {
            found = false;
          }
          if (found) {
            System.out.println(repository);
          }
        }
      }
      Thread.yield();
    }
    threadPool.shutdown();
  }

  private static Map<String, String> readParameters(final String[] args) {
    int argsCount = args.length;

    if (argsCount == 1 && args[0].equals(HELP_FLAG)) {
      printHelp();
      System.exit(EXIT_SUCCESS);
    }else if (argsCount % 2 == 1) {
      printError("Invalid number of arguments");
      System.exit(EXIT_FAILURE);
    }

    Map<String, String> parameters = new HashMap<String, String>();
    for (int i = 0; i < argsCount; i = i + 2) {

      String flag = args[i];

      if (!flag.startsWith(FLAG_START)) {
        printError(String.format("Invalid flag '%s'", flag));
        System.exit(EXIT_FAILURE);
      }

      String value = args[i + 1];
      if (value.startsWith(FLAG_START)) {
        printError(String.format("Invalid value '%s' for flag '%s'", value, flag));
        System.exit(EXIT_FAILURE);
      }


      parameters.put(flag, value);
    }

    return parameters;
  }

  private static List readRepositoriesFile(final String fileName) throws IOException {
    List<String> repositories = new ArrayList<String>();
    BufferedReader reader = null;
    try {
      reader = new BufferedReader(new FileReader(fileName));
      String line;
      while ((line = reader.readLine()) != null) {
        repositories.add(line.trim());
      }
    } finally {
      if (null != reader) {
        reader.close();
      }
    }
    return repositories;
  }

  private static void printHelp() {
    String help = "Usage: mdf -g <group id> -a <artifact id> [-v <version>] [-r <filename>] [-t <threads>] [-h]\n\n"
            + "  -g\tDependency group id\n"
            + "  -a\tDependency artifact id\n"
            + "  -v\tDependency version\n"
            + "  -r\tRepository file\n"
            + "  -t\tMaximum threads\n"
            + "  -h\tHelp";
    System.out.println(help);
  }

  private static void printError(final String message) {
    System.out.println(message);
  }

  private static final class CallableUrlFinder implements Callable<Boolean> {

    private final String repository;
    private final String groupId;
    private final String artifactId;
    private final String version;

    public CallableUrlFinder(final String repository, final String groupId, final String artifactId, final String version) {
      this.repository = repository;
      this.groupId = groupId;
      this.artifactId = artifactId;
      this.version = version;
    }

    @Override
    public Boolean call() throws Exception {
      return isHostingDependency(repository, groupId, artifactId, version);
    }

    private boolean isHostingDependency(final String repository, final String groupId, final String artifactId, final String version) {

      String dependencyUrl = gavToUrl(repository, groupId, artifactId, version);

      HttpURLConnection connection = null;
      try {
        URL url = new URL(dependencyUrl);
        connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod(HTTP_METHOD);
        connection.setReadTimeout(TIMEOUT_MILLISECONDS);
        connection.connect();
        return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
      } catch (Throwable t) {
        return false;
      } finally {
        if (null != connection) {
          connection.disconnect();
        }
      }
    }

    private String gavToUrl(final String repository, final String groupId, final String artifactId, final String version) {
      /*
       * Format:
       *
       * http://mirrors.ibiblio.org/pub/mirrors/maven2/log4j/log4j/1.2.13/log4j-1.2.13.jar
       *
       */

      StringBuilder builder = new StringBuilder();
      builder.append(repository) //
              .append(repository.endsWith(URL_SEPERATOR) ? EMPTY_STRING : URL_SEPERATOR) //
              .append(groupId.replaceAll(PACKAGE_SEPERATOR, URL_SEPERATOR)) //
              .append(URL_SEPERATOR).append(artifactId);
      if (version != null && !version.isEmpty()) {
        builder.append(URL_SEPERATOR) //
                .append(version) //
                .append(URL_SEPERATOR) //
                .append(artifactId) //
                .append(ARTIFACT_VERSION_SEPERATOR) //
                .append(version).append(JAR_FILE_EXTENSION);
      }

      return builder.toString();
    }
  }
}


Build the project with:

mvn clean install
to build a directory and zip assembly containing the executable JAR file, a batch file and shell script to run the utility from the command line.

Running the command with the -h flag shows the help for the utility:

Usage: mdf -g <group id> -a <artifact id> [-v <version>] [-r <filename>] [-t <threads>] [-h]

  -g    Dependency group id
  -a    Dependency artifact id
  -v    Dependency version
  -r    Repository file
  -t    Maximum threads
  -h    Help

To run the utility with a list of repositories and search for log4j:

mdf -g log4j -a log4j -r repositories.txt

outputs:

http://mirrors.ibiblio.org/pub/mirrors/maven2/
http://repository.jboss.org/maven2/

where the repositories list input file contains:

repositories.txt

http://mirrors.ibiblio.org/pub/mirrors/maven2/
http://repository.jboss.org/maven2/
http://download.java.net/maven/2
http://repository.codehaus.org

Source Code