So I’ve been continuing to play with my CDI-and-linking idea, and central to it is the ability to locate CDI “modules” Out There In The World™. The world, in this case, is Maven Central. Well, or perhaps a local mirror of it. Or maybe your employer’s private Nexus repository fronting it. Oh jeez, we’re going to have to really use Maven’s innards to do this, aren’t we?
As noted earlier, even just figuring out what innards to use is hard. So I figured out that the project formerly known as Æther, Maven Artifact Resolver, whose artifact identifier is maven-resolver, is the one to grab.
Then, upon receiving it and opening it up, I realized that the whole thing is driven by Guice—or, if you aren’t into that sort of thing, by a homegrown service locator (which itself is a service, which leads to all sorts of other Jamie Zawinski-esque questions).
The only recipes left over are from the old Æther days and require a bit of squinting to make work. They are also staggeringly complicated. Here’s a gist that downloads the (arbitrarily selected) org.microbean:microbean-configuration-cdi:0.1.0
artifact and its transitive, compile
-scoped dependencies, taking into account local repositories, the user’s Maven ~/.m2/settings.xml
file, active Maven profiles and other things that we all take for granted:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.File; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.List; | |
import java.util.Map; | |
import org.apache.maven.settings.Mirror; | |
import org.apache.maven.settings.Profile; | |
import org.apache.maven.settings.Repository; | |
import org.apache.maven.settings.Settings; | |
import org.apache.maven.repository.internal.MavenRepositorySystemUtils; | |
import org.apache.maven.settings.building.DefaultSettingsBuilder; | |
import org.apache.maven.settings.building.DefaultSettingsBuilderFactory; | |
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest; | |
import org.apache.maven.settings.building.SettingsBuildingResult; | |
import org.apache.maven.settings.building.SettingsBuilder; | |
import org.apache.maven.settings.building.SettingsBuildingException; | |
import org.apache.maven.settings.building.SettingsProblem; | |
import org.eclipse.aether.DefaultRepositoryCache; | |
import org.eclipse.aether.DefaultRepositorySystemSession; | |
import org.eclipse.aether.RepositorySystem; | |
import org.eclipse.aether.RepositorySystemSession; | |
import org.eclipse.aether.artifact.Artifact; | |
import org.eclipse.aether.artifact.DefaultArtifact; | |
import org.eclipse.aether.collection.CollectRequest; | |
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; | |
import org.eclipse.aether.graph.Dependency; | |
import org.eclipse.aether.graph.DependencyFilter; | |
import org.eclipse.aether.impl.DefaultServiceLocator; | |
import org.eclipse.aether.impl.DefaultServiceLocator.ErrorHandler; | |
import org.eclipse.aether.internal.impl.DefaultRepositorySystem; | |
import org.eclipse.aether.repository.LocalRepository; | |
import org.eclipse.aether.repository.LocalRepositoryManager; | |
import org.eclipse.aether.repository.RemoteRepository; | |
import org.eclipse.aether.repository.RemoteRepository.Builder; | |
import org.eclipse.aether.resolution.ArtifactDescriptorRequest; | |
import org.eclipse.aether.resolution.ArtifactDescriptorResult; | |
import org.eclipse.aether.resolution.ArtifactResult; | |
import org.eclipse.aether.resolution.DependencyRequest; | |
import org.eclipse.aether.resolution.DependencyResult; | |
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; | |
import org.eclipse.aether.spi.connector.transport.TransporterFactory; | |
import org.eclipse.aether.spi.locator.ServiceLocator; | |
import org.eclipse.aether.transfer.AbstractTransferListener; | |
import org.eclipse.aether.transfer.TransferEvent; | |
import org.eclipse.aether.transport.file.FileTransporterFactory; | |
import org.eclipse.aether.transport.http.HttpTransporterFactory; | |
import org.eclipse.aether.util.artifact.JavaScopes; | |
import org.eclipse.aether.util.filter.DependencyFilterUtils; | |
import org.eclipse.aether.util.repository.DefaultMirrorSelector; | |
import org.junit.Test; | |
import static org.junit.Assert.assertEquals; | |
import static org.junit.Assert.assertNotNull; | |
public class TestMavenResolverUsage { | |
public TestMavenResolverUsage() { | |
super(); | |
} | |
@Test | |
public void testEverything() throws Exception { | |
// See | |
// https://github.com/eclipse/aether-demo/blob/master/aether-demo-snippets/src/main/java/org/eclipse/aether/examples/util/Booter.java | |
// et al. for general (undocumented) recipe. | |
final DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); | |
assertNotNull(serviceLocator); | |
serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); | |
serviceLocator.addService(TransporterFactory.class, FileTransporterFactory.class); | |
serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); | |
serviceLocator.setErrorHandler(new ErrorHandler() { | |
@Override | |
public final void serviceCreationFailed(final Class<?> type, final Class<?> impl, final Throwable exception) { | |
if (exception != null) { | |
exception.printStackTrace(); | |
} | |
} | |
}); | |
final RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); | |
assertNotNull(repositorySystem); | |
final Settings settings = getSettings(); | |
assertNotNull(settings); | |
final DefaultRepositorySystemSession repositorySystemSession = MavenRepositorySystemUtils.newSession(); | |
assertNotNull(repositorySystemSession); | |
repositorySystemSession.setTransferListener(new TransferListener()); | |
repositorySystemSession.setOffline(settings.isOffline()); | |
repositorySystemSession.setCache(new DefaultRepositoryCache()); | |
final Collection<? extends Mirror> mirrors = settings.getMirrors(); | |
if (mirrors != null && !mirrors.isEmpty()) { | |
final DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector(); | |
for (final Mirror mirror : mirrors) { | |
assert mirror != null; | |
mirrorSelector.add(mirror.getId(), | |
mirror.getUrl(), | |
mirror.getLayout(), | |
false, /* not a repository manager; settings.xml does not encode this information */ | |
mirror.getMirrorOf(), | |
mirror.getMirrorOfLayouts()); | |
} | |
repositorySystemSession.setMirrorSelector(mirrorSelector); | |
} | |
String localRepositoryString = settings.getLocalRepository(); | |
if (localRepositoryString == null) { | |
localRepositoryString = System.getProperty("user.home") + "/.m2/repository"; | |
} | |
final LocalRepository localRepository = new LocalRepository(localRepositoryString); | |
final LocalRepositoryManager localRepositoryManager = repositorySystem.newLocalRepositoryManager(repositorySystemSession, localRepository); | |
assertNotNull(localRepositoryManager); | |
repositorySystemSession.setLocalRepositoryManager(localRepositoryManager); | |
List<RemoteRepository> remoteRepositories = new ArrayList<>(); | |
final Map<String, Profile> profiles = settings.getProfilesAsMap(); | |
if (profiles != null && !profiles.isEmpty()) { | |
final Collection<String> activeProfileKeys = settings.getActiveProfiles(); | |
if (activeProfileKeys != null && !activeProfileKeys.isEmpty()) { | |
for (final String activeProfileKey : activeProfileKeys) { | |
final Profile activeProfile = profiles.get(activeProfileKey); | |
if (activeProfile != null) { | |
final Collection<Repository> repositories = activeProfile.getRepositories(); | |
if (repositories != null && !repositories.isEmpty()) { | |
for (final Repository repository : repositories) { | |
if (repository != null) { | |
Builder builder = new Builder(repository.getId(), repository.getLayout(), repository.getUrl()); | |
final org.apache.maven.settings.RepositoryPolicy settingsReleasePolicy = repository.getReleases(); | |
if (settingsReleasePolicy != null) { | |
final org.eclipse.aether.repository.RepositoryPolicy releasePolicy = new org.eclipse.aether.repository.RepositoryPolicy(settingsReleasePolicy.isEnabled(), settingsReleasePolicy.getUpdatePolicy(), settingsReleasePolicy.getChecksumPolicy()); | |
builder = builder.setReleasePolicy(releasePolicy); | |
} | |
final org.apache.maven.settings.RepositoryPolicy settingsSnapshotPolicy = repository.getSnapshots(); | |
if (settingsSnapshotPolicy != null) { | |
final org.eclipse.aether.repository.RepositoryPolicy snapshotPolicy = new org.eclipse.aether.repository.RepositoryPolicy(settingsSnapshotPolicy.isEnabled(), settingsSnapshotPolicy.getUpdatePolicy(), settingsSnapshotPolicy.getChecksumPolicy()); | |
builder = builder.setSnapshotPolicy(snapshotPolicy); | |
} | |
final RemoteRepository remoteRepository = builder.build(); | |
assert remoteRepository != null; | |
remoteRepositories.add(remoteRepository); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
final RemoteRepository mavenCentral = new Builder("central", "default", "http://central.maven.org/maven2/").build(); | |
assert mavenCentral != null; | |
remoteRepositories.add(mavenCentral); | |
remoteRepositories = repositorySystem.newResolutionRepositories(repositorySystemSession, remoteRepositories); | |
assertNotNull(remoteRepositories); | |
final Artifact artifact = new DefaultArtifact("org.microbean", "microbean-configuration-cdi", "jar", "0.1.0"); | |
final DependencyFilter classpathFilter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE); | |
final CollectRequest collectRequest = new CollectRequest(); | |
collectRequest.setRoot(new Dependency(artifact, JavaScopes.COMPILE)); | |
collectRequest.setRepositories(remoteRepositories); | |
// collectRequest.setRepositories(Collections.singletonList(mavenCentral)); | |
final DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, classpathFilter); | |
final DependencyResult dependencyResult = repositorySystem.resolveDependencies(repositorySystemSession, dependencyRequest); | |
assertNotNull(dependencyResult); | |
final List<ArtifactResult> artifactResults = dependencyResult.getArtifactResults(); | |
assertNotNull(artifactResults); | |
} | |
public static final Settings getSettings() throws SettingsBuildingException { | |
final SettingsBuilder settingsBuilder = new DefaultSettingsBuilderFactory().newInstance(); // this method should be static! | |
assert settingsBuilder != null; | |
final DefaultSettingsBuildingRequest settingsBuildingRequest = new DefaultSettingsBuildingRequest(); | |
settingsBuildingRequest.setSystemProperties(System.getProperties()); | |
// settingsBuildingRequest.setUserProperties(userProperties); // TODO: implement this | |
settingsBuildingRequest.setGlobalSettingsFile(new File("/usr/local/maven/conf/settings.xml")); // TODO: do this for real | |
settingsBuildingRequest.setUserSettingsFile(new File(new File(System.getProperty("user.home")), ".m2/settings.xml")); | |
final SettingsBuildingResult settingsBuildingResult = settingsBuilder.build(settingsBuildingRequest); | |
assert settingsBuildingResult != null; | |
final List<SettingsProblem> settingsBuildingProblems = settingsBuildingResult.getProblems(); | |
if (settingsBuildingProblems != null && !settingsBuildingProblems.isEmpty()) { | |
throw new SettingsBuildingException(settingsBuildingProblems); | |
} | |
return settingsBuildingResult.getEffectiveSettings(); | |
} | |
private static final class TransferListener extends AbstractTransferListener { | |
private TransferListener() { | |
super(); | |
} | |
@Override | |
public void transferInitiated(final TransferEvent event) { | |
System.out.println("*** transfer initiated: " + event); | |
} | |
@Override | |
public void transferStarted(final TransferEvent event) { | |
System.out.println("*** transfer started: " + event); | |
} | |
@Override | |
public void transferProgressed(final TransferEvent event) { | |
System.out.println("*** transfer progressed: " + event); | |
} | |
@Override | |
public void transferSucceeded(final TransferEvent event) { | |
System.out.println("*** transfer succeeded: " + event); | |
} | |
@Override | |
public void transferCorrupted(final TransferEvent event) { | |
System.out.println("*** transfer corrupted: " + event); | |
} | |
@Override | |
public void transferFailed(final TransferEvent event) { | |
System.out.println("*** transfer failed: " + event); | |
} | |
} | |
} |
That seems like an awful lot of work to have to do just to get some stuff over the wire. It also uses the cheesy homegrown service locator which as we all know is not The Future™.
For my purposes, I wanted to junk the service locator and run this from within a CDI 2.0 environment, both because it would be cool and dangerous and unexpected, and because the whole library was written assuming dependency injection in the first place.
So I wrote a portable extension that basically does the job that the cheesy homegrown service locator does, but deferring all the wiring and validation work to CDI, where it belongs.
As if this whole thing weren’t hairy enough already, a good number of the components involved are Plexus components. Plexus was a dependency injection framework and container from a ways back now that also had a notion of what constituted beans and injection points. They called them components and requirements.
So a good portion of some of the internal Maven objects are annotated with Component
and Requirement
. These correspond roughly—very, very roughly—to bean-defining annotations and injection points, respectively.
So I wrote two portable extension methods. One uses the role
element from Component
to figure out what kind of Typed
annotation to add to Plexus components. The other turns a Requirement
annotation with a hint
into a valid CDI injection point with an additional qualifier.
(Along the way, these uncovered MNG-6190, which indicates that not very many people are even using the Maven Artifact Resolver project in any way, or at least not from within a dependency injection container, which is, of course, how it is designed to be used. That’s a shame, because although it is overengineered and fiddly to the point of being virtually inscrutable, it is, as a result, perhaps, quite powerful.)
Then the rest of the effort was split between finding the right types to add into the CDI container, and figuring out how to adapt certain Guice-ish conventions to the CDI world.
The end result is that the huge snarling gist above gets whittled down to five or so lines of code, with CDI doing the scope management and wiring for you.
This should mean that you can now relatively easily incorporate the guts of Maven into your CDI applications for the purposes of downloading and resolving artifacts on demand. See the microbean-maven-cdi
project for more information. Thanks for reading.