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;
     }
 

Reply via email to