Skip to content

Latest commit

 

History

History
930 lines (754 loc) · 38.2 KB

File metadata and controls

930 lines (754 loc) · 38.2 KB

SmallMind Artifact

The smallmind-artifact module is a thin, opinionated Java facade over the Eclipse Aether / Maven Resolver stack. It gives a running JVM the ability to:

  • read the user’s ~/.m2/settings.xml and construct a fully configured Aether repository system from it — mirrors, proxies, server credentials, profile-activated repositories, and local-cache path included;

  • resolve Maven artifacts by coordinate (groupId / artifactId / version / classifier / extension), downloading them to the local repository if they are not already present;

  • resolve the full transitive compile-scope dependency closure of an artifact, producing a flat array of local files suitable for classpath construction;

  • monitor a set of coordinates on a polling cycle, detect when any of them has been re-published (release upgrade or snapshot redeploy), and hand the caller a ClassLoader over the new artifact set so code can be reloaded without restarting the JVM.

The module is small by design: two data-transfer objects, one repository facade, one scanner, one event, and one listener interface. It exists as the narrow slice of Aether that most applications actually need, packaged so that a caller never has to write their own settings parser, session builder, selector chain, or dependency visitor.

Maven Resolver (Aether) is the programmatic artifact-resolution engine that Maven itself uses, and it is the canonical way to fetch Maven artifacts from within a JVM. It is also, out of the box, hostile to casual integration. Building a working RepositorySystemSession from scratch requires assembling a RepositorySystemSupplier, parsing settings.xml with DefaultSettingsBuilderFactory, translating every settings-level entity (Mirror, Proxy, Server, Profile, Repository) into its Aether equivalent, wiring four different selectors (ProxySelector, MirrorSelector, AuthenticationSelector, and a LocalRepositoryManager), and finally stitching a sensible RepositoryPolicy onto each remote repository. The public API surface for this wiring is large, and the "correct" arrangement of its parts is mostly tribal knowledge.

smallmind-artifact does that wiring once, the way a normal Maven command line would do it, and exposes a small facade — MavenRepository — whose two methods cover the vast majority of what non-build-tool code ever wants from Aether: fetch one artifact, or fetch one artifact and its transitive dependencies.

On top of that, the module provides MavenScanner, which turns the same facade into a hot-reload primitive: a polling component that resolves a set of monitored coordinates on a configurable interval and delivers a ClassLoader whenever the underlying artifacts change. This is the mechanism a long-running service uses to swap in a new plugin jar, a new rules module, or a new integration adapter, without a JVM restart and without the service needing to know anything about Maven internals.

  • Reads settings.xml — global or per-directory, falling back to ~/.m2/settings.xml. Active profiles, mirrors, proxies, server credentials (password or SSH private key), and the <localRepository> element are all honored.

  • Builds a reusable RepositorySystem backed by the official RepositorySystemSupplier from Maven Resolver, with the HTTP and file transports and the basic connector already on the classpath.

  • Produces fresh DefaultRepositorySystemSession instances on demand. Each session gets a new DefaultRepositoryCache, the configured offline flag, a merged view of all active-profile properties, and the fully wired selector chain.

  • Resolves single artifacts to local files via MavenRepository.acquireArtifact.

  • Resolves full dependency closures via MavenRepository.resolve, walking the dependency graph depth-first, skipping optional dependencies, deduplicating by artifact identity, and downloading any node whose local file is missing.

  • Polls for artifact changes via MavenScanner, distinguishing release changes (coordinate identity) from snapshot redeploys (coordinate identity plus file modification time).

  • Delivers a GatingClassLoader — the nutsnbolts class-loader abstraction — over every changed artifact’s transitive compile dependency set, so listeners can Class.forName(…​) against the new code without restarting.

  • It does not build projects. There is no compile step, no maven-invoker integration, no plugin execution. The module resolves pre-existing artifacts; it does not produce them.

  • It does not resolve runtime or test scope by itself. MavenRepository.resolve requests the compile scope through CollectRequest.setRoot(new Dependency(artifact, "compile")); callers who need other scopes or richer filters should invoke Aether directly using the session produced by generateSession().

  • It does not manage passwords, tokens, or secrets outside of settings.xml. Whatever credentials exist in the user’s Maven configuration are what the facade uses; there is no separate credential store.

  • It does not cache parsed settings across instances. Each MavenRepository constructor reads and resolves settings.xml once; share the instance if you want that parse reused.

  • It does not, in MavenScanner, enforce any particular classloading discipline on listeners. What a listener does with the supplied ClassLoader — install it as the thread context loader, instantiate a specific class through it, register its contents with a plugin framework — is the listener’s decision.

