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, ornull 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.
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;
+ }
+ Dequemvn: 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 amvn: 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)}.
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.
configuration.
+ * @return a new resolver.
+ */
+ MavenResolver create(Dictionarymvn:
+ * 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(Dictionarymvn: 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 ServiceRegistrationCreating 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 DictionaryThis bundle is the only place in Karaf that links against org.ops4j.pax.url.mvn.