Skip to content

Commit 0fa97cd

Browse files
committed
Merge branch 'scripts-from-jars'
This branch cleans up the script detection, adding support for script discovery from classpath resources such as JAR files.
2 parents 16b68af + 225b419 commit 0fa97cd

File tree

7 files changed

+274
-92
lines changed

7 files changed

+274
-92
lines changed

src/main/java/org/scijava/MenuPath.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public MenuPath() {
5656
* the argument will make a copy.
5757
*/
5858
public MenuPath(final Collection<? extends MenuEntry> menuEntries) {
59-
addAll(menuEntries);
59+
if (menuEntries != null) addAll(menuEntries);
6060
}
6161

6262
/**
@@ -66,8 +66,16 @@ public MenuPath(final Collection<? extends MenuEntry> menuEntries) {
6666
* @see #PATH_SEPARATOR
6767
*/
6868
public MenuPath(final String path) {
69+
this(path, PATH_SEPARATOR);
70+
}
71+
72+
/**
73+
* Creates a menu path with entries parsed from the given string, splitting on
74+
* the specified separator.
75+
*/
76+
public MenuPath(final String path, final String separator) {
6977
if (path != null && !path.isEmpty()) {
70-
final String[] tokens = path.split(PATH_SEPARATOR);
78+
final String[] tokens = path.split(separator);
7179
for (final String token : tokens) {
7280
add(new MenuEntry(token.trim()));
7381
}

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

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

3434
import java.io.File;
35-
import java.io.IOException;
3635
import java.io.Reader;
3736
import java.io.StringReader;
3837
import java.math.BigDecimal;
@@ -52,6 +51,7 @@
5251
import org.scijava.InstantiableException;
5352
import org.scijava.MenuPath;
5453
import org.scijava.Priority;
54+
import org.scijava.app.AppService;
5555
import org.scijava.command.CommandService;
5656
import org.scijava.event.EventHandler;
5757
import org.scijava.log.LogService;
@@ -66,13 +66,12 @@
6666
import org.scijava.plugin.PluginService;
6767
import org.scijava.service.Service;
6868
import org.scijava.service.event.ServicesLoadedEvent;
69-
import org.scijava.util.AppUtils;
7069
import org.scijava.util.ClassUtils;
7170
import org.scijava.util.ColorRGB;
7271
import org.scijava.util.ColorRGBA;
7372

7473
/**
75-
* Default service for working with scripting languages.
74+
* Default service for working with scripts.
7675
*
7776
* @author Johannes Schindelin
7877
* @author Curtis Rueden
@@ -91,6 +90,9 @@ public class DefaultScriptService extends
9190
@Parameter
9291
private CommandService commandService;
9392

93+
@Parameter
94+
private AppService appService;
95+
9496
@Parameter
9597
private ParseService parser;
9698

@@ -106,8 +108,8 @@ public class DefaultScriptService extends
106108
/** Menu prefix to use for each script directory, if any. */
107109
private HashMap<File, MenuPath> menuPrefixes;
108110

109-
/** Index of available scripts, by script <em>file</em>. */
110-
private HashMap<File, ScriptInfo> scripts;
111+
/** Index of available scripts, by script path. */
112+
private HashMap<String, ScriptInfo> scripts;
111113

112114
/** Table of short type names to associated {@link Class}. */
113115
private HashMap<String, Class<?>> aliasMap;
@@ -330,7 +332,7 @@ private HashMap<File, MenuPath> menuPrefixes() {
330332
}
331333

332334
/** Gets {@link #scripts}, initializing if needed. */
333-
private HashMap<File, ScriptInfo> scripts() {
335+
private HashMap<String, ScriptInfo> scripts() {
334336
if (scripts == null) initScripts();
335337
return scripts;
336338
}
@@ -362,8 +364,8 @@ private synchronized void initScriptDirs() {
362364
final ArrayList<File> dirs = new ArrayList<>();
363365

364366
// append default script directories
365-
final File baseDir = AppUtils.getBaseDirectory(getClass()); //FIXME
366-
dirs.add(new File(baseDir, "scripts"));
367+
final File baseDir = appService.getApp().getBaseDirectory();
368+
dirs.add(new File(baseDir, SCRIPTS_RESOURCE_DIR));
367369

368370
// append additional script directories from system property
369371
final String scriptsPath = System.getProperty(SCRIPTS_PATH_PROPERTY);
@@ -386,13 +388,13 @@ private synchronized void initMenuPrefixes() {
386388
private synchronized void initScripts() {
387389
if (scripts != null) return; // already initialized
388390

389-
final HashMap<File, ScriptInfo> map = new HashMap<>();
391+
final HashMap<String, ScriptInfo> map = new HashMap<>();
390392

391393
final ArrayList<ScriptInfo> scriptList = new ArrayList<>();
392-
new ScriptFinder(this).findScripts(scriptList);
394+
new ScriptFinder(context()).findScripts(scriptList);
393395

394396
for (final ScriptInfo info : scriptList) {
395-
map.put(asFile(info.getPath()), info);
397+
map.put(info.getPath(), info);
396398
}
397399

398400
scripts = map;
@@ -461,17 +463,6 @@ private ScriptInfo getOrCreate(final File file) {
461463
return new ScriptInfo(getContext(), file);
462464
}
463465

464-
private File asFile(final String path) {
465-
final File file = new File(path);
466-
try {
467-
return file.getCanonicalFile();
468-
}
469-
catch (final IOException exc) {
470-
log.warn(exc);
471-
return file.getAbsoluteFile();
472-
}
473-
}
474-
475466
@SuppressWarnings({ "rawtypes", "unchecked" })
476467
private Future<ScriptModule> cast(final Future<Module> future) {
477468
return (Future) future;

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

Lines changed: 120 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +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;
41-
import org.scijava.MenuEntry;
46+
import org.scijava.Context;
4247
import org.scijava.MenuPath;
4348
import org.scijava.log.LogService;
4449
import org.scijava.plugin.Parameter;
50+
import org.scijava.util.FileUtils;
4551

4652
/**
4753
* Discovers scripts.
@@ -59,14 +65,33 @@ public class ScriptFinder extends AbstractContextual {
5965

6066
private static final String SCRIPT_ICON = "/icons/script_code.png";
6167

62-
private final ScriptService scriptService;
68+
@Parameter
69+
private ScriptService scriptService;
6370

6471
@Parameter
6572
private LogService log;
6673

67-
public ScriptFinder(final ScriptService scriptService) {
68-
this.scriptService = scriptService;
69-
setContext(scriptService.getContext());
74+
private final String pathPrefix;
75+
76+
/**
77+
* Creates a new script finder.
78+
*
79+
* @param context The SciJava application context housing needed services.
80+
*/
81+
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) {
93+
setContext(context);
94+
this.pathPrefix = pathPrefix;
7095
}
7196

7297
// -- ScriptFinder methods --
@@ -79,86 +104,117 @@ public ScriptFinder(final ScriptService scriptService) {
79104
public void findScripts(final List<ScriptInfo> scripts) {
80105
final List<File> directories = scriptService.getScriptDirectories();
81106

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

84-
final HashSet<File> scriptFiles = new HashSet<>();
85-
for (final File directory : directories) {
86-
if (!directory.exists()) {
87-
log.debug("Ignoring non-existent scripts directory: " +
88-
directory.getAbsolutePath());
89-
continue;
90-
}
91-
final MenuPath prefix = scriptService.getMenuPrefix(directory);
92-
final MenuPath menuPath = prefix == null ? new MenuPath() : prefix;
93-
scriptCount +=
94-
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);
95117
}
96118

97119
log.debug("Found " + scriptCount + " scripts");
98120
}
99121

100122
// -- Helper methods --
101123

102-
/**
103-
* Looks through a directory, discovering and adding scripts.
104-
*
105-
* @param scripts The collection to which the discovered scripts are added.
106-
* @param directory The directory in which to look for scripts recursively.
107-
* @param menuPath The menu path, which must not be {@code null}.
108-
*/
109-
private int discoverScripts(final List<ScriptInfo> scripts,
110-
final Set<File> scriptFiles, final File directory, final MenuPath menuPath)
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;
127+
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);
133+
134+
return createInfos(scripts, urls, scriptMap, null);
135+
}
136+
137+
/** Scans a directory for scripts. */
138+
private int scanDirectory(final List<ScriptInfo> scripts, final Set<URL> urls,
139+
final File dir)
111140
{
112-
final File[] fileList = directory.listFiles();
113-
if (fileList == null) return 0; // directory does not exist
114-
Arrays.sort(fileList);
141+
if (!dir.exists()) {
142+
final String path = dir.getAbsolutePath();
143+
log.debug("Ignoring non-existent scripts directory: " + path);
144+
return 0;
145+
}
146+
final MenuPath menuPrefix = scriptService.getMenuPrefix(dir);
115147

116-
int scriptCount = 0;
117-
final boolean isTopLevel = menuPath.size() == 0;
148+
try {
149+
final Set<URL> dirURL = Collections.singleton(dir.toURI().toURL());
150+
final Map<String, URL> scriptMap = //
151+
FileUtils.findResources(null, dirURL);
118152

119-
for (final File file : fileList) {
120-
if (scriptFiles.contains(file)) continue; // script already added
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+
}
159+
}
121160

122-
final String name = file.getName().replace('_', ' ');
123-
if (file.isDirectory()) {
124-
// recurse into subdirectory
125-
discoverScripts(scripts, scriptFiles, file, subMenuPath(menuPath, name));
126-
}
127-
else if (isTopLevel) {
128-
// ignore scripts in toplevel script directories
161+
private int createInfos(final List<ScriptInfo> scripts, final Set<URL> urls,
162+
final Map<String, URL> scriptMap, final MenuPath menuPrefix)
163+
{
164+
int scriptCount = 0;
165+
for (final String path : scriptMap.keySet()) {
166+
if (!scriptService.canHandleFile(path)) {
167+
log.warn("Ignoring unsupported script: " + path);
129168
continue;
130169
}
131-
else if (scriptService.canHandleFile(file)) {
132-
// found a script!
133-
final int dot = name.lastIndexOf('.');
134-
final String noExt = dot <= 0 ? name : name.substring(0, dot);
135-
scripts.add(createEntry(file, subMenuPath(menuPath, noExt)));
136-
scriptFiles.add(file);
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, "/"));
177+
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...
183+
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);
137203
scriptCount++;
138204
}
205+
catch (final IOException exc) {
206+
log.error("Invalid script URL: " + url, exc);
207+
}
139208
}
140-
141209
return scriptCount;
142210
}
143211

144-
private MenuPath
145-
subMenuPath(final MenuPath menuPath, final String subMenuName)
146-
{
147-
final MenuPath result = new MenuPath(menuPath);
148-
result.add(new MenuEntry(subMenuName));
149-
return result;
150-
}
151-
152-
private ScriptInfo
153-
createEntry(final File scriptFile, final MenuPath menuPath)
154-
{
155-
final ScriptInfo info = new ScriptInfo(getContext(), scriptFile);
156-
info.setMenuPath(menuPath);
157-
158-
// flag script with special icon
159-
menuPath.getLeaf().setIconPath(SCRIPT_ICON);
212+
// -- Deprecated methods --
160213

161-
return info;
214+
/** @deprecated Use {@link #ScriptFinder(Context)} instead. */
215+
@Deprecated
216+
public ScriptFinder(final ScriptService scriptService) {
217+
this(scriptService.context());
162218
}
163219

164220
}

0 commit comments

Comments
 (0)