Goal Entry point

Resolve one artifact jar and get its local file

MavenRepository.acquireArtifact(session, coordinate)

Resolve an artifact plus all compile-scope dependencies

MavenRepository.resolve(session, artifact)

Re-fetch the same artifacts from inside a long-lived process, noticing snapshot redeploys

MavenScanner.start() with a MavenScannerListener

Build a classpath at runtime from a list of Maven coordinates

MavenRepository.resolve(…​) then feed the returned files to any URLClassLoader / ClasspathClassGate chain

Run against a pre-populated local repository, no network

Construct MavenRepository(…​, offline=true)

Use a Maven settings file from a location other than ~/.m2

Three-arg MavenRepository(settingsDirectory, repositoryId, offline)

Class Role

MavenCoordinate

Mutable DTO describing a Maven artifact by groupId, artifactId, version, optional classifier, and an extension that defaults to "jar". Primary input to MavenRepository resolution methods and to MavenScanner. Implements equals / hashCode over all five fields.

MavenRepository

The facade. Constructed once per settings.xml; produces fresh Aether sessions on demand and resolves artifacts and dependency closures against the configured remote repositories.

ArtifactTag

Change-detection token. Pairs a resolved Aether Artifact with the last-modified timestamp of its backing file. Release artifacts compare by identity; snapshots also compare by file timestamp.

MavenScanner

Polling component. Monitors an array of MavenCoordinate instances, resolves them on every cycle, compares the results against stored ArtifactTag values, and notifies listeners when anything has changed.

MavenScannerListener

Observer interface. Single method artifactChange(MavenScannerEvent), invoked on the scanner’s worker thread when a cycle detects at least one change.

MavenScannerEvent

EventObject delivered to listeners. Carries the full delta map (new artifact → prior artifact), the current artifact array in coordinate order, and the ClassLoader spanning all changed artifacts and their transitive dependencies.

A handful of terms recur through the API and the Aether stack underneath, and naming them explicitly makes the examples much easier to read.

Coordinate

A five-field address for a Maven artifact: groupId, artifactId, version, classifier, extension. MavenCoordinate is the module’s representation; org.eclipse.aether.artifact.Artifact is Aether’s. The facade translates from the former to the latter on demand.

Session

An Aether DefaultRepositorySystemSession carrying the selectors, the local-repository manager, the cache, and per-request configuration properties. A session is cheap to build; it is not thread-safe. MavenRepository.generateSession() produces a new one per call.

Remote repository

An Aether RemoteRepository — the destination for HTTP or file-transport requests. The facade derives one per active-profile <repository> and <pluginRepository> in settings.xml, applies the mirror selector, and attaches authentication. Every remote repository is forced to UPDATE_POLICY_ALWAYS / CHECKSUM_POLICY_WARN so that snapshot redeployments are actually fetched.

Artifact tag

The MavenScanner change-detection unit. A tag is (resolved artifact, file mtime). Two tags compare equal if they wrap the same coordinate and, when the coordinate is a snapshot, the file mtimes match. This is the distinction that lets the scanner tell 1.2.3-SNAPSHOT from 1.2.3-SNAPSHOT after a redeploy.

Delta map

The payload of a MavenScannerEvent. Keys are the newly resolved artifacts; values are the artifacts they replaced (or null on first observation). Unchanged coordinates do not appear in the map.

Gating class loader

The GatingClassLoader from smallmind-nutsnbolts. The scanner assembles one per notification, with a ClasspathClassGate for each changed artifact and each of its transitive compile dependencies. The loader’s parent is the thread context class loader at the time the worker runs, so existing framework classes remain resolvable; only the updated artifacts are reloaded.

The module is published as a single artifact. The Aether dependencies ship with compile scope, because applications using the module almost always want them on the runtime classpath. The three maven-* artifacts are declared provided, because they are usually contributed by whatever Maven-aware runtime is hosting the code; if your environment does not already supply them, add them explicitly.

<dependency>
  <groupId>org.smallmind</groupId>
  <artifactId>artifact-maven</artifactId>
  <version>${project.version}</version>
