Creating a Maven resolver

This post is part of a multipart series about creating a graph off all available Maven dependencies.

Resolving maven dependencies is something we in general leave to our dependency management system, Maven, Ivvy, …. For the purpose to the application we are developing we required a detailed control on how the the dependencies are resolved and the JSon representation of this sub-graph. I start off with a short intro on where to find documentation on the Maven resolving mechanism and then go into the details for each of the parts that are required to do so.

###Getting_Started The Maven documentation on how dependencies are resolved in the code base is, well let’s say not really available. We turned to the implementation of the maven-dependency plugin for guidance on how it is done. As it turns out the repository system used in maven is actually something from the Eclipse framework namely Aether.

Aether is initially developed by Sonatype and later transferred to Eclipse. The Maven system depends heavily on this implementation. When you start implementing the Maven dependency mechanism you will end up with the entire Plexus and Maven distribution in your application. Besides the fact that it is huge you get all kinds of dependencies you do not want.

Aether solves this problem. > It’s an easily embeddable Java library to work with artifact repositories, enabling you to fetch artifacts from remote repositories for local consumption and to publish local artifacts to remote repositories for sharing with others.1

It provides you with a library you can easily interact with Repositories. Aether itself can not interact with Maven repositories but provides a framework for this. Apache Maven provides a extension on the Aether provider, maven-aether-provider. With this you can easily interact with Maven repositories.

Aether’s implementation consists of a bunch of components that need to be wired together to get a complete repository system. To satisfy different requirements and smoothly integrate into existing applications, Aether offers multiple ways to do so but we choose to go for the ServiceLocator mechanism.

In order to resolve a Maven artifact you need three components:

  1. RepositorySystem
  2. RemoteRepository
  3. RepositorySession

RepositorySystem

As mentioned before, we choose to utilize the ServiceLoader mechanism. The documentation of Aether provides a piece of sample code that you can copy.

import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.connector.wagon.WagonProvider;
import org.eclipse.aether.connector.wagon.WagonRepositoryConnectorFactory;

...
    private static RepositorySystem newRepositorySystem()
    {
        DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
        locator.addService( RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class );
        locator.addService( TransporterFactory.class, FileTransporterFactory.class );
        locator.addService( TransporterFactory.class, HttpTransporterFactory.class );

        return locator.getService( RepositorySystem.class );
    }
...

Once created you have the basis for a Repository system the The correct repository system is loaded based on the dependencies of your project. In our case this is the maven-aether-provider remember you can not resolve Maven dependencies just yet.

RemoteRepository

The remote repository is the important one, we do not have all available artifacts. i.e. we are not a Mirror of Maven central. The remote repository allows us to download missing artifacts to the local repository.

For the purpose of the application we created a Maven remote repository with Sonatype Nexus. For more information on the capabilities of this check out the product page.

Initialization of the remote repository:

new RemoteRepository("central", "default", "http://nexus/nexus/content/repositories/maven");

RepositorySession

Aether and its components are designed to be stateless and as such all configuration/state has to be passed into the methods. During the session these settings are unlikely to change.

import org.apache.maven.repository.internal.MavenRepositorySystemUtils;

...
    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;
    }

The LocalRepository variable acts as your cache for the artifacts that you resolved. The layout of this directory matches that of your local Maven repository.

Resolving artifacts

Now that all components are wired we can start resolving Artifacts.

The code snippet below resolves an artifact with scope compile. i.e. It resolves all dependencies of the given artifact at compile time. Using compile time scope provides the most usable result for our project.


String artifactCoordinate = "org.apache.maven.plugins:maven-compiler-plugin:2.3";
            DefaultArtifact artifact = new DefaultArtifact(artifactCoordinate);

CollectRequest collectRequest = new CollectRequest();
        collectRequest.setRoot(new Dependency(artifact, JavaScopes.COMPILE)); 
        collectRequest.addRepository(repo);

        CollectResult collectResult = system.collectDependencies(session, collectRequest);

The CollectResult provides a tree like structure for all the dependencies. The image below shows a similar structure:

That is all fine, but a tree it is not a Graph or a JSon structure?

Processing the result

The CollectResult containtains the tree of artifacts. It starts with a RootNode, this root node will have children and can have children as well. This structure represents the good old composite pattern. A composite pattern allows for an other pattern to be present as well, the Visitor pattern. Fortunate for us the developers of Aether did just that.

The Visitor we implemented (an extension of the class DependencyVisitor) takes note of the first time it is called and skips all the dependencies that are more than two levels deep. Leaving us with just the Node visited that we are interested in.

Once the Visitor is done we query the visitor for the dependency graph.

    public boolean visitEnter(DependencyNode node) {
        currentIndentation += 1;

        // get the source node and remember it
        if (currentIndentation == 1) {
            firstLevelArtifactVertex = getArtifactVertexFromArtifactCoordinate(node.getDependency());
        }
        // get the nodes on the second level (the direct dependencies), and add these with the first node to the graph
        else if (currentIndentation == 2) {
            ArtifactVertex secondLevelArtifactVerteX = getArtifactVertexFromArtifactCoordinate(node.getDependency());
            Scope scope = deriveScope(node.getDependency());
            localDependencies.addDependency(firstLevelArtifactVertex, secondLevelArtifactVerteX, scope);
        } 

        return true;
    }

 public boolean visitLeave(DependencyNode node) {
        currentIndentation -= 1;
        return true;
    }

Implementation

The implementation of the model can be found in the Github repository