3232package org .scijava .script ;
3333
3434import 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 ;
3640import java .util .HashSet ;
3741import java .util .List ;
42+ import java .util .Map ;
3843import java .util .Set ;
3944
4045import org .scijava .AbstractContextual ;
41- import org .scijava .MenuEntry ;
46+ import org .scijava .Context ;
4247import org .scijava .MenuPath ;
4348import org .scijava .log .LogService ;
4449import 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