</dependency>

Additional direct dependencies in a standalone deployment:

<dependency>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-model-builder</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-resolver-provider</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-settings</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-settings-builder</artifactId>
</dependency>

The module also pulls in smallmind-nutsnbolts (for Stint, ClassGate, ClasspathClassGate, GatingClassLoader, and ComponentStatus) and scribe-pen (for logging from the scanner worker). Both are standard transitive dependencies.

Note

The module requires a valid settings.xml at the configured location. If ~/.m2/settings.xml does not exist, construction of MavenRepository will raise SettingsBuildingException. A minimal empty settings file is sufficient — the module does not require any specific element to be present — but the file itself must exist and parse.

The most basic operation: fetch an artifact’s jar into the local Maven repository and get back a java.io.File pointing at it.

import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.smallmind.artifact.maven.MavenCoordinate;
import org.smallmind.artifact.maven.MavenRepository;

MavenRepository repository = new MavenRepository("my-service", false);      // (1)
DefaultRepositorySystemSession session = repository.generateSession();       // (2)

MavenCoordinate coordinate = new MavenCoordinate(
    "com.example", "plugin-foo", "1.0.0");                                   // (3)

Artifact artifact = repository.acquireArtifact(session, coordinate);         // (4)
java.io.File jar = artifact.getFile();                                       // (5)
  1. Reads ~/.m2/settings.xml. "my-service" is the User-Agent identifier sent to remote repositories. offline=false permits network access.

  2. Produce a fresh session. A session is single-use in the sense that it is not thread-safe — do not share one between threads — but reusing one across several serial calls on a single thread is fine.

  3. Default extension is "jar" and classifier is null, matching the 99% case.

  4. Returns the resolved Aether artifact with getFile() populated. If the coordinate is not present in any configured remote repository (or the local cache when offline), this throws ArtifactResolutionException.

  5. artifact.getFile() is the local cache location, typically under ~/.m2/repository/com/example/plugin-foo/1.0.0/plugin-foo-1.0.0.jar.

Use this when you need a classpath, not just one jar — e.g. to construct a child class loader for dynamically loaded code.

import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.smallmind.artifact.maven.MavenRepository;

MavenRepository repository = new MavenRepository("my-service", false);
DefaultRepositorySystemSession session = repository.generateSession();

Artifact root = new DefaultArtifact("com.example:plugin-foo:1.0.0");         // (1)
Artifact[] closure = repository.resolve(session, root);                      // (2)

for (Artifact resolved : closure) {
  System.out.println(resolved.getFile().getAbsolutePath());                  // (3)
}
  1. DefaultArtifact understands Aether’s group:artifact:version shorthand. You may also pass a MavenCoordinate through acquireArtifact first if you prefer the DTO form; the root just needs to be an Aether Artifact.

  2. The returned array is the deduplicated dependency closure walked depth-first. Order is not guaranteed. Optional dependencies are excluded.

  3. Every artifact in the closure has a populated getFile().

Important

resolve throws DependencyCollectionException if any POM in the dependency graph cannot be fetched or parsed, and DependencyResolutionException if Aether’s final resolution pass fails. Individual artifact download failures during graph traversal surface as a RuntimeException wrapping an ArtifactResolutionException, because the failure happens inside Aether’s DependencyVisitor callback where a checked exception cannot be thrown directly.

Feeding the closure into a child class loader is the standard plugin-loader pattern. The module does not ship a helper for this step because the shape of the child loader varies by application — most callers want either a plain URLClassLoader or a GatingClassLoader from smallmind-nutsnbolts.

import java.net.URL;
import java.net.URLClassLoader;
import org.eclipse.aether.artifact.Artifact;

URL[] urls = new URL[closure.length];
for (int i = 0; i < closure.length; i++) {
  urls[i] = closure[i].getFile().toURI().toURL();
}

ClassLoader pluginLoader = new URLClassLoader(
    urls, Thread.currentThread().getContextClassLoader());

Class<?> pluginClass = pluginLoader.loadClass("com.example.plugin.Entry");
Object plugin = pluginClass.getDeclaredConstructor().newInstance();

If you need the gating semantics (per-class filtering, per-jar tickets) that the scanner uses internally, use ClasspathClassGate and GatingClassLoader from smallmind-nutsnbolts directly.

