You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/09/13 03:45:33 UTC

(groovy) branch GROOVY-12404 created (now d38a1c289c)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch GROOVY-12404
in repository https://gitbox.apache.org/repos/asf/groovy.git


      at d38a1c289c GROOVY-12404: GroovyScriptEngine resource loader leaks URLConnections and ignores a custom ResourceConnector

This branch includes the following new commits:

     new d38a1c289c GROOVY-12404: GroovyScriptEngine resource loader leaks URLConnections and ignores a custom ResourceConnector

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(groovy) 01/01: GROOVY-12404: GroovyScriptEngine resource loader leaks URLConnections and ignores a custom ResourceConnector

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY-12404
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit d38a1c289cce37d1697972d2ecad446abb40a686
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Sep 13 12:35:22 2026 +0900

    GROOVY-12404: GroovyScriptEngine resource loader leaks URLConnections and ignores a custom ResourceConnector
---
 src/main/java/groovy/util/GroovyScriptEngine.java  | 45 ++++++-----
 .../groovy/util/GroovyScriptEngineTest.groovy      | 90 ++++++++++++++++++++++
 2 files changed, 116 insertions(+), 19 deletions(-)

diff --git a/src/main/java/groovy/util/GroovyScriptEngine.java b/src/main/java/groovy/util/GroovyScriptEngine.java
index 7b1ae85a0f..a1f3787659 100644
--- a/src/main/java/groovy/util/GroovyScriptEngine.java
+++ b/src/main/java/groovy/util/GroovyScriptEngine.java
@@ -145,6 +145,7 @@ public class GroovyScriptEngine implements ResourceConnector {
          */
         public ScriptClassLoader(GroovyClassLoader loader) {
             super(loader);
+            setResLoader();
         }
 
         /**
@@ -161,14 +162,16 @@ public class GroovyScriptEngine implements ResourceConnector {
         private void setResLoader() {
             final GroovyResourceLoader rl = getResourceLoader();
             setResourceLoader(className -> {
-                String filename;
                 for (String extension : getConfig().getScriptExtensions()) {
-                    filename = className.replace('.', File.separatorChar) + "." + extension;
+                    String filename = className.replace('.', '/') + "." + extension;
+                    URLConnection dependentScriptConn = null;
                     try {
-                        URLConnection dependentScriptConn = rc.getResourceConnection(filename);
+                        dependentScriptConn = rc.getResourceConnection(filename);
                         return dependentScriptConn.getURL();
                     } catch (ResourceException e) {
-                        //TODO: maybe do something here?
+                        // Missing this extension; try the next one, then the parent loader.
+                    } finally {
+                        forceClose(dependentScriptConn);
                     }
                 }
                 return rl.loadGroovySource(className);
@@ -189,7 +192,12 @@ public class GroovyScriptEngine implements ResourceConnector {
             for (String depSourcePath : cache.get(".")) {
                 try {
                     cache.get(depSourcePath);
-                    cu.addSource(getResourceConnection(depSourcePath).getURL());
+                    URLConnection conn = rc.getResourceConnection(depSourcePath);
+                    try {
+                        cu.addSource(conn.getURL());
+                    } finally {
+                        forceClose(conn);
+                    }
                 } catch (ResourceException e) {
                     /* ignore */
                 }
@@ -216,24 +224,23 @@ public class GroovyScriptEngine implements ResourceConnector {
                         try {
                             String finalName = name + "." + ext;
                             URLConnection conn = rc.getResourceConnection(finalName);
-                            URL url = conn.getURL();
-                            String path = url.toExternalForm();
-                            ScriptCacheEntry entry = scriptCache.get(path);
-                            Class clazz = null;
-                            if (entry != null) clazz = entry.scriptClass;
-                            if (GroovyScriptEngine.this.isSourceNewer(entry)) {
-                                try {
+                            try {
+                                URL url = conn.getURL();
+                                String path = url.toExternalForm();
+                                ScriptCacheEntry entry = scriptCache.get(path);
+                                Class clazz = null;
+                                if (entry != null) clazz = entry.scriptClass;
+                                if (GroovyScriptEngine.this.isSourceNewer(entry)) {
                                     SourceUnit su = compilationUnit.addSource(url);
                                     return new LookupResult(su, null);
-                                } finally {
-                                    forceClose(conn);
                                 }
-                            } else {
                                 precompiledEntries.put(origName, path);
-                            }
-                            if (clazz != null) {
-                                ClassNode cn = ClassHelper.make(clazz);
-                                return new LookupResult(null, cn);
+                                if (clazz != null) {
+                                    ClassNode cn = ClassHelper.make(clazz);
+                                    return new LookupResult(null, cn);
+                                }
+                            } finally {
+                                forceClose(conn);
                             }
                         } catch (ResourceException re) {
                             // skip
diff --git a/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy b/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy
index aa15d50170..fea754230b 100644
--- a/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy
+++ b/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy
@@ -86,4 +86,94 @@ final class GroovyScriptEngineTest {
         assert counts['Script1'] == 1
         assert counts['Foo'] == 1
     }
+
+    @Test
+    void resourceLoaderClosesConnectorConnection() {
+        File scriptFile = new File(temporaryFolder, 'Foo.groovy')
+        scriptFile.text = 'class Foo {}'
+
+        TrackingURLConnection tracking = null
+        ResourceConnector rc = { String name ->
+            File f = new File(temporaryFolder, name)
+            if (!f.exists()) throw new ResourceException("missing $name")
+            tracking = new TrackingURLConnection(f)
+            tracking
+        }
+
+        GroovyScriptEngine engine = new GroovyScriptEngine(rc)
+        engine.config.scriptExtensions = new LinkedHashSet(['groovy'])
+
+        URL url = engine.groovyClassLoader.resourceLoader.loadGroovySource('Foo')
+        assert url != null
+        assert tracking != null
+        assert tracking.inputStreamOpened : 'URLConnection obtained only for its URL must still be closed'
+    }
+
+    @Test
+    void resourceLoaderUsesCustomConnectorWhenParentIsGroovyClassLoader() {
+        new File(temporaryFolder, 'Helper.groovy').text = 'class Helper { def ping() { "pong" } }'
+
+        ResourceConnector rc = { String name ->
+            File f = new File(temporaryFolder, name)
+            if (!f.exists()) throw new ResourceException("missing $name")
+            f.toURI().toURL().openConnection()
+        }
+
+        GroovyScriptEngine engine = new GroovyScriptEngine(rc, new GroovyClassLoader())
+        Class helper = engine.groovyClassLoader.loadClass('Helper')
+        assert helper.newInstance().ping() == 'pong'
+    }
+
+    @Test
+    void resourceLoaderTriesNextExtensionWhenFirstIsMissing() {
+        new File(temporaryFolder, 'Helper.gy').text = 'class Helper { def ping() { "pong" } }'
+
+        GroovyScriptEngine engine = new GroovyScriptEngine([temporaryFolder.toURI().toURL()] as URL[])
+        engine.config.scriptExtensions = new LinkedHashSet(['groovy', 'gy'])
+
+        Class helper = engine.groovyClassLoader.loadClass('Helper')
+        assert helper.newInstance().ping() == 'pong'
+    }
+
+    @Test
+    void resourceLoaderRequestsForwardSlashPathsForPackagedClasses() {
+        File pkg = new File(temporaryFolder, 'com/example')
+        pkg.mkdirs()
+        new File(pkg, 'Helper.groovy').text = 'package com.example; class Helper { def ping() { "pong" } }'
+
+        def requested = []
+        ResourceConnector rc = { String name ->
+            requested << name
+            File f = new File(temporaryFolder, name)
+            if (!f.exists()) throw new ResourceException("missing $name")
+            f.toURI().toURL().openConnection()
+        }
+
+        GroovyScriptEngine engine = new GroovyScriptEngine(rc)
+        engine.config.scriptExtensions = new LinkedHashSet(['groovy'])
+
+        Class helper = engine.groovyClassLoader.loadClass('com.example.Helper')
+        assert helper.newInstance().ping() == 'pong'
+        assert requested.contains('com/example/Helper.groovy')
+    }
+
+    static final class TrackingURLConnection extends URLConnection {
+        boolean inputStreamOpened
+        private final File file
+
+        TrackingURLConnection(File file) {
+            super(file.toURI().toURL())
+            this.file = file
+        }
+
+        @Override
+        void connect() {
+        }
+
+        @Override
+        InputStream getInputStream() {
+            inputStreamOpened = true
+            file.newInputStream()
+        }
+    }
 }