001    /*
002     * $Id: GroovyScriptEngine.java,v 1.6 2005/04/24 05:55:00 spullara Exp $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     * @author sam
059     * 
060     * To change the template for this generated type comment go to Window -
061     * Preferences - Java - Code Generation - Code and Comments
062     */
063    public class GroovyScriptEngine implements ResourceConnector {
064    
065            /**
066             * Simple testing harness for the GSE. Enter script roots as arguments and
067             * then input script names to run them.
068             * 
069             * @param args
070             * @throws Exception
071             */
072            public static void main(String[] args) throws Exception {
073                    URL[] roots = new URL[args.length];
074                    for (int i = 0; i < roots.length; i++) {
075                            roots[i] = new File(args[i]).toURL();
076                    }
077                    GroovyScriptEngine gse = new GroovyScriptEngine(roots);
078                    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
079                    String line;
080                    while (true) {
081                            System.out.print("groovy> ");
082                            if ((line = br.readLine()) == null || line.equals("quit"))
083                                    break;
084                            try {
085                                    System.out.println(gse.run(line, new Binding()));
086                            } catch (Exception e) {
087                                    e.printStackTrace();
088                            }
089                    }
090            }
091    
092            private URL[] roots;
093            private Map scriptCache = Collections.synchronizedMap(new HashMap());
094            private ResourceConnector rc;
095    
096            private static class ScriptCacheEntry {
097                    private Class scriptClass;
098                    private long lastModified;
099                    private Map dependencies = new HashMap();
100            }
101    
102            public URLConnection getResourceConnection(String resourceName) throws ResourceException {
103                    // Get the URLConnection
104                    URLConnection groovyScriptConn = null;
105    
106                    ResourceException se = null;
107                    for (int i = 0; i < roots.length; i++) {
108                            URL scriptURL = null;
109                            try {
110                                    scriptURL = new URL(roots[i], resourceName);
111                                    groovyScriptConn = scriptURL.openConnection();
112                            } catch (MalformedURLException e) {
113                                    String message = "Malformed URL: " + roots[i] + ", " + resourceName;
114                                    if (se == null) {
115                                            se = new ResourceException(message);
116                                    } else {
117                                            se = new ResourceException(message, se);
118                                    }
119                            } catch (IOException e1) {
120                                    String message = "Cannot open URL: " + scriptURL;
121                                    if (se == null) {
122                                            se = new ResourceException(message);
123                                    } else {
124                                            se = new ResourceException(message, se);
125                                    }
126                            }
127    
128                    }
129    
130                    // If we didn't find anything, report on all the exceptions that
131                    // occurred.
132                    if (groovyScriptConn == null) {
133                            throw se;
134                    }
135    
136                    return groovyScriptConn;
137            }
138    
139            /**
140             * The groovy script engine will run groovy scripts and reload them and
141             * their dependencies when they are modified. This is useful for embedding
142             * groovy in other containers like games and application servers.
143         *
144         * @param roots This an array of URLs where Groovy scripts will be stored. They should
145         * be layed out using their package structure like Java classes 
146             */
147            public GroovyScriptEngine(URL[] roots) {
148                    this.roots = roots;
149                    this.rc = this;
150            }
151    
152            public GroovyScriptEngine(String[] args) throws IOException {
153                    roots = new URL[args.length];
154                    for (int i = 0; i < roots.length; i++) {
155                            roots[i] = new File(args[i]).toURL();
156                    }
157                    this.rc = this;
158            }
159    
160            public GroovyScriptEngine(String arg) throws IOException {
161                    roots = new URL[1];
162                    roots[0] = new File(arg).toURL();
163                    this.rc = this;
164            }
165    
166            public GroovyScriptEngine(ResourceConnector rc) {
167                    this.rc = rc;
168            }
169    
170            public String run(String script, String argument) throws ResourceException, ScriptException {
171                    Binding binding = new Binding();
172                    binding.setVariable("arg", argument);
173                    Object result = run(script, binding);
174                    return result == null ? "" : result.toString();
175            }
176    
177            public Object run(String script, Binding binding) throws ResourceException, ScriptException {
178    
179                    ScriptCacheEntry entry;
180    
181                    script = script.intern();
182                    synchronized (script) {
183    
184                            URLConnection groovyScriptConn = rc.getResourceConnection(script);
185    
186                            // URL last modified
187                            long lastModified = groovyScriptConn.getLastModified();
188                            // Check the cache for the script
189                            entry = (ScriptCacheEntry) scriptCache.get(script);
190                            // If the entry isn't null check all the dependencies
191                            boolean dependencyOutOfDate = false;
192                            if (entry != null) {
193                                    for (Iterator i = entry.dependencies.keySet().iterator(); i.hasNext();) {
194                                            URLConnection urlc = null;
195                                            URL url = (URL) i.next();
196                                            try {
197                                                    urlc = url.openConnection();
198                                                    urlc.setDoInput(false);
199                                                    urlc.setDoOutput(false);
200                                                    long dependentLastModified = urlc.getLastModified();
201                                                    if (dependentLastModified > ((Long) entry.dependencies.get(url)).longValue()) {
202                                                            dependencyOutOfDate = true;
203                                                            break;
204                                                    }
205                                            } catch (IOException ioe) {
206                                                    dependencyOutOfDate = true;
207                                                    break;
208                                            }
209                                    }
210                            }
211    
212                            if (entry == null || entry.lastModified < lastModified || dependencyOutOfDate) {
213                                    // Make a new entry
214                                    entry = new ScriptCacheEntry();
215    
216                                    // Closure variable
217                                    final ScriptCacheEntry finalEntry = entry;
218    
219                                    // Compile the script into an object
220                                    GroovyClassLoader groovyLoader = 
221                                            (GroovyClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
222                                                    public Object run() {
223                                                            return new GroovyClassLoader(getClass().getClassLoader()) {
224                                                                    protected Class findClass(String className) throws ClassNotFoundException {
225                                                                            String filename = className.replace('.', File.separatorChar) + ".groovy";
226                                                                            URLConnection dependentScriptConn = null;
227                                                                            try {
228                                                                                    dependentScriptConn = rc.getResourceConnection(filename);
229                                                                                    finalEntry.dependencies.put(
230                                                                                            dependentScriptConn.getURL(),
231                                                                                            new Long(dependentScriptConn.getLastModified()));
232                                                                            } catch (ResourceException e1) {
233                                                                                    throw new ClassNotFoundException("Could not read " + className + ": " + e1);
234                                                                            }
235                                                                            try {
236                                                                                    return parseClass(dependentScriptConn.getInputStream(), filename);
237                                                                            } catch (CompilationFailedException e2) {
238                                                                                    throw new ClassNotFoundException("Syntax error in " + className + ": " + e2);
239                                                                            } catch (IOException e2) {
240                                                                                    throw new ClassNotFoundException("Problem reading " + className + ": " + e2);
241                                                                            }
242                                                                    }
243                                                            };
244                                                    }
245                                            });
246    
247                                    try {
248                                            entry.scriptClass = groovyLoader.parseClass(groovyScriptConn.getInputStream(), script);
249                                    } catch (Exception e) {
250                                            throw new ScriptException("Could not parse script: " + script, e);
251                                    }
252                                    entry.lastModified = lastModified;
253                                    scriptCache.put(script, entry);
254                            }
255                    }
256                    Script scriptObject = InvokerHelper.createScript(entry.scriptClass, binding);
257                    return scriptObject.run();
258            }
259    }