This is the primary reason to reach past MavenRepository and use MavenScanner. Given one or more coordinates, the scanner polls the repositories at the configured interval and hands you an updated class loader whenever a snapshot has been redeployed or a release version has changed.

import org.smallmind.artifact.maven.MavenCoordinate;
import org.smallmind.artifact.maven.MavenScanner;
import org.smallmind.artifact.maven.MavenScannerEvent;
import org.smallmind.artifact.maven.MavenScannerListener;
import org.smallmind.nutsnbolts.time.Stint;

import java.util.concurrent.TimeUnit;

MavenCoordinate plugin = new MavenCoordinate(
    "com.example", "rules-module", "2.0.0-SNAPSHOT");

MavenScanner scanner = new MavenScanner(
    "rules-host",                                                           // (1)
    false,                                                                   // (2)
    new Stint(30, TimeUnit.SECONDS),                                         // (3)
    plugin);                                                                 // (4)

scanner.addMavenScannerListener(new MavenScannerListener() {
  @Override
  public void artifactChange (MavenScannerEvent event) {
    ClassLoader loader = event.getClassLoader();                             // (5)
    try {
      Class<?> entry = loader.loadClass("com.example.rules.Entry");
      Object rules = entry.getDeclaredConstructor().newInstance();
      RulesHostRegistry.install(rules);                                      // (6)
    } catch (ReflectiveOperationException reflectiveException) {
      // Handle or log.
    }
  }
});

scanner.start();                                                             // (7)

// ... application runs ...

scanner.stop();                                                              // (8)
  1. User-Agent identifier for outbound HTTP requests.

  2. offline=false permits network access; true restricts the scanner to whatever is already in the local ~/.m2/repository.

  3. Cycle interval. Any java.util.concurrent.TimeUnit-based Stint works.

  4. One or more coordinates to monitor. Each is tracked independently.

  5. The loader is already wired with the changed artifacts and their compile dependencies; its parent is the thread context loader that was active when the worker invoked the scan.

  6. What to do with the loader is application-specific; the module takes no opinion beyond supplying it.

  7. Performs an initial synchronous scan — which will always fire the listener because every tag starts as null, so every coordinate registers as "changed" the first time — and then launches a daemon worker thread.

  8. Signals the worker to stop and blocks until it exits.

A CI runner or a reproducible-build environment may want to guarantee that no network traffic happens — all artifacts must be pre-populated in the local repository.

MavenRepository offline = new MavenRepository("ci-runner", true);            // (1)
DefaultRepositorySystemSession session = offline.generateSession();
Artifact artifact = offline.acquireArtifact(session, coordinate);            // (2)
  1. offline=true propagates to every session produced by generateSession() via session.setOffline(true).

  2. If the artifact is missing from the local cache this throws ArtifactResolutionException without attempting a remote fetch.

Packaged environments often carry their own settings.xml — a container image that pre-configures mirrors for an internal proxy, for example.

MavenRepository repository = new MavenRepository(
    "/opt/app/conf",      // directory containing settings.xml                (1)
    "my-service",
    false);
  1. The path is the containing directory, not the file itself. The constructor appends /settings.xml. Passing null or an empty string reverts to ~/.m2.

A mutable DTO. The no-argument constructor leaves every field unset; setters must be called before the coordinate is usable. The convenience constructors progressively fill more fields:

Constructor Use when

MavenCoordinate()

Building up a coordinate from external data (e.g. parsed config). Every setter must be called except setClassifier (optional) before the coordinate is passed to Aether.

MavenCoordinate(groupId, artifactId, version)

Standard jar artifacts. Extension defaults to "jar", classifier to null.

MavenCoordinate(groupId, artifactId, classifier, version)

Sources jars, tests jars, javadoc jars — anywhere the classifier is the only deviation from the jar default.

MavenCoordinate(groupId, artifactId, classifier, extension, version)

Non-jar packaging: "pom", "war", "zip", "tar.gz", etc.

equals and hashCode include all five fields, with null classifier and null extension treated as equal to each other but distinct from any non-null value.

Two constructors, parallel except for the settings directory:

// Reads ~/.m2/settings.xml
MavenRepository (String repositoryId, boolean offline)

// Reads <settingsDirectory>/settings.xml; null/empty falls back to ~/.m2
MavenRepository (String settingsDirectory, String repositoryId, boolean offline)

Three operations:

generateSession()

