This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-analyser.git
The following commit(s) were added to refs/heads/master by this push:
new b91e3d1 SLING-13398 - Feature analyser does not properly handle
multi-release jars (#63)
b91e3d1 is described below
commit b91e3d19c34e21582e2b9300a7d55a4b129ac334
Author: Robert Munteanu <[email protected]>
AuthorDate: Tue Oct 27 14:39:56 2026 +0100
SLING-13398 - Feature analyser does not properly handle multi-release jars
(#63)
---
.../extensions/AnalyserMetaDataHandler.java | 7 +--
.../feature/scanner/impl/BundleDescriptorImpl.java | 42 +++++++++++++-
.../scanner/impl/BundleDescriptorImplTest.java | 65 +++++++++++++++++++++-
3 files changed, 106 insertions(+), 8 deletions(-)
diff --git
a/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java
b/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java
index 434c69c..48a867f 100644
---
a/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java
+++
b/src/main/java/org/apache/sling/feature/analyser/extensions/AnalyserMetaDataHandler.java
@@ -28,7 +28,6 @@ import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.StringJoiner;
-import java.util.jar.JarFile;
import jakarta.json.Json;
import jakarta.json.JsonObject;
@@ -44,9 +43,9 @@ import org.apache.sling.feature.Feature;
import org.apache.sling.feature.builder.HandlerContext;
import org.apache.sling.feature.builder.PostProcessHandler;
import org.apache.sling.feature.impl.felix.utils.resource.ResourceUtils;
-import org.apache.sling.feature.io.IOUtils;
import org.apache.sling.feature.scanner.BundleDescriptor;
import org.apache.sling.feature.scanner.PackageInfo;
+import org.apache.sling.feature.scanner.impl.BundleDescriptorImpl;
import org.apache.sling.feature.scanner.impl.SystemBundleDescriptor;
import org.apache.sling.feature.scanner.spi.FrameworkScanner;
import org.osgi.framework.Constants;
@@ -196,8 +195,8 @@ public class AnalyserMetaDataHandler implements
PostProcessHandler {
private Optional<JsonObject> getManifest(HandlerContext handlerContext,
ArtifactId bundle) {
URL url = handlerContext.getArtifactProvider().provide(bundle);
- try (JarFile jarFile = IOUtils.getJarFileFromURL(url, false, null)) {
- return Optional.ofNullable(jarFile.getManifest()).map(manifest -> {
+ try {
+ return
Optional.ofNullable(BundleDescriptorImpl.getManifest(url)).map(manifest -> {
JsonObjectBuilder manifestBuilder = Json.createObjectBuilder();
manifest.getMainAttributes().entrySet().stream()
.forEachOrdered(
diff --git
a/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
b/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
index e2074d1..772d71f 100644
---
a/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
+++
b/src/main/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImpl.java
@@ -21,6 +21,7 @@ package org.apache.sling.feature.scanner.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
@@ -32,6 +33,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
+import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
@@ -74,7 +76,20 @@ public class BundleDescriptorImpl extends BundleDescriptor {
/** The corresponding artifact from the feature. */
private final Artifact artifact;
- static Manifest getManifest(URL file) throws IOException {
+ /**
+ * Read the bundle manifest, applying the OSGi multi-release JAR
supplemental
+ * manifest for the running Java platform, if present.
+ * @param file The bundle URL
+ * @return The effective manifest, or {@code null} if there is no manifest
+ * @throws IOException If the manifest cannot be read
+ */
+ public static Manifest getManifest(URL file) throws IOException {
+ String javaVersion = System.getProperty("java.specification.version");
+ int majorVersion = Integer.parseInt(javaVersion.startsWith("1.") ?
javaVersion.substring(2) : javaVersion);
+ return getManifest(file, majorVersion);
+ }
+
+ static Manifest getManifest(URL file, int javaVersion) throws IOException {
Manifest manifest = null;
try {
if ("file".equals(file.getProtocol()) && new
File(file.toURI()).isDirectory()) {
@@ -88,6 +103,31 @@ public class BundleDescriptorImpl extends BundleDescriptor {
} else {
try (JarFile jarFile = IOUtils.getJarFileFromURL(file, true,
null)) {
manifest = jarFile.getManifest();
+ if (manifest != null
+ && "true"
+ .equalsIgnoreCase(
+
manifest.getMainAttributes().getValue("Multi-Release"))) {
+ // OSGi Core 3.2.8: use only the first supplemental
manifest found,
+ // and replace only Import-Package and
Require-Capability.
+ for (int version = javaVersion; version >= 9;
version--) {
+ JarEntry entry =
+ jarFile.getJarEntry("META-INF/versions/" +
version + "/OSGI-INF/MANIFEST.MF");
+ if (entry != null) {
+ try (InputStream is =
jarFile.getInputStream(entry)) {
+ Manifest supplemental = new Manifest(is);
+ for (String header :
+ new String[]
{Constants.IMPORT_PACKAGE, Constants.REQUIRE_CAPABILITY}) {
+ String value =
+
supplemental.getMainAttributes().getValue(header);
+ if (value != null) {
+
manifest.getMainAttributes().putValue(header, value);
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
} catch (final IOException ioe) {
// rethrow with more info
throw new IOException(file + " : " + ioe.getMessage(),
ioe);
diff --git
a/src/test/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImplTest.java
b/src/test/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImplTest.java
index 46b7bb7..f222ffc 100644
---
a/src/test/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImplTest.java
+++
b/src/test/java/org/apache/sling/feature/scanner/impl/BundleDescriptorImplTest.java
@@ -24,10 +24,15 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
+import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
@@ -39,6 +44,10 @@ import org.apache.sling.feature.scanner.FeatureDescriptor;
import org.apache.sling.feature.scanner.PackageInfo;
import org.apache.sling.feature.scanner.Scanner;
import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import static org.junit.Assert.assertEquals;
@@ -91,6 +100,47 @@ public class BundleDescriptorImplTest {
.getValue("Manifest-Version"));
}
+ @ParameterizedTest(name = "Java {0}")
+ @CsvSource({"8, 1.8, true", "21, 21, false"})
+ void testMultiReleaseBundle(int javaVersion, String
requiredExecutionEnvironmentVersion, boolean importsSunMisc)
+ throws Exception {
+ String rootManifest = "Bundle-SymbolicName: test.bundle\n"
+ + "Bundle-Version: 1\n"
+ + "Bundle-ManifestVersion: 2\n"
+ + "Multi-Release: true\n"
+ + "Import-Package: java.util,sun.misc\n"
+ + "Require-Capability:
osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"\n";
+ Map<String, String> entries = new HashMap<>();
+ for (int version : new int[] {11, 21}) {
+ entries.put(
+ "META-INF/versions/" + version + "/OSGI-INF/MANIFEST.MF",
+ "Manifest-Version: 1.0\n"
+ + "Import-Package: java.util\n"
+ + "Require-Capability:
osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=" + version + "))\"\n");
+ }
+ URL jar = createBundle(rootManifest, entries).toURI().toURL();
+ BundleDescriptorImpl descriptor = new BundleDescriptorImpl(
+ new Artifact(ArtifactId.fromMvnId("test:multi-release:1")),
+ jar,
+ null,
+ BundleDescriptorImpl.getManifest(jar, javaVersion));
+
+ Assertions.assertEquals(
+ importsSunMisc,
+ descriptor.getImportedPackages().stream().anyMatch(p ->
"sun.misc".equals(p.getName())));
+ Assertions.assertEquals(
+ "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=" +
requiredExecutionEnvironmentVersion + "))\"",
+
descriptor.getManifest().getMainAttributes().getValue(Constants.REQUIRE_CAPABILITY));
+ Assertions.assertEquals(
+ "(&(osgi.ee=JavaSE)(version=" +
requiredExecutionEnvironmentVersion + "))",
+ descriptor.getRequirements().stream()
+ .filter(r -> "osgi.ee".equals(r.getNamespace()))
+ .findFirst()
+ .get()
+ .getDirectives()
+ .get("filter"));
+ }
+
/**
* for SLING-10182
*/
@@ -119,12 +169,21 @@ public class BundleDescriptorImplTest {
}
private File createBundle(String manifest) throws IOException {
+ return createBundle(manifest, Collections.emptyMap());
+ }
+
+ private File createBundle(String manifest, Map<String, String> entries)
throws IOException {
File f = Files.createTempFile("bundle", ".jar").toFile();
f.deleteOnExit();
- Manifest mf = new Manifest(new
ByteArrayInputStream(manifest.getBytes("UTF-8")));
+ Manifest mf = new Manifest(new
ByteArrayInputStream(manifest.getBytes(StandardCharsets.UTF_8)));
mf.getMainAttributes().putValue("Manifest-Version", "1.0");
- JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf);
- os.close();
+ try (JarOutputStream os = new JarOutputStream(new FileOutputStream(f),
mf)) {
+ for (Map.Entry<String, String> entry : entries.entrySet()) {
+ os.putNextEntry(new JarEntry(entry.getKey()));
+ os.write(entry.getValue().getBytes(StandardCharsets.UTF_8));
+ os.closeEntry();
+ }
+ }
return f;
}