Maven: get all dependencies programmatically

Puce picture Puce · Nov 25, 2016 · Viewed 9.1k times · Source

How can I programmatically get all dependencies of a Maven module outside a Maven execution environment?

So far I have:

via maven-core:

Path pomPath = ...;
MavenXpp3Reader reader = new MavenXpp3Reader();
try (InputStream is = Files.newInputStream(pomPath)) {
    Model model = reader.read(is);
    this.mavenProject = new MavenProject(model);
}

and via jcabi-aether:

File localRepo = Paths.get(System.getProperty("user.home"), ".m2").toFile();
new Classpath(mavenProject, localRepo, "runtime")

Is this generally correct so far?

The issue now is that I'm getting a NullPointerException:

Caused by: java.lang.NullPointerException
    at com.jcabi.aether.Aether.mrepos(Aether.java:197)
    at com.jcabi.aether.Aether.<init>(Aether.java:140)
    at com.jcabi.aether.Classpath.<init>(Classpath.java:125)

since mavenProject.getRemoteProjectRepositories() returns null.

How can I initialze the MavenProject to contain the configured remote repos taking the settings.xml file (mirrors, proxies, repositories etc.) into account as well?

Answer

Tunaki picture Tunaki · Nov 26, 2016

Outside of a Maven plugin, the way to operate on artifacts is through Aether. The team has a sample project to get the transitive dependencies of a given artifact called ResolveTransitiveDependencies. Once you have the Aether dependencies set up (like shown here), you can simply have:

public static void main(final String[] args) throws Exception {
    DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
    RepositorySystem system = newRepositorySystem(locator);
    RepositorySystemSession session = newSession(system);

    RemoteRepository central = new RemoteRepository.Builder("central", "default", "http://repo1.maven.org/maven2/").build();

    Artifact artifact = new DefaultArtifact("group.id:artifact.id:version");

    CollectRequest collectRequest = new CollectRequest(new Dependency(artifact, JavaScopes.COMPILE), Arrays.asList(central));
    DependencyFilter filter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE);
    DependencyRequest request = new DependencyRequest(collectRequest, filter);
    DependencyResult result = system.resolveDependencies(session, request);

    for (ArtifactResult artifactResult : result.getArtifactResults()) {
        System.out.println(artifactResult.getArtifact().getFile());
    }
}

private static RepositorySystem newRepositorySystem(DefaultServiceLocator locator) {
    locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
    locator.addService(TransporterFactory.class, FileTransporterFactory.class);
    locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
    return locator.getService(RepositorySystem.class);
}

private static RepositorySystemSession newSession(RepositorySystem system) {
    DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
    LocalRepository localRepo = new LocalRepository("target/local-repo");
    session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
    return session;
}

It will download the artifacts and place them into "target/local-repo".

Note that you can configure proxy and mirrors with the DefaultProxySelector and DefaultMirrorSelector on the system session. It would be possible to read the Maven settings file and use it to populate the session, but things get really ugly really fast...


When you want tight coupling with Maven itself because you have access to the POM to process and want to take the settings into account, it is a lot simpler to directly invoke Maven programmatically. In this case, you're interested in the path of each dependencies, including transitive dependencies, of a given POM file. For that the dependency:list goal, together with setting the outputAbsoluteArtifactFilename to true, will give (almost) exactly that.

To invoke Maven programmatically, it is possible to use the Invoker API. Adding the dependency to your project:

<dependency>
  <groupId>org.apache.maven.shared</groupId>
  <artifactId>maven-invoker</artifactId>
  <version>2.2</version>
</dependency>

you can have:

InvocationRequest request = new DefaultInvocationRequest();
request.setPomFile(new File(pomPath));
request.setGoals(Arrays.asList("dependency:list"));
Properties properties = new Properties();
properties.setProperty("outputFile", "dependencies.txt"); // redirect output to a file
properties.setProperty("outputAbsoluteArtifactFilename", "true"); // with paths
properties.setProperty("includeScope", "runtime"); // only runtime (scope compile + runtime)
// if only interested in scope runtime, you may replace with excludeScope = compile
request.setProperties(properties);

Invoker invoker = new DefaultInvoker();
// the Maven home can be omitted if the "maven.home" system property is set
invoker.setMavenHome(new File("/path/to/maven/home"));
invoker.setOutputHandler(null); // not interested in Maven output itself
InvocationResult result = invoker.execute(request);
if (result.getExitCode() != 0) {
    throw new IllegalStateException("Build failed.");
}

Pattern pattern = Pattern.compile("(?:compile|runtime):(.*)");
try (BufferedReader reader = Files.newBufferedReader(Paths.get("dependencies.txt"))) {
    while (!"The following files have been resolved:".equals(reader.readLine()));
    String line;
    while ((line = reader.readLine()) != null && !line.isEmpty()) {
        Matcher matcher = pattern.matcher(line);
        if (matcher.find()) {
            // group 1 contains the path to the file
            System.out.println(matcher.group(1));
        }
    }
}

This creates an invocation request which contains: the goals to invoke and the system properties, just like you would launch mvn dependency:list -Dprop=value on the command-line. The path to the settings to use will default to the standard location of "${user.home}/settings.xml", but it would also be possible to specify the path to the settings with request.setUserSettingsFile(...) and request.setGlobalSettingsFile(...). The invoker needs to be set the Maven home (i.e. installation directory), but only if the "maven.home" system property isn't set.

The result of invoking dependency:list is redirected to a file, that is later post-processed. The output of that goal consists of the list of dependencies in the format (the classifier may be absent, if there are none):

group.id:artifact.id:type[:classifier]:version:scope:pathname

There isn't a way to output only the path of the resolved artifact's file, and the fact that the classifier may be absent complicates the parsing a bit (we can't split on : with a limit, since the path could contain a :...). First, the resolved artifacts are below the line "The following files have been resolved:" in the output file, then, since the wanted scope are only compile or runtime, we can get the path of the artifact's file with a simple regular expression that takes everything which is after compile: or runtime:. That path can then directly be used as a new File.

If the hoops during post-processing look too fragile, I guess you could create your own plugin that just outputs the resolved artifact's filename.