From 4766f1225adc0e90673f2942444e6b3ecf24e979 Mon Sep 17 00:00:00 2001 From: Karl Nelson Date: Wed, 27 Nov 2024 12:37:40 -0800 Subject: [PATCH] Cleanup and work for nonascii friends --- jpype/_core.py | 32 ++-- .../jpype/classloader/DynamicClassLoader.java | 36 +++-- .../jpype/classloader/JPypeClassLoader.java | 149 ------------------ .../classloader/JpypeSystemClassLoader.java | 43 ----- test/jpypetest/test_startup.py | 6 +- 5 files changed, 48 insertions(+), 218 deletions(-) delete mode 100644 native/java/org/jpype/classloader/JPypeClassLoader.java delete mode 100644 native/java/org/jpype/classloader/JpypeSystemClassLoader.java diff --git a/jpype/_core.py b/jpype/_core.py index 23e9088fd..55cf92448 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -168,7 +168,6 @@ def startJVM( *jvmargs: str, jvmpath: typing.Optional[_PathOrStr] = None, classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, - modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, ignoreUnrecognized: bool = False, convertStrings: bool = False, interrupt: bool = not interactive(), @@ -214,6 +213,10 @@ def startJVM( TypeError: if a keyword argument conflicts with the positional arguments. """ + +# Code for 1.6 +# modulepath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None, + if _jpype.isStarted(): raise OSError('JVM is already started') global _JVM_started @@ -286,19 +289,19 @@ def startJVM( elif system_class_loader: # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 raise ValueError("system classloader cannot be specified with non ascii characters in the classpath") - elif support_lib.isascii(): - # ok, setup the jpype system classloader and add to the path after startup - # this guarentees all classes have the same permissions as they did in the past - extra_jvm_args += [ - '-Djava.system.class.loader=org.jpype.classloader.JpypeSystemClassLoader', - '-Djava.class.path=%s'%support_lib - ] else: - # We are screwed no matter what we try or do. - # Unfortunately the jdk maintainers don't seem to care either. - # This bug is almost 10 years old and spans 16 jdk versions and counting. - # https://bugs.openjdk.org/browse/JDK-8079633?jql=text%20~%20%22ParseUtil%22 - raise ValueError("jpype jar must be ascii to add to the system class path") + if not support_lib.isascii(): + import tempfile + import shutil + tmp = tempfile.gettempdir() + if not tmp.isascii(): + raise ValueError("Unable to create ascii temp directory. Clear TEMPDIR, TEMP, and TMP environment variables") + support_lib2 = os.path.join(tmp, "org.jpype.jar") + shutil.copyfile(support_lib, support_lib2) + support_lib = support_lib2 + + extra_jvm_args += ['-Djava.system.class.loader=org.jpype.classloader.DynamicClassLoader' ] + extra_jvm_args += ['-Djava.class.path=%s'%os.path.join(tmp, "org.jpype.jar") ] if agent: extra_jvm_args += ['-javaagent:' + support_lib] @@ -344,8 +347,9 @@ def startJVM( if late_load and classpath: # now we can add to the system classpath cl = _jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() + from pathlib import Path for cp in _expandClassPath(classpath): - cl.addPath(_jpype._java_lang_String(cp)) + cl.addFile(Path(cp)) def initializeResources(): diff --git a/native/java/org/jpype/classloader/DynamicClassLoader.java b/native/java/org/jpype/classloader/DynamicClassLoader.java index 2192391e9..c2e9365c5 100644 --- a/native/java/org/jpype/classloader/DynamicClassLoader.java +++ b/native/java/org/jpype/classloader/DynamicClassLoader.java @@ -14,6 +14,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; @@ -26,7 +27,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; -public class DynamicClassLoader extends ClassLoader +public class DynamicClassLoader extends URLClassLoader { List loaders = new LinkedList<>(); @@ -34,7 +35,20 @@ public class DynamicClassLoader extends ClassLoader public DynamicClassLoader(ClassLoader parent) { - super(parent); + super(new URL[0], parent); + } + + + public void addFile(String path) throws Throwable + { + addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); + } + + // this is required to add a Java agent even if it is already in the path + @SuppressWarnings("unused") + private void appendToClassPathForInstrumentation(String path) throws Throwable + { + addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); } public int getCode() @@ -77,7 +91,7 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) } }); - loaders.add(new URLClassLoader(urls.toArray(new URL[urls.size()]))); + loaders.add(new URLClassLoader(urls.toArray(new URL[0]))); } public void addFile(Path path) throws FileNotFoundException @@ -120,7 +134,7 @@ public Class findClass(String name) throws ClassNotFoundException, ClassFormatEr try { URLConnection connection = url.openConnection(); - try ( InputStream is = connection.getInputStream()) + try (InputStream is = connection.getInputStream()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int bytes; @@ -146,6 +160,11 @@ public URL getResource(String name) URL url = this.getParent().getResource(name); if (url != null) return url; + + url = super.getResource(name); + if (url != null) + return url; + for (ClassLoader cl : this.loaders) { url = cl.getResource(name); @@ -164,12 +183,11 @@ public URL getResource(String name) public Enumeration getResources(String name) throws IOException { ArrayList out = new ArrayList<>(); - Enumeration urls = getParent().getResources(name); - out.addAll(Collections.list(urls)); + out.addAll(Collections.list(getParent().getResources(name))); + out.addAll(Collections.list(super.getResources(name))); for (URLClassLoader cl : this.loaders) { - urls = cl.findResources(name); - out.addAll(Collections.list(urls)); + out.addAll(Collections.list(cl.findResources(name))); } // Both with and without / should generate the same result if (name.endsWith("/")) @@ -201,7 +219,7 @@ public void scanJar(Path p1) return; if (Files.isDirectory(p1)) return; - try ( JarFile jf = new JarFile(p1.toFile())) + try (JarFile jf = new JarFile(p1.toFile())) { Enumeration entries = jf.entries(); URI abs = p1.toAbsolutePath().toUri(); diff --git a/native/java/org/jpype/classloader/JPypeClassLoader.java b/native/java/org/jpype/classloader/JPypeClassLoader.java deleted file mode 100644 index 21e5268bb..000000000 --- a/native/java/org/jpype/classloader/JPypeClassLoader.java +++ /dev/null @@ -1,149 +0,0 @@ -/* **************************************************************************** - Licensed 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. - - See NOTICE file for details. -**************************************************************************** */ -package org.jpype.classloader; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.TreeMap; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; - -/** - * Specialized class loader for JPype resources. - *

- * Loader to convert the internally stored resources into java classes. This - * prevents class load order problems when there are class dependencies. - *

- */ -public class JPypeClassLoader extends ClassLoader -{ - - static private JPypeClassLoader instance; - private TreeMap map = new TreeMap<>(); - - /** - * Get the class loader. - * - * @return the singleton class loader. - */ - public static JPypeClassLoader getInstance() - { - if (instance == null) - { - JPypeClassLoader.instance = new JPypeClassLoader(getSystemClassLoader()); - } - return instance; - } - - private JPypeClassLoader(ClassLoader parent) - { - super(parent); - } - - /** - * Add a class to the class loader. - *

- * This can be called from within python to add a class to the Java JVM. - * - * @param name is the name of the class. - * @param code is the byte code. - */ - public void importClass(String name, byte[] code) - { - map.put(name, code); - } - - /** - * Import a jar from memory into the class loader. - *

- * Does not handle unknown jar entry lengths. - * - * @param bytes - */ - public void importJar(byte[] bytes) - { - try (JarInputStream is = new JarInputStream(new ByteArrayInputStream(bytes))) - { - while (true) - { - JarEntry nextEntry = is.getNextJarEntry(); - if (nextEntry == null) - break; - - // Skip directories and other non-class resources - long size = nextEntry.getSize(); - if (size == 0) - continue; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int q; - while ((q = is.read()) != -1) - baos.write(q); - byte[] data = baos.toByteArray(); - - // Store all classes we find - String name = nextEntry.getName(); - importClass(name, data); - } - } catch (IOException ex) - { - throw new RuntimeException(ex); - } - } - - /** - * Loads a class from the class loader. - * - * @param name is the name of the class with java class notation (using dots). - * @return the class - * @throws ClassNotFoundException was not found by the class loader. - * @throws ClassFormatError if the class byte code was invalid. - */ - @Override - public Class findClass(String name) throws ClassNotFoundException, ClassFormatError - { - String mname = name.replace('.', '/') + ".class"; - byte[] data = map.get(mname); - if (data == null) - { - // Call the default implementation, throws ClassNotFoundException - return super.findClass(name); - } - - Class cls = defineClass(name, data, 0, data.length); - if (cls == null) - throw new ClassFormatError("Class load was null"); - return cls; - } - - /** - * Overload for thunk resources. - * - * @param s - * @return - */ - @Override - public InputStream getResourceAsStream(String s) - { - if (this.map.containsKey(s)) - { - return new ByteArrayInputStream(this.map.get(s)); - } - return super.getResourceAsStream(s); - } -} diff --git a/native/java/org/jpype/classloader/JpypeSystemClassLoader.java b/native/java/org/jpype/classloader/JpypeSystemClassLoader.java deleted file mode 100644 index efa1853ff..000000000 --- a/native/java/org/jpype/classloader/JpypeSystemClassLoader.java +++ /dev/null @@ -1,43 +0,0 @@ -/* **************************************************************************** - Licensed 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. - - See NOTICE file for details. -**************************************************************************** */ -package org.jpype.classloader; - -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Paths; - -public final class JpypeSystemClassLoader extends URLClassLoader { - - public JpypeSystemClassLoader(ClassLoader parent) throws Throwable { - super(new URL[0], parent); - } - - public void addPath(String path) throws Throwable { - addURL(Paths.get(path).toAbsolutePath().toUri().toURL()); - } - - public void addPaths(String[] paths) throws Throwable { - for (String path : paths) { - addPath(path); - } - } - - // this is required to add a Java agent even if it is already in the path - @SuppressWarnings("unused") - private void appendToClassPathForInstrumentation(String path) throws Throwable { - addPath(path); - } -} diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index 8dff5d4e1..0a3eadc00 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -152,7 +152,7 @@ def testNonASCIIPath(self): """ jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/unicode_à😎/sample_package.jar") cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] @@ -162,7 +162,7 @@ def testOldStyleNonASCIIPath(self): """ jpype.startJVM("-Djava.class.path=test/jar/unicode_à😎/sample_package.jar", jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B'] def testNonASCIIPathWithSystemClassLoader(self): @@ -207,7 +207,7 @@ def testDefaultSystemClassLoader(self): # we introduce no behavior change unless absolutely necessary jpype.startJVM(jvmpath=Path(self.jvmpath)) cl = jpype.JClass("java.lang.ClassLoader").getSystemClassLoader() - self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.JpypeSystemClassLoader")) + self.assertNotEqual(type(cl), jpype.JClass("org.jpype.classloader.DynamicClassLoader")) def testServiceWithNonASCIIPath(self): jpype.startJVM(