Returns a fresh DefaultRepositorySystemSession. Each session gets its own DefaultRepositoryCache, merged user properties from every active profile, the shared selector chain, and a LocalRepositoryManager anchored at the settings <localRepository> (falling back to ~/.m2/repository).

acquireArtifact(session, coordinate) and acquireArtifact(session, artifact)

Resolves a single artifact. The coordinate-accepting overload constructs a DefaultArtifact and delegates to the artifact-accepting overload. Throws ArtifactResolutionException on any resolution failure.

resolve(session, artifact)

Resolves the root artifact and its full compile-scope dependency closure. Returns a deduplicated Artifact[]. See the Behavioral Reference section for exact graph-walk semantics.

Two constructors, parallel except for the settings directory:

MavenScanner (String repositoryId, boolean offline,
              Stint cycleStint, MavenCoordinate... mavenCoordinates)

MavenScanner (String settingsDirectory, String repositoryId, boolean offline,
              Stint cycleStint, MavenCoordinate... mavenCoordinates)

cycleStint is required and must be non-null; mavenCoordinates is required and must be non-null (though an empty array is technically allowed, a scanner with nothing to monitor is a degenerate case).

Lifecycle:

addMavenScannerListener(listener) / removeMavenScannerListener(listener)

Register and deregister. May be called before or after start(). Listeners registered before start() receive the initial "everything is new" notification.

start()

Idempotent. If the scanner is already running, returns immediately. Otherwise performs one synchronous scan on the calling thread — any resolution failures propagate here — and launches a daemon worker for subsequent cycles.

stop()

Idempotent. Signals the worker to finish the current cycle and exit, then blocks on the exit latch. Callers that want non-blocking shutdown should invoke stop() from a thread that can afford to wait.

Three getters:

getArtifactDeltaMap()

Only the artifacts that changed this cycle. Keys are the new resolved artifacts, values are the prior artifacts they replaced (or null on first observation).

getArtifacts()

Every monitored coordinate, in registration order. Entries are null for coordinates that have not been successfully resolved yet.

getClassLoader()

A GatingClassLoader over the changed artifacts plus their transitive compile dependencies. Parent is the thread context class loader active when the scan ran.

Single method artifactChange(MavenScannerEvent). Called on the scanner’s worker thread. Implementations must be thread-safe or must hand the event off to a dedicated executor.

Sessions are intentionally cheap and short-lived. generateSession() does no I/O of its own — it wires pre-computed selectors, a new cache, and a fresh local-repo manager onto a DefaultRepositorySystemSession. A session is valid for as long as the MavenRepository instance that produced it is alive; sharing selectors, settings, and RepositorySystem across sessions is what makes the facade inexpensive to reuse.

A session is not thread-safe. Calling acquireArtifact or resolve with the same session from multiple threads is undefined. The recommended pattern is one session per resolution operation (or per sequential operation chain) on a single thread.

MavenRepository.resolve walks the root’s dependency graph with a custom DependencyVisitor:

  • Nodes whose Dependency.isOptional() returns true are skipped entirely; their children are also skipped because visitEnter returns the membership-check result, not the optional flag, so a reference that is already in visitedSet will also cause visitEnter to return false and cut off that branch.

  • A HashSet<DependencyNode> keyed by object identity prevents the same node from being processed twice in diamond graphs.

  • A HashSet<Artifact> deduplicates by artifact identity across branches — the same coordinate reached through two different paths appears once in the returned array.

  • For each kept node, if the dependency’s artifact has no File yet, acquireArtifact is called to download it. An ArtifactResolutionException during this download is wrapped in a RuntimeException because the visitor’s contract does not allow checked exceptions.

  • After the depth-first walk, Aether’s repositorySystem.resolveDependencies is invoked on the root node to perform a final top-down resolution pass. This is how Aether normally verifies the graph, and it may raise DependencyResolutionException if any node that survived the walk cannot be resolved.

The practical implication: the returned array is what Aether actually made available as local files for the compile scope, minus optional branches, deduplicated.

