From 86564e0d37f722ed1548c38a75c9afed8da3fd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Sun, 20 Sep 2026 20:53:22 +0200 Subject: [PATCH] feat(features): decouple features service from Pax URL 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 #2212 --- assemblies/features/framework/pom.xml | 5 + .../framework/src/main/feature/feature.xml | 2 + .../karaf/features/command/FeatureExport.java | 2 +- features/core/pom.xml | 28 +--- .../internal/download/DownloadManagers.java | 2 +- .../download/impl/MavenDownloadManager.java | 2 +- .../download/impl/MavenDownloadTask.java | 2 +- .../features/internal/osgi/Activator.java | 4 + .../service/FeaturesProcessingSerializer.java | 13 +- .../internal/service/FeaturesServiceImpl.java | 26 +++- .../service/SimpleFeaturesServiceImpl.java | 26 +++- .../internal/util/PropertyResolver.java | 89 ++++++++++++ .../internal/util/PropertySubstitutor.java | 104 ++++++++++++++ .../karaf/features/spi/MavenResolver.java | 108 +++++++++++++++ .../features/spi/MavenResolverFactory.java | 57 ++++++++ .../karaf/features/spi/MavenResolvers.java | 62 +++++++++ .../maven/TestMavenResolverFactory.java | 54 ++++++++ ...he.karaf.features.spi.MavenResolverFactory | 17 +++ itests/test/pom.xml | 6 + maven/pom.xml | 1 + maven/resolver-paxurl/pom.xml | 103 ++++++++++++++ .../maven/resolver/paxurl/Activator.java | 71 ++++++++++ .../resolver/paxurl/LazyMavenResolver.java | 127 ++++++++++++++++++ .../resolver/paxurl/PaxUrlMavenResolver.java | 74 ++++++++++ .../paxurl/PaxUrlMavenResolverFactory.java | 42 ++++++ ...he.karaf.features.spi.MavenResolverFactory | 17 +++ profile/pom.xml | 7 + .../karaf/profile/assembly/Builder.java | 29 ++-- .../assembly/CustomDownloadManager.java | 2 +- tooling/karaf-maven-plugin/pom.xml | 6 + .../org/apache/karaf/tooling/VerifyMojo.java | 6 +- .../tooling/utils/ReactorMavenResolver.java | 38 +----- 32 files changed, 1035 insertions(+), 97 deletions(-) create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/util/PropertyResolver.java create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/util/PropertySubstitutor.java create mode 100644 features/core/src/main/java/org/apache/karaf/features/spi/MavenResolver.java create mode 100644 features/core/src/main/java/org/apache/karaf/features/spi/MavenResolverFactory.java create mode 100644 features/core/src/main/java/org/apache/karaf/features/spi/MavenResolvers.java create mode 100644 features/core/src/test/java/org/apache/karaf/features/internal/maven/TestMavenResolverFactory.java create mode 100644 features/core/src/test/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory create mode 100644 maven/resolver-paxurl/pom.xml create mode 100644 maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/Activator.java create mode 100644 maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/LazyMavenResolver.java create mode 100644 maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolver.java create mode 100644 maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolverFactory.java create mode 100644 maven/resolver-paxurl/src/main/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory diff --git a/assemblies/features/framework/pom.xml b/assemblies/features/framework/pom.xml index efc75bc7065..be7b04f2f17 100644 --- a/assemblies/features/framework/pom.xml +++ b/assemblies/features/framework/pom.xml @@ -186,6 +186,11 @@ org.apache.karaf.features org.apache.karaf.features.core + + org.apache.karaf.maven + org.apache.karaf.maven.resolver.paxurl + ${project.version} + diff --git a/assemblies/features/framework/src/main/feature/feature.xml b/assemblies/features/framework/src/main/feature/feature.xml index 00bfdaaa487..443707f903b 100644 --- a/assemblies/features/framework/src/main/feature/feature.xml +++ b/assemblies/features/framework/src/main/feature/feature.xml @@ -44,6 +44,7 @@ mvn:org.apache.felix/org.apache.felix.configurator/${felix.configurator.version} mvn:org.apache.karaf.config/org.apache.karaf.config.core/${project.version} + mvn:org.apache.karaf.maven/org.apache.karaf.maven.resolver.paxurl/${project.version} mvn:org.apache.karaf.features/org.apache.karaf.features.core/${project.version} mvn:jakarta.xml.bind/jakarta.xml.bind-api/${spec.jaxb-api.version} mvn:org.glassfish.jaxb/jaxb-runtime/${spec.jaxb.version} @@ -76,6 +77,7 @@ mvn:org.apache.felix/org.apache.felix.fileinstall/${felix.fileinstall.version} + mvn:org.apache.karaf.maven/org.apache.karaf.maven.resolver.paxurl/${project.version} mvn:org.apache.karaf.features/org.apache.karaf.features.core/${project.version} mvn:jakarta.xml.bind/jakarta.xml.bind-api/${spec.jaxb-api.version} mvn:org.glassfish.jaxb/jaxb-runtime/${spec.jaxb.version} diff --git a/features/command/src/main/java/org/apache/karaf/features/command/FeatureExport.java b/features/command/src/main/java/org/apache/karaf/features/command/FeatureExport.java index efc4b0db907..7ccbddc7b8e 100644 --- a/features/command/src/main/java/org/apache/karaf/features/command/FeatureExport.java +++ b/features/command/src/main/java/org/apache/karaf/features/command/FeatureExport.java @@ -33,7 +33,7 @@ import org.apache.karaf.shell.api.action.lifecycle.Reference; import org.apache.karaf.shell.api.action.lifecycle.Service; import org.apache.karaf.shell.support.completers.FileCompleter; -import org.ops4j.pax.url.mvn.MavenResolver; +import org.apache.karaf.features.spi.MavenResolver; /** * Simple {@link FeaturesCommandSupport} implementation that allows a user in diff --git a/features/core/pom.xml b/features/core/pom.xml index 23a089cbade..61f4c36442d 100644 --- a/features/core/pom.xml +++ b/features/core/pom.xml @@ -129,24 +129,6 @@ provided - - org.ops4j.pax.url - pax-url-aether - - - - org.ops4j.base - ops4j-base-util-property - - - org.ops4j.base - ops4j-base-util-collections - - - org.ops4j.pax.swissbox - pax-swissbox-property - - jakarta.activation jakarta.activation-api @@ -158,12 +140,6 @@ test - - org.ops4j.pax.tinybundles - tinybundles - test - - org.hamcrest hamcrest @@ -214,6 +190,7 @@ org.apache.karaf.features, + org.apache.karaf.features.spi, org.apache.karaf.features.management, org.apache.karaf.features.management.codec;-noimport:=true, org.osgi.service.repository, @@ -248,9 +225,6 @@ org.apache.karaf.util.xml, org.eclipse.equinox.internal.region.*;-split-package:=merge-first, org.apache.felix.resolver.*, - org.ops4j.pax.swissbox.*, - org.ops4j.util.*, - org.ops4j.lang.*, com.fasterxml.jackson* diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/download/DownloadManagers.java b/features/core/src/main/java/org/apache/karaf/features/internal/download/DownloadManagers.java index 59d37ead3b7..f7d10eecd27 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/download/DownloadManagers.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/download/DownloadManagers.java @@ -19,7 +19,7 @@ import java.util.concurrent.ScheduledExecutorService; import org.apache.karaf.features.internal.download.impl.MavenDownloadManager; -import org.ops4j.pax.url.mvn.MavenResolver; +import org.apache.karaf.features.spi.MavenResolver; public final class DownloadManagers { diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadManager.java b/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadManager.java index ede3337e7de..1f73bf2ac48 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadManager.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadManager.java @@ -29,7 +29,7 @@ import org.apache.karaf.features.internal.download.Downloader; import org.apache.karaf.features.internal.download.StreamProvider; import org.apache.karaf.features.internal.util.MultiException; -import org.ops4j.pax.url.mvn.MavenResolver; +import org.apache.karaf.features.spi.MavenResolver; public class MavenDownloadManager implements DownloadManager { diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadTask.java b/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadTask.java index 41a1e2b04e3..76d8b481808 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadTask.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/download/impl/MavenDownloadTask.java @@ -22,7 +22,7 @@ import java.util.Objects; import org.apache.karaf.util.maven.Parser; -import org.ops4j.pax.url.mvn.MavenResolver; +import org.apache.karaf.features.spi.MavenResolver; public class MavenDownloadTask extends AbstractRetryableDownloadTask { diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java index 6640b6d699f..9214aec524c 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java @@ -57,6 +57,7 @@ import org.apache.karaf.features.internal.service.StateStorage; import org.apache.karaf.features.internal.util.SystemExitManager; import org.apache.karaf.util.ThreadUtils; +import org.apache.karaf.features.spi.MavenResolverFactory; import org.apache.karaf.util.tracker.BaseActivator; import org.apache.karaf.util.tracker.annotation.ProvideService; import org.apache.karaf.util.tracker.annotation.RequireService; @@ -84,6 +85,7 @@ @Services( requires = { @RequireService(ConfigurationAdmin.class), + @RequireService(MavenResolverFactory.class), @RequireService(value = URLStreamHandlerService.class, filter = "(url.handler.protocol=mvn)") }, provides = { @@ -200,6 +202,7 @@ protected void doStart() throws Exception { configurationAdmin, installSupport, cfg); + simpleFeaturesService.setMavenResolverFactory(getTrackedService(MavenResolverFactory.class)); try { EventAdminListener eventAdminListener = new EventAdminListener(bundleContext); simpleFeaturesService.registerListener(eventAdminListener); @@ -216,6 +219,7 @@ protected void doStart() throws Exception { installSupport, globalRepository, cfg); + featuresService.setMavenResolverFactory(getTrackedService(MavenResolverFactory.class)); try { EventAdminListener eventAdminListener = new EventAdminListener(bundleContext); featuresService.registerListener(eventAdminListener); diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java index e89bb349d62..78398b94eb7 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessingSerializer.java @@ -46,9 +46,8 @@ import org.apache.karaf.features.internal.model.processing.FeaturesProcessing; import org.apache.karaf.features.internal.model.processing.ObjectFactory; import org.apache.karaf.util.xml.IndentingXMLEventWriter; -import org.ops4j.pax.swissbox.property.BundleContextPropertyResolver; -import org.ops4j.util.property.DictionaryPropertyResolver; -import org.ops4j.util.property.PropertyResolver; +import org.apache.karaf.features.internal.util.PropertyResolver; +import org.apache.karaf.features.internal.util.PropertySubstitutor; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; @@ -106,9 +105,9 @@ public FeaturesProcessing read(InputStream stream, Properties versions) throws E Unmarshaller unmarshaller = FEATURES_PROCESSING_CONTEXT.createUnmarshaller(); UnmarshallerHandler handler = unmarshaller.getUnmarshallerHandler(); - // BundleContextPropertyResolver gives access to e.g., ${karaf.base} - final PropertyResolver resolver = bundleContext == null ? new DictionaryPropertyResolver(versions) - : new DictionaryPropertyResolver(versions, new BundleContextPropertyResolver(bundleContext)); + // the bundle context resolver gives access to e.g., ${karaf.base} + final PropertyResolver resolver = bundleContext == null ? PropertyResolver.forDictionary(versions) + : PropertyResolver.forDictionary(versions, PropertyResolver.forBundleContext(bundleContext)); // indirect unmarshaling with property resolution inside XML attribute values and CDATA SAXParserFactory spf = SAXParserFactory.newInstance(); @@ -314,7 +313,7 @@ private void flushBuffer(boolean resolve) throws SAXException { } private String resolve(String value) { - String resolved = org.ops4j.util.collections.PropertyResolver.resolve(properties, value); + String resolved = PropertySubstitutor.substitute(properties, value); if (resolved.contains("${")) { // there are still unresolved properties - just log warning LOG.warn("Value {} has unresolved properties, please check configuration.", value); diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java index 80c08631ab8..8eebd73146a 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java @@ -75,8 +75,9 @@ import org.apache.karaf.util.json.JsonWriter; import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList; import org.eclipse.equinox.region.RegionDigraph; -import org.ops4j.pax.url.mvn.MavenResolver; -import org.ops4j.pax.url.mvn.MavenResolvers; +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; +import org.apache.karaf.features.spi.MavenResolvers; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.InvalidSyntaxException; @@ -112,6 +113,8 @@ public class FeaturesServiceImpl implements FeaturesService, BootManaged, Deploy private final FeatureRepoFinder featureFinder; private final ConfigurationAdmin configurationAdmin; private final Resolver resolver; + + private volatile MavenResolverFactory mavenResolverFactory; private final BundleInstallSupport installSupport; private final FeaturesServiceConfig cfg; private RepositoryCache repositories; @@ -1086,18 +1089,29 @@ private void doProvision(Map> requirements, / } } + /** + * Set the {@link MavenResolverFactory} to use. When left unset, the factory is looked up with + * {@link MavenResolvers#factory()}, which is how non-OSGi embedders get one. + * + * @param mavenResolverFactory the factory to use. + */ + public void setMavenResolverFactory(MavenResolverFactory mavenResolverFactory) { + this.mavenResolverFactory = mavenResolverFactory; + } + protected DownloadManager createDownloadManager() throws IOException { - Dictionary props = getMavenConfig(); - MavenResolver resolver = MavenResolvers.createMavenResolver(props, "org.ops4j.pax.url.mvn"); + MavenResolverFactory factory = mavenResolverFactory != null ? mavenResolverFactory : MavenResolvers.factory(); + Dictionary props = getMavenConfig(factory.getConfigurationPid()); + MavenResolver resolver = factory.create(props); ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(cfg.downloadThreads, ThreadUtils.namedThreadFactory("downloader")); executor.setMaximumPoolSize(cfg.downloadThreads); return DownloadManagers.createDownloadManager(resolver, executor, cfg.scheduleDelay, cfg.scheduleMaxRun); } - private Dictionary getMavenConfig() throws IOException { + private Dictionary getMavenConfig(String pid) throws IOException { Hashtable props = new Hashtable<>(); if (configurationAdmin != null) { - Configuration config = configurationAdmin.getConfiguration("org.ops4j.pax.url.mvn", null); + Configuration config = configurationAdmin.getConfiguration(pid, null); if (config != null) { Dictionary cfg = config.getProcessedProperties(null); if (cfg != null) { diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java index 1e33492b477..6f2e4aefc71 100644 --- a/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java +++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java @@ -65,8 +65,9 @@ import org.apache.karaf.features.internal.model.JaxbUtil; import org.apache.karaf.util.ThreadUtils; import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList; -import org.ops4j.pax.url.mvn.MavenResolver; -import org.ops4j.pax.url.mvn.MavenResolvers; +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; +import org.apache.karaf.features.spi.MavenResolvers; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.InvalidSyntaxException; @@ -108,6 +109,8 @@ public class SimpleFeaturesServiceImpl implements FeaturesService, BootManaged, private final ConfigurationAdmin configurationAdmin; private final BundleInstallSupport installSupport; private final FeaturesServiceConfig cfg; + + private volatile MavenResolverFactory mavenResolverFactory; private RepositoryCache repositories; private FeaturesProcessor featuresProcessor; @@ -842,18 +845,29 @@ private void doProvision(Map> requirements, } } + /** + * Set the {@link MavenResolverFactory} to use. When left unset, the factory is looked up with + * {@link MavenResolvers#factory()}, which is how non-OSGi embedders get one. + * + * @param mavenResolverFactory the factory to use. + */ + public void setMavenResolverFactory(MavenResolverFactory mavenResolverFactory) { + this.mavenResolverFactory = mavenResolverFactory; + } + protected DownloadManager createDownloadManager() throws IOException { - Dictionary props = getMavenConfig(); - MavenResolver resolver = MavenResolvers.createMavenResolver(props, "org.ops4j.pax.url.mvn"); + MavenResolverFactory factory = mavenResolverFactory != null ? mavenResolverFactory : MavenResolvers.factory(); + Dictionary props = getMavenConfig(factory.getConfigurationPid()); + MavenResolver resolver = factory.create(props); ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(cfg.downloadThreads, ThreadUtils.namedThreadFactory("downloader")); executor.setMaximumPoolSize(cfg.downloadThreads); return DownloadManagers.createDownloadManager(resolver, executor, cfg.scheduleDelay, cfg.scheduleMaxRun); } - private Dictionary getMavenConfig() throws IOException { + private Dictionary getMavenConfig(String pid) throws IOException { Hashtable props = new Hashtable<>(); if (configurationAdmin != null) { - Configuration config = configurationAdmin.getConfiguration("org.ops4j.pax.url.mvn", null); + Configuration config = configurationAdmin.getConfiguration(pid, null); if (config != null) { Dictionary cfg = config.getProcessedProperties(null); if (cfg != null) { diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/util/PropertyResolver.java b/features/core/src/main/java/org/apache/karaf/features/internal/util/PropertyResolver.java new file mode 100644 index 00000000000..77f90934293 --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/util/PropertyResolver.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.features.internal.util; + +import java.util.Dictionary; + +import org.osgi.framework.BundleContext; + +/** + * Resolves a property by name, optionally delegating to a fallback resolver. + * + *

