Skip to content

Commit ff73f88

Browse files
committed
ScriptFinder: add support for scripts inside JARs
This is a near-total rewrite of the script detection logic to use FileUtils.findResources instead of recursive directory listings. This change offers (at least) two major advantages: 1. Support for non-file resources, particularly scripts within JARs. 2. Built-in recursive resource scanning, instead of doing it ourselves. The new logic splits the script detection into two steps: 1) scan classpath resources beneath a given path prefix (default "scripts"); and 2) scan directories given by ScriptService#getScriptDirectories().
1 parent d691f00 commit ff73f88

File tree

1 file changed

+104
-62
lines changed

1 file changed

+104
-62
lines changed

src/main/java/org/scijava/script/ScriptFinder.java

Lines changed: 104 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,22 @@
3232
package org.scijava.script;
3333

3434
import java.io.File;
35-
import java.util.Arrays;
35+
import java.io.IOException;
36+
import java.io.InputStreamReader;
37+
import java.net.MalformedURLException;
38+
import java.net.URL;
39+
import java.util.Collections;
3640
import java.util.HashSet;
3741
import java.util.List;
42+
import java.util.Map;
3843
import java.util.Set;
3944

4045
import org.scijava.AbstractContextual;
4146
import org.scijava.Context;
42-
import org.scijava.MenuEntry;
4347
import org.scijava.MenuPath;
4448
import org.scijava.log.LogService;
4549
import org.scijava.plugin.Parameter;
50+
import org.scijava.util.FileUtils;
4651

4752
/**
4853
* Discovers scripts.
@@ -66,13 +71,27 @@ public class ScriptFinder extends AbstractContextual {
6671
@Parameter
6772
private LogService log;
6873

74+
private final String pathPrefix;
75+
6976
/**
7077
* Creates a new script finder.
7178
*
7279
* @param context The SciJava application context housing needed services.
7380
*/
7481
public ScriptFinder(final Context context) {
82+
this(context, ScriptService.SCRIPTS_RESOURCE_DIR);
83+
}
84+
85+
/**
86+
* Creates a new script finder.
87+
*
88+
* @param context The SciJava application context housing needed services.
89+
* @param pathPrefix the path prefix beneath which to scan classpath
90+
* resources, or null to skip classpath scanning.
91+
*/
92+
public ScriptFinder(final Context context, final String pathPrefix) {
7593
setContext(context);
94+
this.pathPrefix = pathPrefix;
7695
}
7796

7897
// -- ScriptFinder methods --
@@ -85,86 +104,109 @@ public ScriptFinder(final Context context) {
85104
public void findScripts(final List<ScriptInfo> scripts) {
86105
final List<File> directories = scriptService.getScriptDirectories();
87106

107+
final Set<URL> urls = new HashSet<>();
88108
int scriptCount = 0;
89109

90-
final HashSet<File> scriptFiles = new HashSet<>();
91-
for (final File directory : directories) {
92-
if (!directory.exists()) {
93-
log.debug("Ignoring non-existent scripts directory: " +
94-
directory.getAbsolutePath());
95-
continue;
96-
}
97-
final MenuPath prefix = scriptService.getMenuPrefix(directory);
98-
final MenuPath menuPath = prefix == null ? new MenuPath() : prefix;
99-
scriptCount +=
100-
discoverScripts(scripts, scriptFiles, directory, menuPath);
110+
scriptCount += scanResources(scripts, urls);
111+
112+
// NB: We use a separate call to findResources for each directory so that
113+
// we can distinguish which URLs came from each directory, because each
114+
// directory may have a different menu prefix.
115+
for (final File dir : directories) {
116+
scriptCount += scanDirectory(scripts, urls, dir);
101117
}
102118

103119
log.debug("Found " + scriptCount + " scripts");
104120
}
105121

106122
// -- Helper methods --
107123

108-
/**
109-
* Looks through a directory, discovering and adding scripts.
110-
*
111-
* @param scripts The collection to which the discovered scripts are added.
112-
* @param directory The directory in which to look for scripts recursively.
113-
* @param menuPath The menu path, which must not be {@code null}.
114-
*/
115-
private int discoverScripts(final List<ScriptInfo> scripts,
116-
final Set<File> scriptFiles, final File directory, final MenuPath menuPath)
117-
{
118-
final File[] fileList = directory.listFiles();
119-
if (fileList == null) return 0; // directory does not exist
120-
Arrays.sort(fileList);
124+
/** Scans classpath resources for scripts (e.g., inside JAR files). */
125+
private int scanResources(final List<ScriptInfo> scripts, final Set<URL> urls) {
126+
if (pathPrefix == null) return 0;
121127

122-
int scriptCount = 0;
123-
final boolean isTopLevel = menuPath.size() == 0;
128+
// NB: We leave the baseDirectory argument null, because scripts on disk
129+
// will be picked up in the subsequent logic, which handles multiple
130+
// script directories rather than being limited to a single one.
131+
final Map<String, URL> scriptMap = //
132+
FileUtils.findResources(null, pathPrefix, null);
124133

125-
for (final File file : fileList) {
126-
if (scriptFiles.contains(file)) continue; // script already added
134+
return createInfos(scripts, urls, scriptMap, null);
135+
}
127136

128-
final String name = file.getName().replace('_', ' ');
129-
if (file.isDirectory()) {
130-
// recurse into subdirectory
131-
discoverScripts(scripts, scriptFiles, file, subMenuPath(menuPath, name));
132-
}
133-
else if (isTopLevel) {
134-
// ignore scripts in toplevel script directories
135-
continue;
136-
}
137-
else if (scriptService.canHandleFile(file)) {
138-
// found a script!
139-
final int dot = name.lastIndexOf('.');
140-
final String noExt = dot <= 0 ? name : name.substring(0, dot);
141-
scripts.add(createEntry(file, subMenuPath(menuPath, noExt)));
142-
scriptFiles.add(file);
143-
scriptCount++;
144-
}
137+
/** Scans a directory for scripts. */
138+
private int scanDirectory(final List<ScriptInfo> scripts, final Set<URL> urls,
139+
final File dir)
140+
{
141+
if (!dir.exists()) {
142+
final String path = dir.getAbsolutePath();
143+
log.debug("Ignoring non-existent scripts directory: " + path);
144+
return 0;
145145
}
146+
final MenuPath menuPrefix = scriptService.getMenuPrefix(dir);
146147

147-
return scriptCount;
148-
}
148+
try {
149+
final Set<URL> dirURL = Collections.singleton(dir.toURI().toURL());
150+
final Map<String, URL> scriptMap = //
151+
FileUtils.findResources(null, dirURL);
149152

150-
private MenuPath
151-
subMenuPath(final MenuPath menuPath, final String subMenuName)
152-
{
153-
final MenuPath result = new MenuPath(menuPath);
154-
result.add(new MenuEntry(subMenuName));
155-
return result;
153+
return createInfos(scripts, urls, scriptMap, menuPrefix);
154+
}
155+
catch (final MalformedURLException exc) {
156+
log.error("Invalid script directory: " + dir, exc);
157+
return 0;
158+
}
156159
}
157160

158-
private ScriptInfo
159-
createEntry(final File scriptFile, final MenuPath menuPath)
161+
private int createInfos(final List<ScriptInfo> scripts, final Set<URL> urls,
162+
final Map<String, URL> scriptMap, final MenuPath menuPrefix)
160163
{
161-
final ScriptInfo info = new ScriptInfo(getContext(), scriptFile);
162-
info.setMenuPath(menuPath);
164+
int scriptCount = 0;
165+
for (final String path : scriptMap.keySet()) {
166+
if (!scriptService.canHandleFile(path)) {
167+
log.warn("Ignoring unsupported script: " + path);
168+
continue;
169+
}
170+
171+
final int dot = path.lastIndexOf('.');
172+
final String basePath = dot <= 0 ? path : path.substring(0, dot);
173+
final String friendlyPath = basePath.replace('_', ' ');
174+
175+
final MenuPath menuPath = new MenuPath(menuPrefix);
176+
menuPath.addAll(new MenuPath(friendlyPath, "/"));
163177

164-
// flag script with special icon
165-
menuPath.getLeaf().setIconPath(SCRIPT_ICON);
178+
// E.g.:
179+
// path = "File/Import/Movie_File....groovy"
180+
// basePath = "File/Import/Movie_File..."
181+
// friendlyPath = "File/Import/Movie File..."
182+
// menuPath = File > Import > Movie File...
166183

167-
return info;
184+
// NB: Ignore base-level scripts (not nested in any menu).
185+
if (menuPath.size() == 1) continue;
186+
187+
final URL url = scriptMap.get(path);
188+
189+
// NB: Skip scripts whose URLs have already been added.
190+
if (urls.contains(url)) continue;
191+
urls.add(url);
192+
193+
try {
194+
final ScriptInfo info = new ScriptInfo(getContext(), //
195+
path, new InputStreamReader(url.openStream()));
196+
197+
info.setMenuPath(menuPath);
198+
199+
// flag script with special icon
200+
menuPath.getLeaf().setIconPath(SCRIPT_ICON);
201+
202+
scripts.add(info);
203+
scriptCount++;
204+
}
205+
catch (final IOException exc) {
206+
log.error("Invalid script URL: " + url, exc);
207+
}
208+
}
209+
return scriptCount;
168210
}
169211

170212
// -- Deprecated methods --

0 commit comments

Comments
 (0)