Each cycle resolves every monitored coordinate fresh (no per-coordinate caching between cycles) and wraps the result in an ArtifactTag. The tag is compared to the stored one for that coordinate index:

  • On first scan (tag is null) every coordinate registers as "changed." The initial start() call will always fire the listener for every coordinate, with null values in the delta map.

  • For release artifacts (Artifact.isSnapshot() == false) the comparison is artifact identity only. A release redeployed with the same version will not be detected, because Maven releases are conceptually immutable. If you really mean to redeploy a release and want it observed, bump the version.

  • For snapshot artifacts both the artifact identity and the local file’s lastModified() timestamp must match. A snapshot redeploy, which refreshes the local file’s mtime (because UPDATE_POLICY_ALWAYS forces a download), will register as changed.

When at least one coordinate has changed, all changed coordinates' transitive dependencies are resolved and combined into a single GatingClassLoader. Unchanged coordinates are not re-walked that cycle.

The initial scan runs on the calling thread and propagates any resolution exception:

  • ArtifactResolutionException

  • DependencyCollectionException

  • DependencyResolutionException

Subsequent scans run on the daemon worker. Any exception thrown inside updateArtifact() is caught, logged at ERROR level through LoggerManager.getLogger(MavenScanner.class), and swallowed. The cycle loop continues unless the worker is interrupted or stop() is called. This is the deliberate choice that a transient network failure should not kill the scanner.

A failed scan leaves the stored ArtifactTag array unchanged, so the next successful scan will still detect the change that the failed scan missed.

MavenRepository

Construction is single-threaded. After construction, generateSession() and the two resolution methods are safe to call from multiple threads provided each thread uses its own session. The shared RepositorySystem, Settings, selectors, and remoteRepositoryList are effectively immutable after the constructor returns.

MavenScanner

All four public lifecycle methods (start, stop, addMavenScannerListener, removeMavenScannerListener) are synchronized. updateArtifact() is also synchronized, so a caller that invokes stop() while a scan is in progress will wait for that scan to finish before the status flag flips. Listeners are invoked on the worker thread, not on the thread that called addMavenScannerListener.

MavenScannerListener implementations

Must tolerate being called from a daemon worker thread. If the listener needs to run on a specific thread (an application’s event loop, a Swing EDT, a reactor thread), it must marshal the event itself.

The constructor builds an effective Settings object by running DefaultSettingsBuilderFactory().newInstance().build(request) over a DefaultSettingsBuildingRequest whose global settings file is the file at <settingsDirectory>/settings.xml. This is Maven’s own settings builder, so profile activation, interpolation, and global/user merging all behave the way mvn would.

From the effective settings the facade extracts:

  • All profiles. Profiles are considered active if their id appears in <activeProfiles> or if their <activation> block sets <activeByDefault>true</activeByDefault>.

  • Every active profile’s <repositories> and <pluginRepositories>, each translated into a RemoteRepository with both release and snapshot policies forced to UPDATE_POLICY_ALWAYS / CHECKSUM_POLICY_WARN.

  • All mirrors, installed on a DefaultMirrorSelector.

  • All proxies, installed on a DefaultProxySelector with optional basic auth and non-proxy-hosts patterns.

  • All servers, installed on a DefaultAuthenticationSelector wrapped in ConservativeAuthenticationSelector. Each server contributes a username/password pair and/or an SSH private-key path with passphrase.

  • The <localRepository> path, or ~/.m2/repository if unset.

Profile <properties> blocks are merged into each session’s user properties every time generateSession() is called — see the Defaults Reference for the subtle last-profile-wins behavior this implies.

Setting Default

MavenCoordinate.extension

"jar"

MavenCoordinate.classifier

null

Settings directory when constructor arg is null/empty

~/.m2

Local repository when <localRepository> is absent

~/.m2/repository

RemoteRepository release policy

UPDATE_POLICY_ALWAYS, CHECKSUM_POLICY_WARN

RemoteRepository snapshot policy

UPDATE_POLICY_ALWAYS, CHECKSUM_POLICY_WARN

CollectRequest root dependency scope

"compile"

MavenRepository.resolve optional-dependency handling

Skipped

User-Agent header format

Maven-Repository/<repositoryId> (Java <version>; <os.name> <os.version>) Aether

Note

The session-user-properties population loop in generateSession calls session.setUserProperties(profile.getProperties()) once per active profile, replacing rather than merging. In practice the last active profile’s <properties> wins. If you need merged properties across profiles, compose them yourself before constructing the repository or mutate the returned session.

No plugin execution

This module resolves artifacts; it does not run Maven plugins. A coordinate that only exists as a Maven plugin binding (not as a consumable artifact) is still fetchable as a jar, but nothing in smallmind-artifact will invoke its mojos.