A blank value is treated as no value, so resolution falls through to the fallback.

+ */ +@FunctionalInterface +public interface PropertyResolver { + + /** + * Resolve a property. + * + * @param key the property name. + * @return the property value, or null if unknown. + */ + String get(String key); + + /** + * A resolver backed by a dictionary. + * + * @param properties the properties to read, may be null. + * @return the resolver. + */ + static PropertyResolver forDictionary(Dictionary properties) { + return forDictionary(properties, null); + } + + /** + * A resolver backed by a dictionary, falling back to another resolver. + * + * @param properties the properties to read, may be null. + * @param fallback the resolver to consult when the dictionary has no value, may be null. + * @return the resolver. + */ + static PropertyResolver forDictionary(Dictionary properties, PropertyResolver fallback) { + return key -> { + String value = null; + if (properties != null) { + Object raw = properties.get(key); + if (raw instanceof String) { + value = (String) raw; + } + } + if (value != null && value.trim().isEmpty()) { + value = null; + } + if (value == null && fallback != null) { + value = fallback.get(key); + } + return value; + }; + } + + /** + * A resolver backed by the framework properties, which themselves fall back to system properties. + * It gives access to e.g. ${karaf.base}. + * + * @param bundleContext the bundle context to read, may be null. + * @return the resolver. + */ + static PropertyResolver forBundleContext(BundleContext bundleContext) { + return key -> { + String value = bundleContext == null ? null : bundleContext.getProperty(key); + return value != null && value.trim().isEmpty() ? null : value; + }; + } + +} diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/util/PropertySubstitutor.java b/features/core/src/main/java/org/apache/karaf/features/internal/util/PropertySubstitutor.java new file mode 100644 index 00000000000..6e0a76354a8 --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/util/PropertySubstitutor.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.features.internal.util; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Properties; +import java.util.StringTokenizer; + +/** + * Substitutes ${...} placeholders in a string. + * + *

System properties take precedence over the supplied properties. Placeholders that can't be + * resolved are left untouched, so callers can detect and report them. Nesting + * (${a${b}}) is supported.

+ */ +public final class PropertySubstitutor { + + private static final String MARKER = "${"; + + private PropertySubstitutor() { } + + /** + * Substitute the placeholders found in value. + * + * @param properties the properties to resolve placeholders against. + * @param value the value to substitute, may be null. + * @return the substituted value. + */ + public static String substitute(Properties properties, String value) { + if (value == null || value.indexOf('$') < 0 || !value.contains(MARKER)) { + return value; + } + Deque stack = new ArrayDeque<>(); + StringTokenizer tokenizer = new StringTokenizer(value, "${}", true); + while (tokenizer.hasMoreTokens()) { + processToken(tokenizer.nextToken(), stack, properties); + } + StringBuilder result = new StringBuilder(); + while (!stack.isEmpty()) { + result.insert(0, stack.pop()); + } + return result.toString(); + } + + private static void processToken(String token, Deque stack, Properties properties) { + if ("}".equals(token)) { + if (stack.size() < 2) { + // unbalanced closing brace - keep it verbatim + push(stack, token); + return; + } + String name = stack.pop(); + String marker = stack.pop(); + if (MARKER.equals(marker)) { + startProperty(name, properties, stack); + } else { + push(stack, marker); + push(stack, MARKER + name + "}"); + } + } else if ("$".equals(token)) { + stack.push(token); + } else { + push(stack, token); + } + } + + private static void startProperty(String name, Properties properties, Deque stack) { + String value = System.getProperty(name); + if (value == null && properties != null) { + value = properties.getProperty(name); + } + push(stack, value == null ? MARKER + name + "}" : value); + } + + private static void push(Deque stack, String value) { + if (stack.isEmpty()) { + stack.push(value); + return; + } + String top = stack.pop(); + if (MARKER.equals(top)) { + stack.push(top); + stack.push(value); + } else { + stack.push(top + value); + } + } + +} diff --git a/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolver.java b/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolver.java new file mode 100644 index 00000000000..eadb8972c25 --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolver.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.features.spi; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; + +/** + * Resolves mvn: URIs to local files. + * + *

This is the only contract the features service requires from a Maven artifact + * provider. Implementations are looked up through {@link MavenResolvers}.

+ */ +public interface MavenResolver extends Closeable { + + /** + * How likely a failed resolution is to succeed when retried. + */ + enum RetryChance { + /** Retrying will never help. */ + NEVER, + /** Retrying may help, but is unlikely to. */ + LOW, + /** Retrying is likely to help. */ + HIGH, + /** Not enough information to tell. */ + UNKNOWN + } + + /** + * Resolve a mvn: URI to a local file. + * + * @param url the URI to resolve. + * @return the resolved file. + * @throws IOException if the artifact can't be resolved. + */ + File resolve(String url) throws IOException; + + /** + * Resolve a mvn: URI to a local file, carrying over the failure of a + * previous attempt so the implementation can adjust its behaviour (repository + * ordering, update policies, ...). + * + * @param url the URI to resolve. + * @param previousException the exception thrown by the previous attempt, or null. + * @return the resolved file. + * @throws IOException if the artifact can't be resolved. + */ + File resolve(String url, Exception previousException) throws IOException; + + /** + * Resolve a Maven artifact from its coordinates. + * + *

The default implementation builds the equivalent mvn: URI and delegates to + * {@link #resolve(String)}.

+ * + * @param groupId the group id. + * @param artifactId the artifact id. + * @param classifier the classifier, or null/empty for none. + * @param extension the extension (packaging/type), or null/empty for the default. + * @param version the version. + * @return the resolved file. + * @throws IOException if the artifact can't be resolved. + */ + default File resolve(String groupId, String artifactId, String classifier, String extension, String version) + throws IOException { + StringBuilder uri = new StringBuilder("mvn:") + .append(groupId).append('/').append(artifactId).append('/').append(version); + boolean hasClassifier = classifier != null && !classifier.isEmpty(); + if (hasClassifier || (extension != null && !extension.isEmpty())) { + uri.append('/').append(extension == null ? "" : extension); + } + if (hasClassifier) { + uri.append('/').append(classifier); + } + return resolve(uri.toString()); + } + + /** + * Tell whether a failed resolution is worth retrying. + * + * @param exception the exception thrown by {@link #resolve(String, Exception)}. + * @return the chance that a retry succeeds. + */ + default RetryChance isRetryableException(Exception exception) { + return RetryChance.UNKNOWN; + } + + @Override + default void close() throws IOException { + } + +} diff --git a/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolverFactory.java b/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolverFactory.java new file mode 100644 index 00000000000..a6b7ec69c4d --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolverFactory.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.features.spi; + +import java.util.Dictionary; + +/** + * Creates {@link MavenResolver} instances. + * + *

Providers are discovered with {@link java.util.ServiceLoader} (see + * {@link MavenResolvers}), so a provider must declare itself in + * META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory.

+ */ +public interface MavenResolverFactory { + + /** + * The ConfigAdmin PID holding this provider's configuration. It is also the default + * prefix of the configuration properties. + * + * @return the configuration PID. + */ + String getConfigurationPid(); + + /** + * Create a resolver. + * + * @param configuration the resolver configuration. + * @param propertyPrefix the prefix of the keys to read from configuration. + * @return a new resolver. + */ + MavenResolver create(Dictionary configuration, String propertyPrefix); + + /** + * Create a resolver, reading the configuration under {@link #getConfigurationPid()}. + * + * @param configuration the resolver configuration. + * @return a new resolver. + */ + default MavenResolver create(Dictionary configuration) { + return create(configuration, getConfigurationPid()); + } + +} diff --git a/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolvers.java b/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolvers.java new file mode 100644 index 00000000000..72eca212adc --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/spi/MavenResolvers.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.features.spi; + +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +/** + * Looks up the {@link MavenResolverFactory} provider to use. + */ +public final class MavenResolvers { + + private MavenResolvers() { } + + /** + * Look up the factory provided by the class loader of this class. + * + * @return the factory. + * @throws IllegalStateException if no provider is available. + */ + public static MavenResolverFactory factory() { + return factory(MavenResolvers.class.getClassLoader()); + } + + /** + * Look up the factory provided by the given class loader. + * + * @param classLoader the class loader to search for providers. + * @return the factory. + * @throws IllegalStateException if no provider is available. + */ + public static MavenResolverFactory factory(ClassLoader classLoader) { + try { + Iterator factories = + ServiceLoader.load(MavenResolverFactory.class, classLoader).iterator(); + if (factories.hasNext()) { + return factories.next(); + } + } catch (ServiceConfigurationError e) { + throw new IllegalStateException("Unable to load a " + MavenResolverFactory.class.getName() + + " provider. Make sure a Maven resolver provider is available.", e); + } + throw new IllegalStateException("No " + MavenResolverFactory.class.getName() + + " provider found. Make sure a Maven resolver provider is available."); + } + +} diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/maven/TestMavenResolverFactory.java b/features/core/src/test/java/org/apache/karaf/features/internal/maven/TestMavenResolverFactory.java new file mode 100644 index 00000000000..25cc057b900 --- /dev/null +++ b/features/core/src/test/java/org/apache/karaf/features/internal/maven/TestMavenResolverFactory.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.features.internal.maven; + +import java.io.File; +import java.io.IOException; +import java.util.Dictionary; + +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; + +/** + * {@link MavenResolverFactory} for the tests of this module, which never resolve mvn: + * URIs - they use their own URL handlers. Any actual resolution is a test bug and fails loudly. + */ +public class TestMavenResolverFactory implements MavenResolverFactory { + + public static final String PID = "org.apache.karaf.features.test.mvn"; + + @Override + public String getConfigurationPid() { + return PID; + } + + @Override + public MavenResolver create(Dictionary configuration, String propertyPrefix) { + return new MavenResolver() { + @Override + public File resolve(String url) throws IOException { + throw new IOException("No Maven resolver is configured for the features core tests: " + url); + } + + @Override + public File resolve(String url, Exception previousException) throws IOException { + return resolve(url); + } + }; + } + +} diff --git a/features/core/src/test/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory b/features/core/src/test/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory new file mode 100644 index 00000000000..5c8e0b923b7 --- /dev/null +++ b/features/core/src/test/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory @@ -0,0 +1,17 @@ +##--------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +##--------------------------------------------------------------------------- +org.apache.karaf.features.internal.maven.TestMavenResolverFactory diff --git a/itests/test/pom.xml b/itests/test/pom.xml index ae41411c472..7c47724e476 100644 --- a/itests/test/pom.xml +++ b/itests/test/pom.xml @@ -55,6 +55,12 @@ common ${project.version}
+ + + org.ops4j.pax.url + pax-url-aether + test + jakarta.annotation diff --git a/maven/pom.xml b/maven/pom.xml index b0caee874c9..0875614361d 100644 --- a/maven/pom.xml +++ b/maven/pom.xml @@ -35,6 +35,7 @@ core + resolver-paxurl diff --git a/maven/resolver-paxurl/pom.xml b/maven/resolver-paxurl/pom.xml new file mode 100644 index 00000000000..0bf6bae2593 --- /dev/null +++ b/maven/resolver-paxurl/pom.xml @@ -0,0 +1,103 @@ + + + + + + 4.0.0 + + + org.apache.karaf + karaf + 4.5.0-SNAPSHOT + ../../pom.xml + + + org.apache.karaf.maven + org.apache.karaf.maven.resolver.paxurl + bundle + Apache Karaf :: Maven :: Resolver :: Pax URL + Provides the Karaf MavenResolver SPI on top of pax-url-aether. + + + + + org.apache.karaf + karaf-bom + ${project.version} + pom + import + + + + + + + org.osgi + org.osgi.framework + + + org.osgi + org.osgi.service.cm + + + org.apache.karaf.features + org.apache.karaf.features.core + provided + + + + org.ops4j.pax.url + pax-url-aether + + + + + + + org.apache.felix + maven-bundle-plugin + + + ${project.artifactId} + + org.apache.karaf.maven.resolver.paxurl.Activator + + The Apache Software Foundation + + org.osgi.framework;version="[1,3)", + org.apache.karaf.features.spi, + org.ops4j.pax.url.mvn, + org.osgi.service.cm;resolution:=optional, + * + + + org.apache.karaf.maven.resolver.paxurl + + + org.apache.karaf.features.spi.MavenResolverFactory, + org.apache.karaf.features.spi.MavenResolver + + + + + + + + diff --git a/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/Activator.java b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/Activator.java new file mode 100644 index 00000000000..12e5f001bea --- /dev/null +++ b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/Activator.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.maven.resolver.paxurl; + +import java.io.IOException; +import java.util.Hashtable; + +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * Registers the pax-url backed {@link MavenResolverFactory}, so that the features service can + * resolve mvn: URIs, along with a ready to use {@link MavenResolver} for the + * components that just want to resolve an artifact. + */ +public class Activator implements BundleActivator { + + private ServiceRegistration factoryRegistration; + private ServiceRegistration resolverRegistration; + private LazyMavenResolver resolver; + + @Override + public void start(BundleContext context) { + Hashtable properties = new Hashtable<>(); + properties.put("provider", "pax-url-aether"); + + MavenResolverFactory factory = new PaxUrlMavenResolverFactory(); + factoryRegistration = context.registerService(MavenResolverFactory.class, factory, properties); + + resolver = new LazyMavenResolver(context, factory); + resolverRegistration = context.registerService(MavenResolver.class, resolver, properties); + } + + @Override + public void stop(BundleContext context) { + if (resolverRegistration != null) { + resolverRegistration.unregister(); + resolverRegistration = null; + } + if (factoryRegistration != null) { + factoryRegistration.unregister(); + factoryRegistration = null; + } + if (resolver != null) { + try { + resolver.close(); + } catch (IOException e) { + // the resolver is going away anyway + } + resolver = null; + } + } + +} diff --git a/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/LazyMavenResolver.java b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/LazyMavenResolver.java new file mode 100644 index 00000000000..6199444e3c3 --- /dev/null +++ b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/LazyMavenResolver.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.maven.resolver.paxurl; + +import java.io.File; +import java.io.IOException; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; + +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * A {@link MavenResolver} that builds its delegate on first use, from the ConfigAdmin + * configuration of the factory's PID. + * + *

Creating it lazily means this bundle does not have to wait for ConfigAdmin to start, and + * configuration changes made before the first resolution are picked up.

+ */ +public class LazyMavenResolver implements MavenResolver { + + private final BundleContext bundleContext; + private final MavenResolverFactory factory; + + private volatile MavenResolver delegate; + + public LazyMavenResolver(BundleContext bundleContext, MavenResolverFactory factory) { + this.bundleContext = bundleContext; + this.factory = factory; + } + + private MavenResolver delegate() throws IOException { + MavenResolver resolver = delegate; + if (resolver == null) { + synchronized (this) { + resolver = delegate; + if (resolver == null) { + resolver = factory.create(readConfiguration()); + delegate = resolver; + } + } + } + return resolver; + } + + private Dictionary readConfiguration() throws IOException { + Hashtable properties = new Hashtable<>(); + ServiceReference reference = + bundleContext.getServiceReference(ConfigurationAdmin.class); + if (reference == null) { + return properties; + } + try { + ConfigurationAdmin configurationAdmin = bundleContext.getService(reference); + if (configurationAdmin == null) { + return properties; + } + Configuration configuration = configurationAdmin.getConfiguration(factory.getConfigurationPid(), null); + if (configuration != null) { + Dictionary cfg = configuration.getProcessedProperties(null); + if (cfg != null) { + for (Enumeration keys = cfg.keys(); keys.hasMoreElements(); ) { + String key = keys.nextElement(); + Object value = cfg.get(key); + if (key != null && value != null) { + properties.put(key, value.toString()); + } + } + } + } + return properties; + } finally { + bundleContext.ungetService(reference); + } + } + + @Override + public File resolve(String url) throws IOException { + return delegate().resolve(url); + } + + @Override + public File resolve(String url, Exception previousException) throws IOException { + return delegate().resolve(url, previousException); + } + + @Override + public File resolve(String groupId, String artifactId, String classifier, String extension, String version) + throws IOException { + return delegate().resolve(groupId, artifactId, classifier, extension, version); + } + + @Override + public RetryChance isRetryableException(Exception exception) { + MavenResolver resolver = delegate; + return resolver == null ? RetryChance.UNKNOWN : resolver.isRetryableException(exception); + } + + @Override + public void close() throws IOException { + MavenResolver resolver = delegate; + delegate = null; + if (resolver != null) { + resolver.close(); + } + } + +} diff --git a/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolver.java b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolver.java new file mode 100644 index 00000000000..c178d422dfc --- /dev/null +++ b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolver.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.maven.resolver.paxurl; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import org.apache.karaf.features.spi.MavenResolver; + +/** + * Adapts a pax-url-aether resolver to the Karaf {@link MavenResolver} SPI. + * + *

This bundle is the only place in Karaf that links against org.ops4j.pax.url.mvn.

+ */ +public class PaxUrlMavenResolver implements MavenResolver { + + private final org.ops4j.pax.url.mvn.MavenResolver delegate; + + public PaxUrlMavenResolver(org.ops4j.pax.url.mvn.MavenResolver delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public File resolve(String url) throws IOException { + return delegate.resolve(url); + } + + @Override + public File resolve(String url, Exception previousException) throws IOException { + return delegate.resolve(url, previousException); + } + + @Override + public File resolve(String groupId, String artifactId, String classifier, String extension, String version) + throws IOException { + return delegate.resolve(groupId, artifactId, classifier, extension, version); + } + + @Override + public RetryChance isRetryableException(Exception exception) { + switch (delegate.isRetryableException(exception)) { + case NEVER: + return RetryChance.NEVER; + case LOW: + return RetryChance.LOW; + case HIGH: + return RetryChance.HIGH; + case UNKNOWN: + default: + return RetryChance.UNKNOWN; + } + } + + @Override + public void close() throws IOException { + delegate.close(); + } + +} diff --git a/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolverFactory.java b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolverFactory.java new file mode 100644 index 00000000000..3b754ccbf39 --- /dev/null +++ b/maven/resolver-paxurl/src/main/java/org/apache/karaf/maven/resolver/paxurl/PaxUrlMavenResolverFactory.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.maven.resolver.paxurl; + +import java.util.Dictionary; + +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; +import org.ops4j.pax.url.mvn.MavenResolvers; + +/** + * {@link MavenResolverFactory} backed by pax-url-aether. + */ +public class PaxUrlMavenResolverFactory implements MavenResolverFactory { + + public static final String PID = "org.ops4j.pax.url.mvn"; + + @Override + public String getConfigurationPid() { + return PID; + } + + @Override + public MavenResolver create(Dictionary configuration, String propertyPrefix) { + return new PaxUrlMavenResolver(MavenResolvers.createMavenResolver(configuration, propertyPrefix)); + } + +} diff --git a/maven/resolver-paxurl/src/main/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory b/maven/resolver-paxurl/src/main/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory new file mode 100644 index 00000000000..9a4f90e3bdc --- /dev/null +++ b/maven/resolver-paxurl/src/main/resources/META-INF/services/org.apache.karaf.features.spi.MavenResolverFactory @@ -0,0 +1,17 @@ +##--------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +##--------------------------------------------------------------------------- +org.apache.karaf.maven.resolver.paxurl.PaxUrlMavenResolverFactory diff --git a/profile/pom.xml b/profile/pom.xml index c038a0c0966..c1db488ec42 100644 --- a/profile/pom.xml +++ b/profile/pom.xml @@ -100,6 +100,13 @@ org.apache.karaf.features.core true
+ + + org.apache.karaf.maven + org.apache.karaf.maven.resolver.paxurl + ${project.version} + true + org.eclipse.platform diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java b/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java index 76bc5d403cf..df4820d12d4 100644 --- a/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java +++ b/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java @@ -93,8 +93,9 @@ import org.apache.karaf.util.Version; import org.apache.karaf.util.config.PropertiesLoader; import org.apache.karaf.util.maven.Parser; -import org.ops4j.pax.url.mvn.MavenResolver; -import org.ops4j.pax.url.mvn.MavenResolvers; +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolverFactory; +import org.apache.karaf.features.spi.MavenResolvers; import org.osgi.framework.Constants; import org.osgi.framework.wiring.BundleRevision; import org.osgi.resource.Resource; @@ -126,6 +127,11 @@ public class Builder { private static final String LIBRARY_CLAUSE_DELEGATE = "delegate"; private static final String START_LEVEL = "start-level"; + /** + * @deprecated the configuration PID is now provided by the + * {@link MavenResolverFactory} in use, see {@link MavenResolverFactory#getConfigurationPid()}. + */ + @Deprecated public static final String ORG_OPS4J_PAX_URL_MVN_PID = "org.ops4j.pax.url.mvn"; /** @@ -678,7 +684,7 @@ public Builder ignoreDependencyFlag(boolean ignoreDependencyFlag) { } /** - * Configures builder to use offline pax-url-aether resolver + * Configures builder to use an offline Maven resolver * @return */ public Builder offline() { @@ -686,7 +692,7 @@ public Builder offline() { } /** - * Configures whether pax-url-aether resolver should work in offline mode + * Configures whether the Maven resolver should work in offline mode * @param offline * @return */ @@ -913,8 +919,8 @@ private void doGenerateAssembly() throws Exception { LOGGER.info("Generating Karaf assembly: " + homeDirectory); // - // Create download manager - combination of pax-url-aether and a resolver wrapper that may - // alter the way pax-url-aether resolver works + // Create download manager - combination of the configured MavenResolver provider and a + // resolver wrapper that may alter the way that resolver works // MavenResolver resolver = createMavenResolver(); manager = new CustomDownloadManager(resolver, executor, null, translatedUrls); @@ -1503,18 +1509,19 @@ private Blacklist processBlacklist(Profile initialProfile) throws IOException { } private MavenResolver createMavenResolver() { + MavenResolverFactory factory = MavenResolvers.factory(); + String pid = factory.getConfigurationPid(); Dictionary props = new Hashtable<>(); if (offline) { - props.put(ORG_OPS4J_PAX_URL_MVN_PID + "offline", "true"); + props.put(pid + "offline", "true"); } if (localRepository != null) { - props.put(ORG_OPS4J_PAX_URL_MVN_PID + ".localRepository", localRepository); + props.put(pid + ".localRepository", localRepository); } if (mavenRepositories != null) { - props.put(ORG_OPS4J_PAX_URL_MVN_PID + ".repositories", mavenRepositories); + props.put(pid + ".repositories", mavenRepositories); } - MavenResolver resolver = MavenResolvers.createMavenResolver(props, ORG_OPS4J_PAX_URL_MVN_PID); - return resolverWrapper.apply(resolver); + return resolverWrapper.apply(factory.create(props, pid)); } /** diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/CustomDownloadManager.java b/profile/src/main/java/org/apache/karaf/profile/assembly/CustomDownloadManager.java index ff49f1159cf..60c770ba864 100644 --- a/profile/src/main/java/org/apache/karaf/profile/assembly/CustomDownloadManager.java +++ b/profile/src/main/java/org/apache/karaf/profile/assembly/CustomDownloadManager.java @@ -23,7 +23,7 @@ import org.apache.karaf.features.internal.download.impl.AbstractDownloadTask; import org.apache.karaf.features.internal.download.impl.MavenDownloadManager; import org.apache.karaf.profile.Profile; -import org.ops4j.pax.url.mvn.MavenResolver; +import org.apache.karaf.features.spi.MavenResolver; public class CustomDownloadManager extends MavenDownloadManager { diff --git a/tooling/karaf-maven-plugin/pom.xml b/tooling/karaf-maven-plugin/pom.xml index 2981d1a788b..3357b636ac1 100644 --- a/tooling/karaf-maven-plugin/pom.xml +++ b/tooling/karaf-maven-plugin/pom.xml @@ -208,6 +208,12 @@ org.ops4j.pax.url pax-url-aether + + + org.apache.karaf.maven + org.apache.karaf.maven.resolver.paxurl + ${project.version} + org.ops4j.pax.url pax-url-war diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java index 88da22a5b66..a13bc88b45e 100644 --- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java +++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/VerifyMojo.java @@ -93,8 +93,8 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.eclipse.aether.repository.WorkspaceReader; -import org.ops4j.pax.url.mvn.MavenResolver; -import org.ops4j.pax.url.mvn.MavenResolvers; +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.features.spi.MavenResolvers; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; @@ -200,7 +200,7 @@ public void execute() throws MojoExecutionException { } // TODO: add more configuration bits ? - resolver = new ReactorMavenResolver(reactor, MavenResolvers.createMavenResolver(config, "maven")); + resolver = new ReactorMavenResolver(reactor, MavenResolvers.factory().create(config, "maven")); doExecute(); } diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/ReactorMavenResolver.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/ReactorMavenResolver.java index c704da1bb98..779017adcc4 100644 --- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/ReactorMavenResolver.java +++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/ReactorMavenResolver.java @@ -20,12 +20,11 @@ import java.io.IOException; import java.net.MalformedURLException; +import org.apache.karaf.features.spi.MavenResolver; +import org.apache.karaf.util.maven.Parser; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.repository.WorkspaceReader; -import org.ops4j.pax.url.mvn.MavenResolver; -import org.ops4j.pax.url.mvn.ServiceConstants; -import org.ops4j.pax.url.mvn.internal.Parser; /** * {@link MavenResolver} that may look up artifacts inside Maven reactor @@ -41,7 +40,7 @@ public ReactorMavenResolver(WorkspaceReader reactor, MavenResolver fallback) { } private Artifact toArtifact(String url) throws MalformedURLException { - if (url.startsWith(ServiceConstants.PROTOCOL + ":")) { + if (url.startsWith("mvn:")) { url = url.substring(4); } Parser parser = new Parser(url); @@ -64,35 +63,10 @@ public File resolve(String url, Exception previousException) throws IOException } @Override - public File resolve(String groupId, String artifactId, String classifier, String extension, String version) throws IOException { + public File resolve(String groupId, String artifactId, String classifier, String extension, String version) + throws IOException { File file = reactor.findArtifact(new DefaultArtifact(groupId, artifactId, classifier, extension, version)); - return file == null ? fallback.resolve(String.format("mvn:%s/%s/%s/%s/%s", groupId, artifactId, version, extension, classifier)) : file; - } - - @Override - public File resolve(String groupId, String artifactId, String classifier, String extension, String version, Exception previousException) throws IOException { - File file = reactor.findArtifact(new DefaultArtifact(groupId, artifactId, classifier, extension, version)); - return file == null ? fallback.resolve(String.format("mvn:%s/%s/%s/%s/%s", groupId, artifactId, version, extension, classifier), previousException) : file; - } - - @Override - public File resolveMetadata(String groupId, String artifactId, String type, String version) throws IOException { - return fallback.resolveMetadata(groupId, artifactId, type, version); - } - - @Override - public File resolveMetadata(String groupId, String artifactId, String type, String version, Exception previousException) throws IOException { - return fallback.resolveMetadata(groupId, artifactId, type, version, previousException); - } - - @Override - public void upload(String groupId, String artifactId, String classifier, String extension, String version, File artifact) throws IOException { - fallback.upload(groupId, artifactId, classifier, extension, version, artifact); - } - - @Override - public void uploadMetadata(String groupId, String artifactId, String type, String version, File artifact) throws IOException { - fallback.uploadMetadata(groupId, artifactId, type, version, artifact); + return file == null ? fallback.resolve(groupId, artifactId, classifier, extension, version) : file; } @Override