Merge pull request #4589 from matthiasblaesing/multi-release
Multi-release jar support
diff --git a/.travis.yml b/.travis.yml
index 0fad142..f25454b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -198,6 +198,20 @@
- hide-logs.sh ant $OPTS -f platform/templates test
- hide-logs.sh ant $OPTS -f platform/templatesui test
- hide-logs.sh ant $OPTS -f platform/uihandler test
+
+ - name: Test platform modules on JDK 11, Batch 1
+ jdk: openjdk8
+ env:
+ - OPTS="-Dmetabuild.jsonurl=https://raw.githubusercontent.com/apache/netbeans-jenkins-lib/master/meta/netbeansrelease.json -silent -Dcluster.config=platform -Djavac.compilerargs=-nowarn -Dbuild.compiler.deprecation=false -Dtest-unit-sys-prop.ignore.random.failures=true -Dvanilla.javac.exists=true"
+ before_script:
+ - nbbuild/travis/ant.sh $OPTS clean
+ - nbbuild/travis/ant.sh $OPTS build
+ - wget https://cdn.azul.com/zulu/bin/zulu11.58.23-ca-jdk11.0.16.1-linux_x64.tar.gz
+ - tar --extract --gzip --directory $HOME -f zulu11.58.23-ca-jdk11.0.16.1-linux_x64.tar.gz
+ - TEST_JDK=$HOME/zulu11.58.23-ca-jdk11.0.16.1-linux_x64
+ - export OPTS="$OPTS -Dtest.nbjdk.home=$TEST_JDK"
+ script:
+ - hide-logs.sh ant $OPTS -f platform/o.n.bootstrap test
- name: Test ide modules
jdk: openjdk8
@@ -441,8 +455,9 @@
env:
- OPTS="-Dmetabuild.jsonurl=https://raw.githubusercontent.com/apache/netbeans-jenkins-lib/master/meta/netbeansrelease.json -quiet -Dcluster.config=java -Djavac.compilerargs=-nowarn -Dbuild.compiler.deprecation=false -Dtest-unit-sys-prop.ignore.random.failures=true"
before_script:
- - wget https://raw.githubusercontent.com/sormuras/bach/master/install-jdk.sh
- - export TEST_JDK=`bash install-jdk.sh --feature 11 --license GPL --emit-java-home --silent | tail -1`
+ - wget https://cdn.azul.com/zulu/bin/zulu11.58.23-ca-jdk11.0.16.1-linux_x64.tar.gz
+ - tar --extract --gzip --directory $HOME -f zulu11.58.23-ca-jdk11.0.16.1-linux_x64.tar.gz
+ - TEST_JDK=$HOME/zulu11.58.23-ca-jdk11.0.16.1-linux_x64
- export OPTS="-Dmetabuild.jsonurl=https://raw.githubusercontent.com/apache/netbeans-jenkins-lib/master/meta/netbeansrelease.json $OPTS -Dtest.nbjdk.home=$TEST_JDK -Dtest.run.args=--limit-modules=java.base,java.logging,java.xml,java.prefs,java.desktop,java.management,java.instrument,jdk.zipfs,java.scripting,java.naming -Dtest.bootclasspath.prepend.args=-Dno.netbeans.bootclasspath.prepend.needed=true"
- nbbuild/travis/ant.sh $OPTS clean
- nbbuild/travis/ant.sh $OPTS build
@@ -620,8 +635,9 @@
before_script:
- nbbuild/travis/ant.sh $BUILD_OPTS clean
- nbbuild/travis/ant.sh $BUILD_OPTS build
- - wget https://raw.githubusercontent.com/sormuras/bach/master/install-jdk.sh
- - export TEST_JDK=`bash install-jdk.sh --feature 11 --license GPL --emit-java-home --silent | tail -1`
+ - wget https://cdn.azul.com/zulu/bin/zulu11.58.23-ca-jdk11.0.16.1-linux_x64.tar.gz
+ - tar --extract --gzip --directory $HOME -f zulu11.58.23-ca-jdk11.0.16.1-linux_x64.tar.gz
+ - TEST_JDK=$HOME/zulu11.58.23-ca-jdk11.0.16.1-linux_x64
- export OPTS="-Dmetabuild.jsonurl=https://raw.githubusercontent.com/apache/netbeans-jenkins-lib/master/meta/netbeansrelease.json $OPTS -Dtest.nbjdk.home=$TEST_JDK -Dtest.run.args=--limit-modules=java.base,java.logging,java.xml,java.prefs,java.desktop,java.management,java.instrument,jdk.zipfs,java.scripting,java.naming -Dtest.bootclasspath.prepend.args=-Dno.netbeans.bootclasspath.prepend.needed=true"
script:
#- ant $TEST_OPTS -f groovy/groovy test
diff --git a/ide/c.jcraft.jsch/build.xml b/ide/c.jcraft.jsch/build.xml
index a7252c9..d06dfb5 100644
--- a/ide/c.jcraft.jsch/build.xml
+++ b/ide/c.jcraft.jsch/build.xml
@@ -33,6 +33,7 @@
<!-- Ensure that the necessary modules/bundles are made available to JSch -->
<attribute name="Require-Bundle" value="com.jcraft.jzlib,bcprov,libs.c.kohlschutter.junixsocket,org.netbeans.libs.jna,org.netbeans.libs.jna.platform"/>
<attribute name="NB-Original-CRC" value="${c.jcraft.jsch.crc32}"/>
+ <attribute name="Multi-Release" value="true"/>
</manifest>
</jar>
</target>
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;
+ }
+
+ }
+}
diff --git a/platform/o.n.bootstrap/src/org/netbeans/JarClassLoader.java b/platform/o.n.bootstrap/src/org/netbeans/JarClassLoader.java
index 33a1399..7a238bc 100644
--- a/platform/o.n.bootstrap/src/org/netbeans/JarClassLoader.java
+++ b/platform/o.n.bootstrap/src/org/netbeans/JarClassLoader.java
@@ -30,6 +30,7 @@
import java.lang.instrument.IllegalClassFormatException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
@@ -53,12 +54,14 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -76,7 +79,29 @@
* @author Petr Nejedly
*/
public class JarClassLoader extends ProxyClassLoader {
+ //
+ // When making changes to this file, check if
+ // platform/netbinox/src/org/netbeans/modules/netbinox/JarBundleFile.java
+ // should also be adjusted. At least the multi-release handling is similar.
+ //
+
private static Stamps cache;
+ 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;
+
+ 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;
+ }
+
static Archive archive = new Archive();
static void initializeCache() {
@@ -272,6 +297,7 @@
}
}
Manifest man = new DelayedManifest();
+
try {
definePackage(pkgName, man, src.getURL());
} catch (IllegalArgumentException x) {
@@ -336,6 +362,7 @@
private ProtectionDomain pd;
protected JarClassLoader jcl;
private static Map<String,Source> sources = new HashMap<String, Source>();
+ private Boolean multiRelease;
public Source(URL url) {
this.url = url;
@@ -412,6 +439,23 @@
return url.toString();
}
+ protected boolean isMultiRelease() {
+ Manifest man = getManifest();
+ if(man == null) {
+ return false;
+ }
+ if(multiRelease != null) {
+ return multiRelease;
+ }
+ if (man.getMainAttributes().containsKey(MULTI_RELEASE)) {
+ String multiReleaseString = (String) man.getMainAttributes().get(MULTI_RELEASE);
+ multiRelease = Boolean.valueOf(multiReleaseString);
+ } else {
+ multiRelease = false;
+ }
+ return multiRelease;
+ }
+
}
static void dumpFiles(File f, int retry) {
@@ -442,6 +486,7 @@
private boolean dead;
private int requests;
private int used;
+ private volatile int[] versions;
private volatile Reference<Manifest> manifest;
/** #141110: expensive to repeatedly look for them */
private final Set<String> nonexistentResources = Collections.synchronizedSet(new HashSet<String>());
@@ -574,13 +619,56 @@
@Override
protected byte[] readClass(String path) throws IOException {
try {
+ if ((! path.startsWith(META_INF)) && isMultiRelease() && RUNTIME_VERSION > BASE_VERSION) {
+ int[] vers = getVersions();
+ for (int version: vers) {
+ byte[] data = archive.getData(this, "META-INF/versions/" + version + "/" + path);
+ if (data != null) {
+ return data;
+ }
+ }
+ }
return archive.getData(this, path);
} catch (ZipException ex) {
dumpFiles(file, -1);
throw ex;
}
}
-
+
+ /**
+ * @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;
+ }
+ try {
+ Set<Integer> vers = new TreeSet<>(Collections.reverseOrder());
+ for(int i = BASE_VERSION; i <= RUNTIME_VERSION; i++) {
+ String directory = "META-INF/versions/" + i;
+ byte[] data = archive.getData(this, directory);
+ if (data != null && data.length == 0) {
+ vers.add(i);
+ }
+ }
+ int[] ret = new int[vers.size()];
+ int i = 0;
+ for (Integer ver : vers) {
+ ret[i++] = ver;
+ }
+ versions = ret;
+ return ret;
+ } catch (IOException ioe) {
+ if (warnedFiles.add(file)) {
+ LOGGER.log(Level.WARNING, "problems with " + file, ioe);
+ dumpFiles(file, -1);
+ }
+ }
+ return new int[0];
+ }
+
@Override
public byte[] resource(String path) throws IOException {
if (nonexistentResources.contains(path)) {
diff --git a/platform/o.n.bootstrap/test/unit/src/org/netbeans/JarClassLoaderTest.java b/platform/o.n.bootstrap/test/unit/src/org/netbeans/JarClassLoaderTest.java
index 2306cba..cc6e36f 100644
--- a/platform/o.n.bootstrap/test/unit/src/org/netbeans/JarClassLoaderTest.java
+++ b/platform/o.n.bootstrap/test/unit/src/org/netbeans/JarClassLoaderTest.java
@@ -18,15 +18,23 @@
*/
package org.netbeans;
+import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.Serializable;
+import java.lang.reflect.Method;
import java.net.JarURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
@@ -37,13 +45,19 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import java.security.Permission;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.Map;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.ToolProvider;
import junit.framework.AssertionFailedError;
import org.netbeans.junit.NbTestCase;
import org.openide.util.Utilities;
import org.openide.util.lookup.Lookups;
import org.openide.util.test.TestFileUtils;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
/** Tests that cover some basic aspects of a Proxy/JarClassLoader.
*
* @author Petr Nejedly
@@ -414,4 +428,86 @@
public @Override void checkPermission(Permission perm, Object ctx) {}
}
+
+ public void testMultiReleaseJar() throws Exception {
+ clearWorkDir();
+
+ // Prepare multi-release jar file
+ 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<>();
+ jarContent.put("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\nMulti-Release: true\n\n".getBytes());
+ 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));
+ File jar = new File(getWorkDir(), "multi-release.jar");
+ try (OutputStream out = new FileOutputStream(jar)) {
+ TestFileUtils.writeZipFile(out, jarContent);
+ }
+
+ // Check multi release class loading
+ JarClassLoader jcl = new JarClassLoader(Arrays.asList(jar), new ProxyClassLoader[0]);
+ Class<?> api = jcl.loadClass("api.API");
+ Method run = api.getDeclaredMethod("run");
+ String output = (String) run.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 = jcl.getResourceAsStream("test/dummy.txt")) {
+ assertEquals(expected, loadUTF8(is));
+ }
+ }
+
+ 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), Kind.SOURCE);
+ this.content = content;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ return content;
+ }
+
+ }
}