blob: cc6e36f84dc891881ddd04f9f8c3b3c74f002a30 [file] [log] [blame]
/*
* 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;
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;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
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
*/
public class JarClassLoaderTest extends NbTestCase {
private static Logger LOGGER = Logger.getLogger(ProxyClassLoader.class.getName());
public JarClassLoaderTest(String name) {
super(name);
}
@Override
protected void setUp() throws Exception {
LOGGER.setUseParentHandlers(false);
LOGGER.setLevel(Level.OFF);
clearWorkDir();
}
public void testCanLoadFromDefaultPackage() throws Exception {
File jar = new File(getWorkDir(), "default-package-resource.jar");
TestFileUtils.writeZipFile(jar, "resource.txt:content", "package/resource.txt:content");
JarClassLoader jcl = new JarClassLoader(Collections.singletonList(jar), new ProxyClassLoader[0]);
assertStreamContent(jcl.getResourceAsStream("package/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/package/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/resource.txt"), "content");
assertURLsContent(jcl.getResources("package/resource.txt"), "content");
assertURLsContent(jcl.getResources("/package/resource.txt"), "content");
assertURLsContent(jcl.getResources("resource.txt"), "content");
assertURLsContent(jcl.getResources("/resource.txt"), "content");
}
public void testKnowsWhichJarsHaveDefaultPackage() throws Exception {
final File nothing = new File(getWorkDir(), "nothing.jar");
TestFileUtils.writeZipFile(nothing, "package/resource.txt:content");
final File a1 = new File(getWorkDir(), "a1.jar");
TestFileUtils.writeZipFile(a1, "A.txt:A", "package/resourceA.txt:content");
final File a2 = new File(getWorkDir(), "a2.jar");
TestFileUtils.writeZipFile(a2, "A.txt:A2", "package/resourceA2.txt:content");
final File b = new File(getWorkDir(), "b.jar");
TestFileUtils.writeZipFile(b, "B.txt:B", "package/resourceB.txt:content");
class CntJCL extends JarClassLoader {
int queried;
public CntJCL(List<File> files, ClassLoader[] parents) {
super(files, parents);
}
@Override
public URL findResource(String name) {
queried++;
return super.findResource(name);
}
@Override
public Enumeration<URL> findResources(String name) {
queried++;
return super.findResources(name);
}
}
final CntJCL[] arr = new CntJCL[] {
new CntJCL(Collections.singletonList(nothing), new ClassLoader[0]),
new CntJCL(Collections.singletonList(a1), new ClassLoader[0]),
new CntJCL(Collections.singletonList(a2), new ClassLoader[0]),
new CntJCL(Collections.singletonList(b), new ClassLoader[0]),
};
ProxyClassLoader pcl = new ProxyClassLoader(arr, true);
assertURLsContent(pcl.getResources("A.txt"), "A", "A2");
assertEquals("No query to nothing.jar", 0, arr[0].queried);
assertEquals("One query to a1.jar", 1, arr[1].queried);
assertEquals("One query to a2.jar", 1, arr[2].queried);
assertEquals("No query to b.jar", 0, arr[3].queried);
assertURLsContent(pcl.getResources("B.txt"), "B");
assertEquals("No query to nothing.jar", 0, arr[0].queried);
assertEquals("Still One query to a1.jar", 1, arr[1].queried);
assertEquals("Still One query to a2.jar", 1, arr[2].queried);
assertEquals("One query to b.jar now", 1, arr[3].queried);
}
public void testCanLoadFromDefaultPackageCachedOldFormat() throws Exception {
doCanLoadCached("META-INF,/MANIFEST.MF,package");
}
public void testCanLoadFromDefaultPackageCached() throws Exception {
doCanLoadCached("META-INF,/MANIFEST.MF,package,default/resource.txt");
}
private void doCanLoadCached(String covPkg) throws Exception {
final File jar = new File(getWorkDir(), "default-package-resource-cached.jar");
TestFileUtils.writeZipFile(jar, "resource.txt:content", "package/resource.txt:content",
"META-INF/MANIFEST.MF:OpenIDE-Module: x.y.z\nCovered-Packages: " + covPkg + ",\n"
);
MockModuleInstaller inst = new MockModuleInstaller();
MockEvents ev = new MockEvents();
ModuleManager mm = new ModuleManager(inst, ev);
Module fake = new Module(mm, null, null, null) {
public List<File> getAllJars() {throw new UnsupportedOperationException();}
public void setReloadable(boolean r) { throw new UnsupportedOperationException();}
public void reload() throws IOException { throw new UnsupportedOperationException();}
protected void classLoaderUp(Set<Module> parents) throws IOException {throw new UnsupportedOperationException();}
protected void classLoaderDown() { throw new UnsupportedOperationException();}
protected void cleanup() { throw new UnsupportedOperationException();}
protected void destroy() { throw new UnsupportedOperationException("Not supported yet.");}
public boolean isFixed() { throw new UnsupportedOperationException("Not supported yet.");}
public Object getLocalizedAttribute(String attr) { throw new UnsupportedOperationException("Not supported yet.");}
public Manifest getManifest() {
try {
return new JarFile(jar, false).getManifest();
} catch (IOException ex) {
throw new AssertionFailedError(ex.getMessage());
}
}
};
JarClassLoader jcl = new JarClassLoader(Collections.singletonList(jar), new ProxyClassLoader[0], false, fake);
assertStreamContent(jcl.getResourceAsStream("package/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/package/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/resource.txt"), "content");
assertURLsContent(jcl.getResources("package/resource.txt"), "content");
assertURLsContent(jcl.getResources("/package/resource.txt"), "content");
assertURLsContent(jcl.getResources("resource.txt"), "content");
assertURLsContent(jcl.getResources("/resource.txt"), "content");
}
public void testCanLoadFromDefaultPackageDirs() throws Exception {
File dir = getWorkDir();
TestFileUtils.writeFile(new File(dir, "resource.txt"), "content");
TestFileUtils.writeFile(new File(dir, "package/resource.txt"), "content");
TestFileUtils.writeFile(new File(dir, "META-INF/services/resource.txt"), "content");
JarClassLoader jcl = new JarClassLoader(Collections.singletonList(dir), new ProxyClassLoader[0]);
assertStreamContent(jcl.getResourceAsStream("package/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/package/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("META-INF/services/resource.txt"), "content");
assertStreamContent(jcl.getResourceAsStream("/META-INF/services/resource.txt"), "content");
assertURLsContent(jcl.getResources("package/resource.txt"), "content");
assertURLsContent(jcl.getResources("/package/resource.txt"), "content");
assertURLsContent(jcl.getResources("resource.txt"), "content");
assertURLsContent(jcl.getResources("/resource.txt"), "content");
}
public void testJarURLConnection() throws Exception {
File jar = new File(getWorkDir(), "default-package-resource.jar");
TestFileUtils.writeZipFile(jar, "META-INF/MANIFEST.MF:Manifest-Version: 1.0\nfoo: bar\n\n", "package/re source++.txt:content");
JarClassLoader jcl = new JarClassLoader(Collections.singletonList(jar), new ProxyClassLoader[0]);
URL url = jcl.getResource("package/re source++.txt");
assertTrue(url.toString(), url.toString().endsWith("default-package-resource.jar!/package/re%20source++.txt"));
URLConnection conn = url.openConnection();
assertEquals(7, conn.getContentLength());
assertTrue(conn instanceof JarURLConnection);
JarURLConnection jconn = (JarURLConnection) conn;
assertEquals("package/re source++.txt", jconn.getEntryName());
assertEquals(Utilities.toURI(jar).toURL(), jconn.getJarFileURL());
assertEquals("bar", jconn.getMainAttributes().getValue("foo"));
assertEquals(jar.getAbsolutePath(), jconn.getJarFile().getName());
}
public void testAddURLMethod() throws Exception {
File jar = new File(getWorkDir(), "default-package-resource.jar");
TestFileUtils.writeZipFile(jar, "META-INF/MANIFEST.MF:Manifest-Version: 1.0\nfoo: bar\n\n", "package/re source++.txt:content");
JarClassLoader jcl = new JarClassLoader(Collections.<File>emptyList(), new ProxyClassLoader[0]);
jcl.addURL(Utilities.toURI(jar).toURL());
URL url = jcl.getResource("package/re source++.txt");
assertTrue(url.toString(), url.toString().endsWith("default-package-resource.jar!/package/re%20source++.txt"));
URLConnection conn = url.openConnection();
assertEquals(7, conn.getContentLength());
assertTrue(conn instanceof JarURLConnection);
JarURLConnection jconn = (JarURLConnection) conn;
assertEquals("package/re source++.txt", jconn.getEntryName());
assertEquals(Utilities.toURI(jar).toURL(), jconn.getJarFileURL());
assertEquals("bar", jconn.getMainAttributes().getValue("foo"));
assertEquals(jar.getAbsolutePath(), jconn.getJarFile().getName());
}
public void testResourceDefinition() throws Exception { // #196595
File jar = new File(getWorkDir(), "some.jar");
TestFileUtils.writeZipFile(jar, "package/resource.txt:content");
ClassLoader cl = new JarClassLoader(Collections.singletonList(jar), new ProxyClassLoader[0]);
URL r = cl.getResource("package/resource.txt");
assertNotNull(r);
assertStreamContent(r.openStream(), "content");
assertEquals(cl, r.getContent(new Class<?>[] {ClassLoader.class}));
}
public void testMetaInfServicesUsesGetContentCL() throws Exception {
final ClassLoader parent = MetaInfServicesToken.class.getClassLoader().getParent();
class JDKOnly extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return parent.loadClass(name);
}
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return parent.loadClass(name);
}
@Override
public URL getResource(String name) {
return parent.getResource(name);
}
@Override
public InputStream getResourceAsStream(String name) {
return parent.getResourceAsStream(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return parent.getResources(name);
}
}
ClassLoader jdkonly = new JDKOnly();
File jar = new File(getWorkDir(), "some.jar");
TestFileUtils.writeZipFile(
jar,
"META-INF/services/java.io.Serializable:org.netbeans.MetaInfServicesToken"
);
URL url = MetaInfServicesToken.class.getProtectionDomain().getCodeSource().getLocation();
URLClassLoader one = new URLClassLoader(new URL[] { url }, jdkonly);
URLClassLoader two = new URLClassLoader(new URL[] { url }, jdkonly);
final String name = MetaInfServicesToken.class.getName();
Class<?> cOne = one.loadClass(name);
Class<?> cTwo = two.loadClass(name);
if (cOne == cTwo) {
fail("Classes should be different, not loaded by: " + cOne.getClassLoader());
}
ClassLoader cl = new JarClassLoader(Collections.singletonList(jar), new ClassLoader[] { two });
ProxyClassLoader all = new ProxyClassLoader(new ClassLoader[] { one, cl }, false);
Object res = Lookups.metaInfServices(all).lookup(Serializable.class);
assertNotNull("One serializable found", res);
assertEquals("It is from the second classloader", cTwo, res.getClass());
}
private void assertURLsContent(Enumeration<URL> urls, String ... contents) throws IOException {
for (String content : contents) {
assertTrue("Enough entries", urls.hasMoreElements());
assertStreamContent(urls.nextElement().openStream(), content);
}
assertFalse("Too many entries", urls.hasMoreElements());
}
private void assertStreamContent(InputStream str, String content) throws IOException {
assertNotNull("Resource found", str);
byte[] data = new byte[content.length()];
DataInputStream dis = new DataInputStream(str);
try {
dis.readFully(data);
} finally {
dis.close();
}
assertEquals(new String(data), content);
}
public void interruptImpl(int toInterrupt) throws Exception {
File jar = new File(getWorkDir(), "interrupted-reading.jar");
TestFileUtils.writeZipFile(jar, "resource.txt:content");
final JarClassLoader jcl = new JarClassLoader(Collections.singletonList(jar), new ProxyClassLoader[0]);
jcl.releaseJars();
// now we have a JarClassLoader with no jars open, let't catch
// him opening a jar
final Semaphore controlSemaphore = new Semaphore(0);
final Object[] results = new Object[2];
Semaphore readSemaphore = new Semaphore(0);
BlockingSecurityManager.initialize(jar.toString(), readSemaphore);
class Tester extends Thread {
int slot;
Tester(int slot) throws Exception {
this.slot = slot;
start();
controlSemaphore.acquire();
}
public void run() {
controlSemaphore.release(); // we're about to start blocking
try {
URL url = jcl.getResource("resource.txt");
assertNotNull(url);
results[slot] = url;
} catch (Throwable t) {
results[slot] = t;
}
}
};
Thread[] threads = new Thread[] { new Tester(0), new Tester(1) };
// threads[0] has reached the blocking point while opening the jar
// threads[1] is blocking in callGet()
Thread.sleep(100); // for sure
threads[toInterrupt].interrupt(); // interrupt the selected thread
readSemaphore.release(1000); // let the read proceed
// wait for them to finish the work
for (Thread t : threads) t.join();
assertTrue("Should be URL: " + results[0], results[0] instanceof URL);
assertTrue("Should be URL: " + results[1], results[1] instanceof URL);
}
public void testCanInterruptOpeningThread() throws Exception {
interruptImpl(0); // try interrupting the opening thread
}
public void testCanInterruptWaitingThread() throws Exception {
interruptImpl(1); // try interrupting the waiting thread
}
private static class BlockingSecurityManager extends SecurityManager {
private static String path;
private static Semaphore sync;
public static void initialize(String path, Semaphore sync) {
BlockingSecurityManager.path = path;
BlockingSecurityManager.sync = sync;
if (System.getSecurityManager() instanceof BlockingSecurityManager) {
// ok
} else {
System.setSecurityManager(new BlockingSecurityManager());
}
}
public @Override void checkRead(String file) {
if (file.equals(path)) {
sync.acquireUninterruptibly();
}
}
public @Override void checkRead(String file, Object context) {
checkRead(file);
}
public @Override void checkPermission(Permission perm) {}
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;
}
}
}