SLING-9425 Analyser fails when bundle requires Java 11

Launch a real framework to obtain framework exports and capabilities.
diff --git a/pom.xml b/pom.xml
index f083af0..d595693 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,12 +84,6 @@
     </build>
     <dependencies>
         <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-text</artifactId>
-            <version>1.6</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
             <groupId>org.apache.geronimo.specs</groupId>
             <artifactId>geronimo-json_1.1_spec</artifactId>
             <version>1.0</version>
@@ -194,5 +188,12 @@
             <version>1.1.1</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <!-- Used by some tests -->
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>6.0.3</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java b/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
index 12f7879..7698a65 100644
--- a/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScanner.java
@@ -16,25 +16,30 @@
  */
 package org.apache.sling.feature.scanner.impl;
 
-
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Enumeration;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
-import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
 
-import org.apache.commons.text.StringSubstitutor;
-import org.apache.commons.text.lookup.StringLookup;
 import org.apache.felix.utils.manifest.Parser;
 import org.apache.felix.utils.resource.ResourceBuilder;
 import org.apache.sling.feature.Artifact;
@@ -43,6 +48,7 @@
 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.fwk.FrameworkPropertiesGatherer;
 import org.apache.sling.feature.scanner.spi.FrameworkScanner;
 import org.osgi.framework.Constants;
 import org.osgi.resource.Capability;
@@ -103,120 +109,112 @@
         return d;
     }
 
-    private List<Capability> calculateSystemCapabilities(final Map<String,String> fwkProps) throws IOException
-    {
-         Map<String, String> mf = new HashMap<>();
-         mf.put(Constants.PROVIDE_CAPABILITY,
-                Stream.of(
-                    fwkProps.get(Constants.FRAMEWORK_SYSTEMCAPABILITIES),
-                    fwkProps.get(Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA)
-                )
-                .filter(Objects::nonNull)
-                .collect(Collectors.joining(",")));
-         mf.put(Constants.BUNDLE_SYMBOLICNAME, Constants.SYSTEM_BUNDLE_SYMBOLICNAME);
-         mf.put(Constants.BUNDLE_MANIFESTVERSION, "2");
-         try
-         {
-             return ResourceBuilder.build(null, mf).getCapabilities(null);
-         }
-         catch (Exception ex) {
-             throw new IOException(ex);
-         }
+    private List<Capability> calculateSystemCapabilities(final Map<String,String> fwkProps) throws IOException {
+        Map<String, String> mf = new HashMap<>();
+        mf.put(Constants.PROVIDE_CAPABILITY, fwkProps.get(Constants.FRAMEWORK_SYSTEMCAPABILITIES));
+        mf.put(Constants.BUNDLE_SYMBOLICNAME, Constants.SYSTEM_BUNDLE_SYMBOLICNAME);
+        mf.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        try
+        {
+            return ResourceBuilder.build(null, mf).getCapabilities(null);
+        }
+        catch (Exception ex) {
+            throw new IOException(ex);
+        }
     }
 
     private Set<PackageInfo> calculateSystemPackages(final Map<String,String> fwkProps) {
-        return
-            Stream.of(
-                Parser.parseHeader(
-                    Stream.of(
-                        fwkProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES),
-                        fwkProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA)
-                    ).filter(Objects::nonNull)
-                    .collect(Collectors.joining(","))
-                )
-            ).map(
+        return Stream.of(
+                Parser.parseHeader(fwkProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES)))
+            .map(
                 clause -> new PackageInfo(clause.getName(), clause.getAttribute("version") != null ? clause.getAttribute("version") : "0.0.0", false))
             .collect(Collectors.toSet());
     }
 
