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 ;
4146import org .scijava .Context ;
42- import org .scijava .MenuEntry ;
4347import org .scijava .MenuPath ;
4448import org .scijava .log .LogService ;
4549import 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