001    /*
002     * $Id: GroovyScriptEngine.java 3669 2006-02-26 22:11:48Z glaforge $version Jan 9, 2004 12:19:58 PM $user Exp $
003     * 
004     * Copyright 2003 (C) Sam Pullara. All Rights Reserved.
005     * 
006     * Redistribution and use of this software and associated documentation
007     * ("Software"), with or without modification, are permitted provided that the
008     * following conditions are met: 1. Redistributions of source code must retain
009     * copyright statements and notices. Redistributions must also contain a copy
010     * of this document. 2. Redistributions in binary form must reproduce the above
011     * copyright notice, this list of conditions and the following disclaimer in
012     * the documentation and/or other materials provided with the distribution. 3.
013     * The name "groovy" must not be used to endorse or promote products derived
014     * from this Software without prior written permission of The Codehaus. For
015     * written permission, please contact info@codehaus.org. 4. Products derived
016     * from this Software may not be called "groovy" nor may "groovy" appear in
017     * their names without prior written permission of The Codehaus. "groovy" is a
018     * registered trademark of The Codehaus. 5. Due credit should be given to The
019     * Codehaus - http://groovy.codehaus.org/
020     * 
021     * THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS ``AS IS'' AND ANY
022     * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
023     * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
024     * DISCLAIMED. IN NO EVENT SHALL THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR
025     * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
026     * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
027     * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
028     * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
029     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
030     * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
031     * DAMAGE.
032     *  
033     */
034    package groovy.util;
035    
036    import groovy.lang.Binding;
037    import groovy.lang.GroovyClassLoader;
038    import groovy.lang.Script;
039    
040    import java.io.BufferedReader;
041    import java.io.File;
042    import java.io.IOException;
043    import java.io.InputStreamReader;
044    import java.net.MalformedURLException;
045    import java.net.URL;
046    import java.net.URLConnection;
047    import java.security.AccessController;
048    import java.security.PrivilegedAction;
049    import java.util.Collections;
050    import java.util.HashMap;
051    import java.util.Iterator;
052    import java.util.Map;
053    
054    import org.codehaus.groovy.control.CompilationFailedException;
055    import org.codehaus.groovy.runtime.InvokerHelper;
056    
057    /**
058     * Specific script engine able to reload modified scripts as well as dealing properly with dependent scripts.
059     *
060     * @author sam
061     * @author Marc Palmer
062     * @author Guillaume Laforge
063     */
064    public class GroovyScriptEngine implements ResourceConnector {
065    
066        /**
067         * Simple testing harness for the GSE. Enter script roots as arguments and
068         * then input script names to run them.
069         *
070         * @param urls
071         * @throws Exception
072         */
073        public static void main(String[] urls) throws Exception {
074            URL[] roots = new URL[urls.length];
075            for (int i = 0; i < roots.length; i++) {
076                roots[i] = new File(urls[i]).toURL();
077            }
078            GroovyScriptEngine gse = new GroovyScriptEngine(roots);
079            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
080            String line;
081            while (true) {
082                System.out.print("groovy> ");
083                if ((line = br.readLine()) == null || line.equals("quit"))
084                    break;
085                try {
086                    System.out.println(gse.run(line, new Binding()));
087                } catch (Exception e) {
088                    e.printStackTrace();
089                }
090            }
091        }
092    
093        private URL[] roots;
094        private Map scriptCache = Collections.synchronizedMap(new HashMap());
095        private ResourceConnector rc;
096        private ClassLoader parentClassLoader = getClass().getClassLoader();
097    
098        private static class ScriptCacheEntry {
099            private Class scriptClass;
100            private long lastModified;
101            private Map dependencies = new HashMap();
102        }
103    
104        /**
105         * Get a resource connection as a <code>URLConnection</code> to retrieve a script
106         * from the <code>ResourceConnector</code>
107         *
108         * @param resourceName name of the resource to be retrieved
109         * @return a URLConnection to the resource
110         * @throws ResourceException
111         */
112        public URLConnection getResourceConnection(String resourceName) throws ResourceException {
113            // Get the URLConnection
114            URLConnection groovyScriptConn = null;
115    
116            ResourceException se = null;
117            for (int i = 0; i < roots.length; i++) {
118                URL scriptURL = null;
119                try {
120                    scriptURL = new URL(roots[i], resourceName);
121    
122                    groovyScriptConn = scriptURL.openConnection();
123    
124                    // Make sure we can open it, if we can't it doesn't exist.
125                    // Could be very slow if there are any non-file:// URLs in there
126                    groovyScriptConn.getInputStream();
127    
128                    break; // Now this is a bit unusual
129    
130                } catch (MalformedURLException e) {
131                    String message = "Malformed URL: " + roots[i] + ", " + resourceName;
132                    if (se == null) {
133                        se = new ResourceException(message);
134                    } else {
135                        se = new ResourceException(message, se);
136                    }
137                } catch (IOException e1) {
138                    String message = "Cannot open URL: " + scriptURL;
139                    if (se == null) {
140                        se = new ResourceException(message);
141                    } else {
142                        se = new ResourceException(message, se);
143                    }
144                }
145            }
146    
147            // If we didn't find anything, report on all the exceptions that occurred.
148            if (groovyScriptConn == null) {
149                throw se;
150            }
151    
152            return groovyScriptConn;
153        }
154    
155        /**
156         * The groovy script engine will run groovy scripts and reload them and
157         * their dependencies when they are modified. This is useful for embedding
158         * groovy in other containers like games and application servers.
159         *
160         * @param roots This an array of URLs where Groovy scripts will be stored. They should
161         * be layed out using their package structure like Java classes 
162         */
163        public GroovyScriptEngine(URL[] roots) {
164            this.roots = roots;
165            this.rc = this;
166        }
167    
168        public GroovyScriptEngine(URL[] roots, ClassLoader parentClassLoader) {
169            this(roots);
170            this.parentClassLoader = parentClassLoader;
171        }
172    
173        public GroovyScriptEngine(String[] urls) throws IOException {
174            roots = new URL[urls.length];
175            for (int i = 0; i < roots.length; i++) {
176                roots[i] = new File(urls[i]).toURL();
177            }
178            this.rc = this;
179        }
180    
181        public GroovyScriptEngine(String[] urls, ClassLoader parentClassLoader) throws IOException {
182            this(urls);
183            this.parentClassLoader = parentClassLoader;
184        }
185    
186        public GroovyScriptEngine(String url) throws IOException {
187            roots = new URL[1];
188            roots[0] = new File(url).toURL();
189            this.rc = this;
190        }
191    
192        public GroovyScriptEngine(String url, ClassLoader parentClassLoader) throws IOException {
193            this(url);
194            this.parentClassLoader = parentClassLoader;
195        }
196    
197        public GroovyScriptEngine(ResourceConnector rc) {
198            this.rc = rc;
199        }
200    
201        public GroovyScriptEngine(ResourceConnector rc, ClassLoader parentClassLoader) {
202            this(rc);
203            this.parentClassLoader = parentClassLoader;
204        }
205    
206        /**
207         * Get the <code>ClassLoader</code> that will serve as the parent ClassLoader of the
208         * {@link GroovyClassLoader} in which scripts will be executed. By default, this is the
209         * ClassLoader that loaded the <code>GroovyScriptEngine</code> class.
210         *
211         * @return parent classloader used to load scripts
212         */
213        public ClassLoader getParentClassLoader() {
214            return parentClassLoader;
215        }
216    
217        /**
218         * @param parentClassLoader ClassLoader to be used as the parent ClassLoader for scripts executed by the engine
219         */
220        public void setParentClassLoader(ClassLoader parentClassLoader) {
221            if (parentClassLoader == null) {
222                throw new IllegalArgumentException("The parent class loader must not be null.");
223            }
224            this.parentClassLoader = parentClassLoader;
225        }
226    
227        /**
228         * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
229         *
230         * @param scriptName
231         * @return the loaded scriptName as a compiled class
232         * @throws ResourceException
233         * @throws ScriptException
234         */
235        public Class loadScriptByName(String scriptName) throws ResourceException, ScriptException {
236            return loadScriptByName( scriptName, getClass().getClassLoader());
237        }
238    
239    
240        /**
241         * Get the class of the scriptName in question, so that you can instantiate Groovy objects with caching and reloading.
242         *
243         * @param scriptName
244         * @return the loaded scriptName as a compiled class
245         * @throws ResourceException
246         * @throws ScriptException
247         */
248        public Class loadScriptByName(String scriptName, ClassLoader parentClassLoader)
249                throws ResourceException, ScriptException {
250            scriptName = scriptName.replace('.', File.separatorChar) + ".groovy";
251            ScriptCacheEntry entry = updateCacheEntry(scriptName, parentClassLoader);
252            return entry.scriptClass;
253        }
254    
255        /**
256         * Locate the class and reload it or any of its dependencies
257         *
258         * @param scriptName
259         * @param parentClassLoader
260         * @return the scriptName cache entry
261         * @throws ResourceException
262         * @throws ScriptException
263         */
264        private ScriptCacheEntry updateCacheEntry(String scriptName, final ClassLoader parentClassLoader)
265                throws ResourceException, ScriptException
266        {
267            ScriptCacheEntry entry;
268    
269            scriptName = scriptName.intern();
270            synchronized (scriptName) {
271    
272                URLConnection groovyScriptConn = rc.getResourceConnection(scriptName);
273    
274                // URL last modified
275                long lastModified = groovyScriptConn.getLastModified();
276                // Check the cache for the scriptName
277                entry = (ScriptCacheEntry) scriptCache.get(scriptName);
278                // If the entry isn't null check all the dependencies
279    
280                boolean dependencyOutOfDate = false;
281                if (entry != null) {
282    
283                    for (Iterator i = entry.dependencies.keySet().iterator(); i.hasNext();) {
284                        URLConnection urlc = null;
285                        URL url = (URL) i.next();
286                        try {
287                            urlc = url.openConnection();
288                            urlc.setDoInput(false);
289                            urlc.setDoOutput(false);
290                            long dependentLastModified = urlc.getLastModified();
291                            if (dependentLastModified > ((Long) entry.dependencies.get(url)).longValue()) {
292                                dependencyOutOfDate = true;
293                                break;
294                            }
295                        } catch (IOException ioe) {
296                            dependencyOutOfDate = true;
297                            break;
298                        }
299                    }
300                }
301    
302                if (entry == null || entry.lastModified < lastModified || dependencyOutOfDate) {
303                    // Make a new entry
304                    entry = new ScriptCacheEntry();
305    
306                    // Closure variable
307                    final ScriptCacheEntry finalEntry = entry;
308    
309                    // Compile the scriptName into an object
310                    GroovyClassLoader groovyLoader =
311                            (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
312                                public Object run() {
313                                    return new GroovyClassLoader(parentClassLoader) {
314                                        protected Class findClass(String className) throws ClassNotFoundException {
315                                            String filename = className.replace('.', File.separatorChar) + ".groovy";
316                                            URLConnection dependentScriptConn = null;
317                                            try {
318                                                dependentScriptConn = rc.getResourceConnection(filename);
319                                                finalEntry.dependencies.put(
320                                                        dependentScriptConn.getURL(),
321                                                        new Long(dependentScriptConn.getLastModified()));
322                                            } catch (ResourceException e1) {
323                                                throw new ClassNotFoundException("Could not read " + className + ": " + e1);
324                                            }
325                                            try {
326                                                return parseClass(dependentScriptConn.getInputStream(), filename);
327                                            } catch (CompilationFailedException e2) {
328                                                throw new ClassNotFoundException("Syntax error in " + className + ": " + e2);
329                                            } catch (IOException e2) {
330                                                throw new ClassNotFoundException("Problem reading " + className + ": " + e2);
331                                            }
332                                        }
333                                    };
334                                }
335                            });
336    
337                    try {
338                        entry.scriptClass = groovyLoader.parseClass(groovyScriptConn.getInputStream(), scriptName);
339                    } catch (Exception e) {
340                        throw new ScriptException("Could not parse scriptName: " + scriptName, e);
341                    }
342                    entry.lastModified = lastModified;
343                    scriptCache.put(scriptName, entry);
344                }
345            }
346            return entry;
347        }
348    
349        /**
350         * Run a script identified by name.
351         *
352         * @param scriptName name of the script to run
353         * @param argument a single argument passed as a variable named <code>arg</code> in the binding
354         * @return a <code>toString()</code> representation of the result of the execution of the script
355         * @throws ResourceException
356         * @throws ScriptException
357         */
358        public String run(String scriptName, String argument) throws ResourceException, ScriptException {
359            Binding binding = new Binding();
360            binding.setVariable("arg", argument);
361            Object result = run(scriptName, binding);
362            return result == null ? "" : result.toString();
363        }
364    
365        /**
366         * Run a script identified by name.
367         *
368         * @param scriptName name of the script to run
369         * @param binding binding to pass to the script
370         * @return an object
371         * @throws ResourceException
372         * @throws ScriptException
373         */
374        public Object run(String scriptName, Binding binding) throws ResourceException, ScriptException {
375    
376            ScriptCacheEntry entry = updateCacheEntry(scriptName, getParentClassLoader());
377            Script scriptObject = InvokerHelper.createScript(entry.scriptClass, binding);
378            return scriptObject.run();
379        }
380    }