-    private static final String DEFAULT_PROPERTIES = "default.properties";
-
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     Map<String,String> getFrameworkProperties(final Map<String,String> appProps, final URL framework)
-    throws IOException {
-        final Map<String, Properties> propsMap = new HashMap<>();
-
-        try (final JarFile zipFile = IOUtils.getJarFileFromURL(framework, true, null)) {
-            Enumeration<? extends ZipEntry> entries = zipFile.entries();
-            
-            while ( entries.hasMoreElements() ) {
-                final ZipEntry entry = entries.nextElement();
-                try (final InputStream zis = zipFile.getInputStream(entry)) {
-                    final String entryName = entry.getName();
-                    if ( entryName.endsWith(".properties") ) {
-                        final Properties props = new Properties();
-                        props.load(zis);
-                        propsMap.put(entryName, props);
-                    }
-                } 
-            }
+            throws IOException {
+        Path appPropsFile = Files.createTempFile("appProps", ".properties");
+        Properties appPropsProperties = new Properties();
+        appPropsProperties.putAll(appProps);
+        try (Writer writer = new FileWriter(appPropsFile.toFile())) {
+            appPropsProperties.store(writer, "appProps");
         }
 
-        final Properties defaultMap = propsMap.get(DEFAULT_PROPERTIES);
-        if ( defaultMap == null ) {
-            return null;
+        File frameworkJar = IOUtils.getFileFromURL(framework, true, null);
+        File gathererCP = getGathererClassPath();
+
+        Path outFile = Files.createTempFile("frameworkCaps", ".properties");
+        Path runDir = Files.createTempDirectory("frameworkCaps");
+
+        List<String> commandLine = Arrays.asList(
+                "-cp",
+                gathererCP + File.pathSeparator + frameworkJar.getAbsolutePath(),
+                FrameworkPropertiesGatherer.class.getName(),
+                appPropsFile.toString(),
+                outFile.toString());
+
+        try {
+            runJava(new ArrayList<>(commandLine), runDir);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IOException(e);
         }
 
-        final Map<String,String> frameworkProps = new HashMap<>();
-        appProps.forEach((key, value) -> frameworkProps.put(key, value.replace("{dollar}", "$")));
-
-        // replace variables
-        defaultMap.put("java.specification.version","1.8");
-
-        defaultMap.put("felix.detect.java.specification.version", "1.8");
-        defaultMap.put("felix.detect.java.version", "0.0.0.JavaSE_018");
-
-
-
-        StringSubstitutor ss = new StringSubstitutor(new StringLookup() {
-
-            @Override
-            public String lookup(String key) {
-                // Normally if a variable cannot be found, StrSubstitutor will
-                // leave the raw variable in place. We need to replace it with
-                // nothing in that case.
-
-                String val = defaultMap.getProperty(key);
-                return val != null ? val : "";
-            }
-        });
-        ss.setEnableSubstitutionInVariables(true);
-
-        for(final Object name : defaultMap.keySet()) {
-            if ( frameworkProps.get(name.toString()) == null ) {
-                final String value = (String)defaultMap.get(name);
-                final String substValue = ss.replace(value);
-                frameworkProps.put(name.toString(), substValue);
-            }
+        Properties gatheredProps = new Properties();
+        try (Reader reader = new FileReader(outFile.toFile())) {
+            gatheredProps.load(reader);
         }
 
-        ss = new StringSubstitutor(new StringLookup() {
+        // after reading, delete all temp files and dirs
+        Files.delete(appPropsFile);
+        Files.delete(outFile);
 
-            @Override
-            public String lookup(String key) {
-                // Normally if a variable cannot be found, StrSubstitutor will
-                // leave the raw variable in place. We need to replace it with
-                // nothing in that case.
-
-                String val = frameworkProps.get(key);
-                return val != null ? val.replace("{dollar}", "$") : "";
-            }
-        });
-
-        ss.setEnableSubstitutionInVariables(true);
-        for (Map.Entry<String, String> entry : frameworkProps.entrySet()) {
-            entry.setValue(ss.replace(entry.getValue().replace("{dollar}", "$")));
+        try (Stream<Path> fileStream = Files.walk(runDir)) {
+            fileStream.sorted(Comparator.reverseOrder())
+            .map(Path::toFile)
+            .forEach(File::delete);
         }
 
-        return frameworkProps;
+        return (Map) gatheredProps;
+    }
+
+    private File getGathererClassPath() throws IOException {
+        return getClasspathForClass(FrameworkPropertiesGatherer.class);
+    }
+
+    static File getClasspathForClass(Class<?> cls) throws IOException {
+        String clsName = cls.getName();
+        String resName = "/" + clsName.replace('.', '/') + ".class";
+        URL resource = cls.getResource(resName);
+        String resURL = URLDecoder.decode(resource.toString(), StandardCharsets.UTF_8.name());
+        if (!resURL.startsWith("jar:file:")) {
+            String urlFile = resource.getFile();
+            return new File(urlFile.substring(0, urlFile.length() - resName.length()));
+        }
+
+        int pingSlash = resURL.indexOf("!/");
+        String fileURL = resURL.substring("jar:".length(), pingSlash);
+        return new File(new URL(fileURL).getFile());
+    }
+
+    private static void runJava(List<String> commandLine, Path execDir)
+            throws IOException, InterruptedException {
+        String java = System.getProperty("java.home") + "/bin/java";
+        commandLine.add(0, java);
+        runCommand(commandLine, execDir);
+    }
+
+    private static void runCommand(List<String> commandLine, Path execDir)
+            throws IOException, InterruptedException {
+        Process process = new ProcessBuilder(commandLine)
+            .directory(execDir.toFile())
+            .inheritIO()
+            .start();
+        int res = process.waitFor();
+        if (res != 0) {
+            throw new IOException("Process returned with a failure: " + res);
+        }
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/fwk/FrameworkPropertiesGatherer.java b/src/main/java/org/apache/sling/feature/scanner/impl/fwk/FrameworkPropertiesGatherer.java
new file mode 100644
index 0000000..2cc6318
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/fwk/FrameworkPropertiesGatherer.java
@@ -0,0 +1,78 @@
+/*
+ * 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.sling.feature.scanner.impl.fwk;
+
+import org.osgi.framework.Constants;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+/**
+ * This class launches an OSGi framework and obtains the system packages and capabilities provided by it.
+ * It takes 2 arguments: <ul>
+ *
+ *  <li>1st argument: the file name of a properties file containing the properties to use to initialise the framework
+ *  <li>2nd argument: the file name to write the result into. This will also be written in the form of a properties file.
+ *
+ * </ul>After obtaining the values, the launched framework is stopped.
+ */
+public class FrameworkPropertiesGatherer {
+    public static void main(String [] args) throws Exception {
+        if (args.length != 2) {
+            System.err.print("usage: FrameworkPropertiesGatherer <in props filename> <out props filename>");
+            System.exit(1);
+        }
+
+        Properties inProps = new Properties();
+        try (Reader reader = new FileReader(args[0])) {
+            inProps.load(reader);
+        }
+
+        ServiceLoader<FrameworkFactory> ldr = ServiceLoader.load(FrameworkFactory.class);
+        FrameworkFactory ff = ldr.iterator().next();
+
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        Framework fwk = ff.newFramework((Map) inProps);
+
+        fwk.init();
+        fwk.start();
+
+        // The headers will contain the computed export packages and framework capabilities.
+        Dictionary<String, String> headers = fwk.getHeaders();
+
+        Properties outProps = new Properties();
+        outProps.put(Constants.FRAMEWORK_SYSTEMPACKAGES, headers.get(Constants.EXPORT_PACKAGE));
+        outProps.put(Constants.FRAMEWORK_SYSTEMCAPABILITIES, headers.get(Constants.PROVIDE_CAPABILITY));
+
+        fwk.stop();
+
+        try (Writer writer = new FileWriter(args[1])) {
+            outProps.store(writer, "Framework exports and capabilities");
+        }
+
+        fwk.waitForStop(1000);
+        System.exit(0); // To be sure
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImportsTest.java b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImportsTest.java
index e56e429..5ebae03 100644
--- a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImportsTest.java
+++ b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckBundleExportsImportsTest.java
@@ -46,7 +46,7 @@
     public static void setupClass() {
         resourceRoot =
                 new File(CheckBundleExportsImportsTest.class.
-                        getResource("/test-framework.jar").getFile()).getParentFile();
+                        getResource("/test-content.zip").getFile()).getParentFile();
     }
 
     @Test
diff --git a/src/test/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScannerTest.java b/src/test/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScannerTest.java
index 617a986..857be73 100644
--- a/src/test/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScannerTest.java
+++ b/src/test/java/org/apache/sling/feature/scanner/impl/FelixFrameworkScannerTest.java
@@ -16,49 +16,83 @@
  */
 package org.apache.sling.feature.scanner.impl;
 
+import org.apache.felix.framework.Felix;
 import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.builder.ArtifactProvider;
 import org.apache.sling.feature.scanner.BundleDescriptor;
+import org.apache.sling.feature.scanner.PackageInfo;
 import org.junit.Test;
+import org.osgi.framework.Constants;
+import org.osgi.resource.Capability;
 
 import java.io.File;
+import java.io.IOException;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 public class FelixFrameworkScannerTest {
     @Test
     public void testGetFrameworkProperties() throws Exception {
-        URL url = getClass().getResource("/test-framework.jar");
+        URL url = getFelixFrameworkJar();
         File fwFile = new File(url.toURI());
 
         FelixFrameworkScanner ffs = new FelixFrameworkScanner();
 
+        String additionalVersions = getAdditionalVersionNumbers();
+
         Map<String,String> kvmap = new HashMap<>();
         Map<String,String> props = ffs.getFrameworkProperties(kvmap, fwFile.toURI().toURL());
-        assertEquals("osgi.service; objectClass:List<String>=org.osgi.service.resolver.Resolver; "
+        String expected = "osgi.service; objectClass:List<String>=org.osgi.service.resolver.Resolver; "
                     + "uses:=org.osgi.service.resolver, "
                 + "osgi.service; objectClass:List<String>=org.osgi.service.startlevel.StartLevel; "
                     + "uses:=org.osgi.service.startlevel, "
                 + "osgi.service; objectClass:List<String>=org.osgi.service.packageadmin.PackageAdmin; "
                     + "uses:=org.osgi.service.packageadmin , "
                 + "osgi.ee; osgi.ee=\"OSGi/Minimum\"; version:List<Version>=\"1.0,1.1,1.2\", "
-                + "osgi.ee; osgi.ee=\"JavaSE\"; version:List<Version>=\"1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8\", "
-                + "osgi.ee; osgi.ee=\"JavaSE/compact1\"; version:List<Version>=\"1.8\", "
-                + "osgi.ee; osgi.ee=\"JavaSE/compact2\"; version:List<Version>=\"1.8\", "
-                + "osgi.ee; osgi.ee=\"JavaSE/compact3\"; version:List<Version>=\"1.8\" ", props.get("org.osgi.framework.system.capabilities"));
+                + "osgi.ee; osgi.ee=\"JavaSE\"; version:List<Version>=\"1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8"
+                    + additionalVersions + "\", "
+                + "osgi.ee; osgi.ee=\"JavaSE/compact1\"; version:List<Version>=\"1.8"
+                    + additionalVersions + "\", "
+                + "osgi.ee; osgi.ee=\"JavaSE/compact2\"; version:List<Version>=\"1.8"
+                    + additionalVersions + "\", "
+                + "osgi.ee; osgi.ee=\"JavaSE/compact3\"; version:List<Version>=\"1.8"
+                    + additionalVersions + "\"";
+        String actual = props.get("org.osgi.framework.system.capabilities");
+
+        // Remove spaces
+        assertEquals(expected.replaceAll("\\s",""), actual.replaceAll("\\s",""));
+    }
+
+    private String getAdditionalVersionNumbers() {
+        StringBuilder sb = new StringBuilder();
+
+        String javaVersion = System.getProperty("java.specification.version");
+        if (!javaVersion.startsWith("1.")) {
+            int curVersion = Integer.parseInt(javaVersion);
+
+            for (int i=9; i<=curVersion; i++) {
+                sb.append(',');
+                sb.append(i);
+            }
+        }
+
+        return sb.toString();
     }
 
     @Test
     public void testGetFrameworkExports() throws Exception {
-        URL fwFile = getClass().getResource("/test-framework.jar");
-
+        URL fwFile = getFelixFrameworkJar();
         FelixFrameworkScanner ffs = new FelixFrameworkScanner();
 
         Map<String,String> kvmap = new HashMap<>();
+        kvmap.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, "org.foo.bar");
+        kvmap.put(Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA, "ding.dong;ding.dong=\"yeah!\"");
 
         BundleDescriptor bundleDescriptor = ffs.scan(new ArtifactId("org.apache.felix",
                 "org.apache.felix.framework",
@@ -70,7 +104,26 @@
                     }
                 });
 
-        assertFalse(bundleDescriptor.getExportedPackages().isEmpty());
-        assertFalse(bundleDescriptor.getCapabilities().isEmpty());
+        Set<PackageInfo> exportedPackages = bundleDescriptor.getExportedPackages();
+        assertFalse(exportedPackages.isEmpty());
+        boolean foundFooBar = false;
+        for (PackageInfo pi : exportedPackages) {
+            if (pi.getName().equals("org.foo.bar"))
+                foundFooBar = true;
+        }
+        assertTrue(foundFooBar);
+
+        Set<Capability> providedCaps = bundleDescriptor.getCapabilities();
+        assertFalse(providedCaps.isEmpty());
+        boolean foundDingDong = false;
+        for (Capability cap : providedCaps) {
+            if (cap.getNamespace().equals("ding.dong") && "yeah!".equals(cap.getAttributes().get("ding.dong")))
+                foundDingDong = true;
+        }
+        assertTrue(foundDingDong);
+    }
+
+    private URL getFelixFrameworkJar() throws IOException {
+        return FelixFrameworkScanner.getClasspathForClass(Felix.class).toURI().toURL();
     }
 }
diff --git a/src/test/resources/test-framework.jar b/src/test/resources/test-framework.jar
deleted file mode 100644
index 9ce022d..0000000
--- a/src/test/resources/test-framework.jar
+++ /dev/null
Binary files differ