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