Compile scope only

MavenRepository.resolve hard-codes the root dependency scope to "compile". To pull runtime or test scope, or to apply a DependencyFilter, use the repositorySystem and session produced by the facade directly — the facade does not expose the root scope as a parameter.

Release redeploys not detected

MavenScanner intentionally compares release artifacts by identity only. If your workflow republishes a release version in place, the scanner will not notice. This matches Maven’s semantic model of releases as immutable; it is a design choice, not an oversight.

Last-profile-wins user properties

generateSession overwrites user properties per active profile rather than merging them. If two active profiles define the same property with different values, the value from the profile encountered last in the iteration order wins.

Exception boxing during transitive resolution

An ArtifactResolutionException thrown while downloading a transitive dependency inside resolve is wrapped in an unchecked RuntimeException because the Aether DependencyVisitor interface does not permit checked exceptions. Callers that want to react specifically to resolution failures during traversal must unwrap the cause.

Scanner listener errors are not handled

The scanner does not catch exceptions thrown by listeners. A listener that throws will propagate through updateArtifact() into the worker loop’s catch-all, which logs and continues — but the remaining listeners registered for that event will not be invoked.

Single cycle interval

Every coordinate shares the same cycle Stint. There is no way to poll one coordinate more frequently than another within the same scanner. If you need heterogeneous intervals, run multiple scanners.

The three maven-* artifacts (maven-model-builder, maven-resolver-provider, maven-settings, maven-settings-builder) are declared provided in the module POM. When embedding the module in a standalone application, you must put them on the runtime classpath yourself. In most container or plugin hosts they are already present.

Aether’s HTTP transport depends on Apache HttpClient. The maven-resolver-transport-http dependency pulls it in transitively. If your application already ships a different HttpClient major version, verify that the versions are compatible.

Credentials are taken exclusively from the <servers> block of settings.xml. There is no mechanism for programmatic credential injection. To override credentials for a specific deployment, ship a dedicated settings.xml and point the second constructor at it.

Each MavenScannerEvent carries a fresh GatingClassLoader. Previous class loaders handed to earlier listeners will be garbage-collected only once every reference to them — including any instances they loaded, any classes still reachable from the JVM’s class metadata, and any resources they opened — is released. A listener that installs the loader globally without releasing the previous one will retain every generation of classes ever loaded, which is a classpath leak. The standard remediation is to discard the previous loader explicitly as part of the listener’s install step.

The scanner’s worker swallows resolution exceptions and keeps polling. This is usually the right choice for a long-running service — a five-minute artifact-repository outage should not take the plugin host down — but it means a misconfigured repository will fail silently after the initial scan succeeds. Watch the ERROR-level log output of org.smallmind.artifact.maven.MavenScanner in production.

offline=true sets RepositorySystemSession.setOffline(true). Aether interprets this as "do not attempt any remote operation" — missing artifacts raise ArtifactResolutionException without trying the network. The User-Agent string is still constructed (it is assigned to configProps regardless) but will never be sent anywhere.

Use this list as a pre-flight before shipping a service that embeds the module:

  • ❏ Confirm settings.xml exists at the configured location and parses (construct a MavenRepository in a startup health check).

  • ❏ Confirm every expected remote repository appears in repository.generateSession() output (inspect the `LocalRepositoryManager’s parent session, or enable debug logging on Aether).

  • ❏ For scanners, register listeners before calling start() so that the initial "everything is new" notification is delivered.

  • ❏ For scanners, ensure stop() is called from a shutdown hook or container lifecycle callback; the worker is a daemon thread and will not keep the JVM alive, but in-flight downloads may be abandoned mid-transfer.

  • ❏ For scanners, verify listener code releases references to the previous ClassLoader before installing the new one.

  • ❏ For offline deployments, pre-populate ~/.m2/repository (or the configured local repo) with every expected artifact and its transitive dependencies.

  • Maven Resolver (Aether) — the underlying resolution engine.

  • Maven settings reference — the element names consumed by MavenRepository.

  • org.smallmind.nutsnbolts.lang.GatingClassLoader and org.smallmind.nutsnbolts.lang.ClasspathClassGate — the class-loading primitives the scanner hands to listeners.

  • org.smallmind.nutsnbolts.time.Stint — the duration abstraction used for the scanner cycle interval.