Skip to content

feat(features): decouple features service from Pax URL - #2928

Open
jbonofre wants to merge 7 commits into
apache:mainfrom
jbonofre:feature/decouple-pax-url-spi
Open

jbonofre wants to merge 7 commits into
apache:mainfrom
jbonofre:feature/decouple-pax-url-spi

Conversation

@jbonofre

Copy link
Copy Markdown
Member

Why

org.apache.karaf.features.core linked directly against org.ops4j.pax.url.mvn, and used ops4j-base + pax-swissbox for property resolution. That made Pax URL a hard dependency of the core provisioning path, so a distribution without Pax URL was not possible and the resolver could not be refactored independently.

This removes that coupling without changing anything users see. Pax URL stays in the standard distribution, same bundle, same configuration, same behaviour.

Relates to #2212.

What

A Karaf owned resolver contract — org.apache.karaf.features.spi, exported by features core:

public interface MavenResolver extends Closeable {
    enum RetryChance { NEVER, LOW, HIGH, UNKNOWN }
    File resolve(String url) throws IOException;
    File resolve(String url, Exception previousException) throws IOException;
    default File resolve(String groupId, String artifactId, String classifier,
                         String extension, String version) throws IOException { ... }
    default RetryChance isRetryableException(Exception exception) { ... }
}

public interface MavenResolverFactory {
    String getConfigurationPid();
    MavenResolver create(Dictionary<String, String> configuration, String propertyPrefix);
}

Providers are found as an OSGi service inside the framework, and with ServiceLoader outside it, so the assembly Builder and the karaf-maven-plugin keep working unchanged.

The Pax URL binding moved out into a new bundle, org.apache.karaf.maven.resolver.paxurl. It registers a MavenResolverFactory and a MavenResolver built lazily from the org.ops4j.pax.url.mvn PID, so it does not have to wait for ConfigAdmin and configuration changes made before the first resolution are still picked up. It ships in the framework feature at start level 14.

No new startup ordering machinery. The features Activator already declared its requirements, so the factory is just another @RequireService:

org.apache.karaf.features.spi.MavenResolverFactory=
org.osgi.service.cm.ConfigurationAdmin=
org.osgi.service.url.URLStreamHandlerService=(url.handler.protocol\=mvn)

BaseActivator holds doStart() until a provider is present. All three requirements are Karaf or OSGi contracts — none names Pax URL.

Property resolution in FeaturesProcessingSerializer no longer uses ops4j-base / pax-swissbox. PropertyResolver and PropertySubstitutor reimplement the semantics the serializer depends on: system properties take precedence over the supplied dictionary, blank means absent, and unresolved ${...} are left untouched so the existing warning still fires.

The unused tinybundles test dependency is dropped, and feature:export-bundles now consumes the MavenResolver service the provider registers.

Result

org.apache.karaf.features.core and org.apache.karaf.features.command have no org.ops4j dependency, import or private package. Across the whole boot set, exactly one bundle imports org.ops4j.pax.url:

IMPORTS pax.url -> org.apache.karaf.maven.resolver.paxurl

A Pax URL free distribution now only needs a bundle registering a MavenResolverFactory and an mvn: URLStreamHandlerService, plus a framework feature that lists it instead of pax-url-aether + resolver-paxurl. Nothing in the features service has to change.

Behaviour changes

Two, both in previously broken paths:

  • Malformed ${...} placeholders in org.apache.karaf.features.xml are kept verbatim instead of raising EmptyStackException. Well formed input never reaches that path.
  • ReactorMavenResolver implements 3 methods instead of 9 and uses org.apache.karaf.util.maven.Parser rather than the Pax URL internal one.

Optional dependencies are not transitive, so profile, karaf-maven-plugin and itests/test now declare explicitly what they used to inherit from features core.

Testing

  • Full reactor build green.
  • Unit tests green: features core (139), features command (16), profile (13), karaf-maven-plugin (24).
  • The assembled distribution boots, the features service starts, and boot features are provisioned through the new path, including resolving and installing mvn:org.ops4j.pax.url/pax-url-wrap/3.0.3/jar/uber.
  • Generated etc/startup.properties confirms the layering:
mvn:org.ops4j.pax.url/pax-url-aether/3.0.3                        = 5
mvn:org.apache.karaf.maven/org.apache.karaf.maven.resolver.paxurl = 14
mvn:org.apache.karaf.features/org.apache.karaf.features.core      = 15

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Test Results

  729 files  ±0    729 suites  ±0   1h 6m 38s ⏱️ + 7m 37s
1 077 tests ±0    943 ✅ ±0  134 💤 ±0  0 ❌ ±0 
3 231 runs  ±0  2 829 ✅ ±0  402 💤 ±0  0 ❌ ±0 

Results for commit 59b8c67. ± Comparison against base commit 97b0404.

♻️ This comment has been updated with latest results.

@jbonofre

jbonofre commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

FYI, I also updated the manual/documentation to mention the resolver.

jbonofre and others added 6 commits October 16, 2026 10:25
The features service linked directly against org.ops4j.pax.url.mvn, plus
ops4j-base and pax-swissbox for property resolution. That made Pax URL a hard
dependency of the core provisioning path and blocked assembling a distribution
without it.

Introduce org.apache.karaf.features.spi, a Karaf owned resolver contract
(MavenResolver, MavenResolverFactory, MavenResolvers), and consume it
everywhere the features service resolves mvn: URIs. Providers are discovered as
an OSGi service inside the framework and with ServiceLoader outside it, so the
assembly builder and the karaf-maven-plugin keep working unchanged.

The Pax URL binding moves to a new bundle, org.apache.karaf.maven.resolver.paxurl,
which registers a MavenResolverFactory and a MavenResolver built lazily from the
org.ops4j.pax.url.mvn PID. It ships in the standard distribution at start level
14, so behaviour and configuration are unchanged; it is now the only bundle in
the boot set that imports org.ops4j.pax.url.

The features Activator declares the factory with @RequireService, so the
features service simply waits for a provider instead of building a resolver
itself - no new startup ordering machinery.

Property resolution in FeaturesProcessingSerializer no longer uses ops4j-base
and pax-swissbox. PropertyResolver and PropertySubstitutor reimplement the
semantics the serializer relies on: system properties take precedence over the
supplied dictionary, blank means absent, and unresolved ${...} placeholders are
left untouched so the existing warning still fires. Malformed placeholders are
now kept verbatim rather than raising EmptyStackException.

org.apache.karaf.features.core and org.apache.karaf.features.command no longer
have any org.ops4j dependency, import or private package.

Relates to apache#2212
…/download/impl/LocalMavenResolver.java

Co-authored-by: CptBartender <fryderyk.wysocki@gmail.com>
…olver

The features service calls resolve(url, previousException) for every attempt,
with a null exception for the first one. Providing a default implementation
delegating to resolve(url) means a resolver that has no use for the previous
failure does not have to override it.
@jbonofre
jbonofre force-pushed the feature/decouple-pax-url-spi branch from 43bbcb1 to 8484c78 Compare October 16, 2026 08:25
Boot repositories no longer require a mvn URL handler to be registered, which made the features service race with Pax URL at startup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants