Implementation of multi-release jar support for Netbinox
diff --git a/platform/netbinox/nbproject/project.properties b/platform/netbinox/nbproject/project.properties
index dfa06c9..dc24e53 100644
--- a/platform/netbinox/nbproject/project.properties
+++ b/platform/netbinox/nbproject/project.properties
@@ -17,7 +17,7 @@
 
 is.autoload=true
 release.external/org.eclipse.osgi_3.9.1.nb9.jar=modules/ext/org.eclipse.osgi_3.9.1.nb9.jar
-javac.source=1.6
+javac.source=1.8
 javac.target=1.8
 javac.compilerargs=-Xlint -Xlint:-serial
 
diff --git a/platform/netbinox/nbproject/project.xml b/platform/netbinox/nbproject/project.xml
index e123c78..da9a30a 100644
--- a/platform/netbinox/nbproject/project.xml
+++ b/platform/netbinox/nbproject/project.xml
@@ -92,6 +92,11 @@
                         <recursive/>
                         <compile-dependency/>
                     </test-dependency>
+                    <test-dependency>
+                        <code-name-base>org.openide.util.ui</code-name-base>
+                        <compile-dependency/>
+                        <test/>
+                    </test-dependency>
                 </test-type>
             </test-dependencies>
             <public-packages>
diff --git a/platform/netbinox/src/org/netbeans/modules/netbinox/JarBundleFile.java b/platform/netbinox/src/org/netbeans/modules/netbinox/JarBundleFile.java
index eb916cc..077ccf2 100644
--- a/platform/netbinox/src/org/netbeans/modules/netbinox/JarBundleFile.java
+++ b/platform/netbinox/src/org/netbeans/modules/netbinox/JarBundleFile.java
@@ -30,17 +30,21 @@
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.Attributes.Name;
+import java.util.jar.Manifest;
 import java.util.logging.Level;
 import org.eclipse.osgi.baseadaptor.BaseData;
 import org.eclipse.osgi.baseadaptor.bundlefile.BundleEntry;
 import org.eclipse.osgi.baseadaptor.bundlefile.BundleFile;
 import org.eclipse.osgi.baseadaptor.bundlefile.DirBundleFile;
-import org.eclipse.osgi.baseadaptor.bundlefile.DirZipBundleEntry;
 import org.eclipse.osgi.baseadaptor.bundlefile.MRUBundleFileList;
 import org.eclipse.osgi.baseadaptor.bundlefile.ZipBundleFile;
 import org.netbeans.core.netigso.spi.BundleContent;
 import org.netbeans.core.netigso.spi.NetigsoArchive;
 import org.openide.modules.ModuleInfo;
+import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
 
 /** This is fake bundle. It is created by the Netbinox infrastructure to 
@@ -49,14 +53,36 @@
  * @author Jaroslav Tulach <jtulach@netbeans.org>
  */
 final class JarBundleFile extends BundleFile implements BundleContent {
-    private BundleFile delegate;
-
+    //
+    // When making changes to this file, check if
+    // platform/o.n.bootstrap/src/org/netbeans/JarClassLoader.java (JarClassLoader/JarSource)
+    // should also be adjusted. At least the multi-release handling is similar.
+    //
+    private static final String META_INF = "META-INF/";
+    private static final Name MULTI_RELEASE = new Name("Multi-Release");
+    private static final int BASE_VERSION = 8;
+    private static final int RUNTIME_VERSION;
     private static Map<Long,File> usedIds;
 
+    static {
+        int version;
+        try {
+            Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);
+            version = (int) runtimeVersion.getClass().getMethod("major").invoke(runtimeVersion);
+        } catch (ReflectiveOperationException ex) {
+            version = BASE_VERSION;
+        }
+        RUNTIME_VERSION = version;
+    }
+
+    private BundleFile delegate;
+
     private final MRUBundleFileList mru;
     private final BaseData data;
     private final NetigsoArchive archive;
-    
+    private int[] versions;
+    private Boolean isMultiRelease;
+
     JarBundleFile(
         File base, BaseData data, NetigsoArchive archive,
         MRUBundleFileList mru, boolean isBase
@@ -171,6 +197,18 @@
 
     @Override
     public File getFile(String file, boolean bln) {
+        if (((! file.startsWith(META_INF)) ) && isMultiRelease()) {
+            for (int version : getVersions()) {
+                File f = getFile0("META-INF/versions/" + version + "/" + file, bln);
+                if (f != null) {
+                    return f;
+                }
+            }
+        }
+        return getFile0(file, bln);
+    }
+
+    private File getFile0(String file, boolean bln) {
         byte[] exists = getCachedEntry(file);
         if (exists == null) {
             return null;
@@ -181,6 +219,18 @@
 
     @Override
     public byte[] resource(String name) throws IOException {
+        if ((! name.startsWith(META_INF)) && isMultiRelease()) {
+            for (int version : getVersions()) {
+                byte[] b = resource0("META-INF/versions/" + version + "/" + name);
+                if (b != null) {
+                    return b;
+                }
+            }
+        }
+        return resource0(name);
+    }
+
+    private byte[] resource0(String name) throws IOException {
         BundleEntry u = findEntry("resource", name);
         if (u == null) {
             return null;
@@ -262,6 +312,18 @@
 
     @Override
     public BundleEntry getEntry(final String name) {
+        if ((! name.startsWith(META_INF)) && isMultiRelease()) {
+            for (int version : getVersions()) {
+                BundleEntry be = getEntry0("META-INF/versions/" + version + "/" + name);
+                if(be != null) {
+                    return be;
+                }
+            }
+        }
+        return getEntry0(name);
+    }
+
+    private BundleEntry getEntry0(final String name) {
         if (!archive.isActive()) {
             return delegate("inactive", name).getEntry(name); // NOI18N
         }
@@ -351,4 +413,50 @@
             return findEntry("getFileURL", name).getFileURL(); // NOI18N
         }
     }
+
+    /**
+     * @return versions for which a {@code META-INF/versions/NUMBER} entry exists.
+     * The order is from largest version to lowest. Only versions supported by
+     * the runtime VM are reported.
+     */
+    private int[] getVersions() {
+        if (versions != null) {
+            return versions;
+        }
+
+        Set<Integer> vers = new TreeSet<>(Collections.reverseOrder());
+        for(int i = BASE_VERSION; i <= RUNTIME_VERSION; i++) {
+            String directory = "META-INF/versions/" + i;
+            BundleEntry be = delegate("getVersions", directory).getEntry(directory);
+            if (be != null) {
+                vers.add(i);
+            }
+        }
+        int[] ret = new int[vers.size()];
+        int i = 0;
+        for (Integer ver : vers) {
+            ret[i++] = ver;
+        }
+        versions = ret;
+        return versions;
+    }
+
+    private boolean isMultiRelease() {
+        if(isMultiRelease != null) {
+            return isMultiRelease;
+        }
+        BundleEntry be = delegate("isMultiRelease", "META-INF/MANIFEST.MF").getEntry("META-INF/MANIFEST.MF");
+        if(be == null) {
+            isMultiRelease = false;
+        } else {
+            try {
+                Manifest manifest = new Manifest(be.getInputStream());
+                isMultiRelease = Boolean.valueOf(manifest.getMainAttributes().getValue(MULTI_RELEASE));
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+
+        }
+        return isMultiRelease;
+    }
 }
diff --git a/platform/netbinox/src/org/netbeans/modules/netbinox/NetbinoxLoader.java b/platform/netbinox/src/org/netbeans/modules/netbinox/NetbinoxLoader.java
index bfeefde..34c0fae 100644
--- a/platform/netbinox/src/org/netbeans/modules/netbinox/NetbinoxLoader.java
+++ b/platform/netbinox/src/org/netbeans/modules/netbinox/NetbinoxLoader.java
@@ -20,8 +20,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.ProtectionDomain;
@@ -34,7 +32,6 @@
 import org.eclipse.osgi.baseadaptor.loader.ClasspathManager;
 import org.eclipse.osgi.framework.adaptor.ClassLoaderDelegate;
 import org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader;
-import org.openide.util.Exceptions;
 import org.osgi.framework.FrameworkEvent;
 
 /** Classloader that eliminates some unnecessary disk touches.
diff --git a/platform/netbinox/test/unit/src/org/netbeans/modules/netbinox/NetbinoxMultiversionJarTest.java b/platform/netbinox/test/unit/src/org/netbeans/modules/netbinox/NetbinoxMultiversionJarTest.java
new file mode 100644
index 0000000..f186ea4
--- /dev/null
+++ b/platform/netbinox/test/unit/src/org/netbeans/modules/netbinox/NetbinoxMultiversionJarTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.netbeans.modules.netbinox;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.ToolProvider;
+import org.netbeans.MockEvents;
+import org.netbeans.MockModuleInstaller;
+import org.netbeans.ModuleManager;
+import org.openide.util.test.TestFileUtils;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static junit.framework.TestCase.assertEquals;
+
+public class NetbinoxMultiversionJarTest extends NetigsoHid {
+
+    public NetbinoxMultiversionJarTest(String name) {
+        super(name);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        Locale.setDefault(new Locale("te", "ST"));
+        clearWorkDir();
+        File ud = new File(getWorkDir(), "ud");
+        ud.mkdirs();
+        System.setProperty("netbeans.user", ud.getPath());
+
+        data = new File(getDataDir(), "jars");
+        jars = new File(getWorkDir(), "space in path");
+        jars.mkdirs();
+
+        File classes = new File(getWorkDir(), "classes");
+        classes.mkdirs();
+        ToolProvider.getSystemJavaCompiler()
+                .getTask(null, null, d -> {
+                    throw new IllegalStateException(d.toString());
+                }, Arrays.asList("-d", classes.getAbsolutePath()), null,
+                        Arrays.asList(new SourceFileObject("test/Impl.java", "package test; public class Impl { public static String get() { return \"base\"; } }"),
+                                new SourceFileObject("api/API.java", "package api; public class API { public static String run() { return test.Impl.get(); } }")))
+                .call();
+        File classes9 = new File(new File(new File(classes, "META-INF"), "versions"), "9");
+        classes9.mkdirs();
+        ToolProvider.getSystemJavaCompiler()
+                .getTask(null, null, d -> {
+                    throw new IllegalStateException(d.toString());
+                }, Arrays.asList("-d", classes9.getAbsolutePath(), "-classpath", classes.getAbsolutePath()), null,
+                        Arrays.asList(new SourceFileObject("test/Impl.java", "package test; public class Impl { public static String get() { return \"9\"; } }")))
+                .call();
+        Map<String, byte[]> jarContent = new LinkedHashMap<>();
+        String manifest
+                = "Manifest-Version: 1.0\n"
+                + "Bundle-SymbolicName: test.module\n"
+                + "Bundle-Version: 1.0\n"
+                + "Multi-Release: true\n"
+                + "";
+        jarContent.put("META-INF/MANIFEST.MF", manifest.getBytes(UTF_8));
+        Path classesPath = classes.toPath();
+        Files.walk(classesPath)
+                .filter(p -> Files.isRegularFile(p))
+                .forEach(p -> {
+                    try {
+                        jarContent.put(classesPath.relativize(p).toString(), TestFileUtils.readFileBin(p.toFile()));
+                    } catch (IOException ex) {
+                        throw new IllegalStateException(ex);
+                    }
+                });
+        jarContent.put("test/dummy.txt", "base".getBytes(UTF_8));
+        jarContent.put("META-INF/versions/9/test/dummy.txt", "9".getBytes(UTF_8));
+        simpleModule = new File(jars, "multi-release.jar");
+        try ( OutputStream out = new FileOutputStream(simpleModule)) {
+            TestFileUtils.writeZipFile(out, jarContent);
+        }
+    }
+
+    public void testMultiReleaseJar() throws Exception {
+        MockModuleInstaller installer = new MockModuleInstaller();
+        MockEvents ev = new MockEvents();
+        ModuleManager mgr = new ModuleManager(installer, ev);
+        mgr.mutexPrivileged().enterWriteAccess();
+        Set<org.netbeans.Module> all = null;
+        try {
+            org.netbeans.Module m1 = mgr.create(simpleModule, null, false, false, false);
+            all = Collections.singleton(m1);
+
+            mgr.enable(all);
+
+            // Check multi release class loading
+            Class<?> impl = m1.getClassLoader().loadClass("test.Impl");
+            Method get = impl.getMethod("get");
+            String output = (String) get.invoke(null);
+
+            String expected;
+            try {
+                Class.forName("java.lang.Runtime$Version");
+                expected = "9";
+            } catch (ClassNotFoundException ex) {
+                expected = "base";
+            }
+            assertEquals(expected, output);
+
+            // Check multi release resource loading
+            try(InputStream is = m1.getClassLoader().getResourceAsStream("test/dummy.txt")) {
+                assertEquals(expected, loadUTF8(is));
+            }
+
+        } finally {
+            if (all != null) {
+                mgr.disable(all);
+            }
+            mgr.mutexPrivileged().exitWriteAccess();
+        }
+
+    }
+
+    private static String loadUTF8(InputStream is) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[2048];
+        int read;
+        while ((read = is.read(buffer)) > 0) {
+            baos.write(buffer, 0, read);
+        }
+        return baos.toString("UTF-8");
+    }
+
+    private static final class SourceFileObject extends SimpleJavaFileObject {
+
+        private final String content;
+
+        public SourceFileObject(String path, String content) throws URISyntaxException {
+            super(new URI("mem://" + path), JavaFileObject.Kind.SOURCE);
+            this.content = content;
+        }
+
+        @Override
+        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+            return content;
+        }
+
+    }
+}