001/*
002 * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. 
003 * Use is subject to license terms.
004 *
005 * Redistribution and use in source and binary forms, with or without modification, are 
006 * permitted provided that the following conditions are met: Redistributions of source code 
007 * must retain the above copyright notice, this list of conditions and the following disclaimer.
008 * Redistributions in binary form must reproduce the above copyright notice, this list of 
009 * conditions and the following disclaimer in the documentation and/or other materials 
010 * provided with the distribution. Neither the name of the Sun Microsystems nor the names of 
011 * is contributors may be used to endorse or promote products derived from this software 
012 * without specific prior written permission. 
013
014 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
015 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 
016 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 
017 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
018 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
019 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
020 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
021 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
022 * POSSIBILITY OF SUCH DAMAGE.
023 */
024
025/*
026 * JavaScriptEngine.java
027 * @author A. Sundararajan
028 */
029
030package org.kepler.scriptengine.java;
031
032import java.io.IOException;
033import java.io.Reader;
034import java.lang.reflect.Method;
035import java.lang.reflect.Modifier;
036import java.util.Iterator;
037import java.util.Map;
038
039import javax.script.AbstractScriptEngine;
040import javax.script.Bindings;
041import javax.script.Compilable;
042import javax.script.CompiledScript;
043import javax.script.Invocable;
044import javax.script.ScriptContext;
045import javax.script.ScriptEngine;
046import javax.script.ScriptEngineFactory;
047import javax.script.ScriptException;
048import javax.script.SimpleBindings;
049
050/**
051 * This is script engine for Java programming language.
052 */
053public class JavaScriptEngine extends AbstractScriptEngine 
054        implements Compilable, Invocable {
055
056    private Class<?> _clazz;
057    
058    // Java compiler
059    private JavaCompiler compiler;
060
061    public JavaScriptEngine() {
062        compiler = new JavaCompiler();
063    }
064
065    // my factory, may be null
066    private ScriptEngineFactory factory;          
067
068    // my implementation for CompiledScript
069    private class JavaCompiledScript extends CompiledScript {
070        private Class<?> clazz;
071
072        JavaCompiledScript (Class<?> clazz) {
073            this.clazz = clazz;
074        }
075
076        @Override
077                public ScriptEngine getEngine() {
078            return JavaScriptEngine.this;
079        }
080
081        @Override
082                public Object eval(ScriptContext ctx) throws ScriptException {
083            return evalClass(clazz, ctx);
084        }
085    }
086
087    @Override
088        public CompiledScript compile(String script) throws ScriptException {
089        Class<?> clazz = parse(script, context); 
090        return new JavaCompiledScript(clazz);
091    }
092
093    @Override
094        public CompiledScript compile(Reader reader) throws ScriptException {        
095        return compile(readFully(reader));
096    }
097
098    @Override
099        public Object eval(String str, ScriptContext ctx) 
100                       throws ScriptException { 
101        _clazz = parse(str, ctx);
102        return _clazz;
103        //return evalClass(clazz, ctx);
104    }
105
106    @Override
107        public Object eval(Reader reader, ScriptContext ctx)
108                       throws ScriptException {
109        return eval(readFully(reader), ctx);
110    }
111
112    @Override
113        public ScriptEngineFactory getFactory() {
114        synchronized (this) {
115            if (factory == null) {
116                factory = new JavaScriptEngineFactory();
117            }
118        }
119        return factory;
120    }
121
122    @Override
123        public Bindings createBindings() {
124        return new SimpleBindings();
125    }
126
127    void setFactory(ScriptEngineFactory factory) {
128        this.factory = factory;
129    }
130
131    // Internals only below this point
132
133    private Class<?> parse(String str, ScriptContext ctx) throws ScriptException {        
134        String fileName = getFileName(ctx);
135        String sourcePath = getSourcePath(ctx);
136        String classPath = getClassPath(ctx);
137
138        Map<String, byte[]> classBytes = compiler.compile(fileName, str,
139                            ctx.getErrorWriter(), sourcePath, classPath);
140
141        if (classBytes == null) {
142            throw new ScriptException("compilation failed");
143        }
144
145        // create a ClassLoader to load classes from MemoryJavaFileManager
146        try(MemoryClassLoader loader = new MemoryClassLoader(classBytes, classPath,
147                                            getParentLoader(ctx))) {
148
149            String mainClassName = getMainClassName(ctx);
150            if (mainClassName != null) {
151                try {
152                    Class<?> clazz = loader.load(mainClassName);
153                    Method mainMethod = findMainMethod(clazz);
154                    if (mainMethod == null) {
155                        throw new ScriptException("no main method in " + mainClassName);
156                    }
157                    return clazz;
158                } catch (ClassNotFoundException cnfe) {
159                    throw new ScriptException(cnfe);
160                }
161            }
162    
163            // no main class configured - load all compiled classes
164            Iterable<Class<?>> classes;
165            try {
166                classes = loader.loadAll();
167            } catch (ClassNotFoundException exp) {
168                throw new ScriptException(exp);
169            }
170                    
171            // search for class with main method
172            Class<?> c = findMainClass(classes);
173            if (c != null) {
174                return c;
175            } else {
176                // if class with "main" method, then
177                // return first class
178                Iterator<Class<?>> itr = classes.iterator();
179                if (itr.hasNext()) {
180                    return itr.next();
181                } else {
182                    return null;
183                }
184            } 
185        
186        } catch (IOException e) {
187            throw new ScriptException("Error closing class loader: " + e.getMessage());
188        }
189    }
190    
191    private static Class<?> findMainClass(Iterable<Class<?>> classes) {
192        // find a public class with public static main method
193        for (Class<?> clazz : classes) {
194            int modifiers = clazz.getModifiers();
195            if (Modifier.isPublic(modifiers)) {
196                Method mainMethod = findMainMethod(clazz);
197                if (mainMethod != null) {
198                    return clazz;
199                }                
200            }
201        }
202
203        // okay, try to find package private class that
204        // has public static main method
205        for (Class<?> clazz : classes) {
206            Method mainMethod = findMainMethod(clazz);
207            if (mainMethod != null) {
208                return clazz;
209            }
210        }
211
212        // no main class found!
213        return null;
214    }
215
216    // find public static void main(String[]) method, if any
217    private static Method findMainMethod(Class<?> clazz) {
218        try {
219            Method mainMethod = clazz.getMethod("main", new Class[] { String[].class });
220            int modifiers = mainMethod.getModifiers();
221            if (Modifier.isPublic(modifiers) && 
222                Modifier.isStatic(modifiers)) {
223                return mainMethod;
224            }
225        } catch (NoSuchMethodException nsme) {
226        }
227        return null;
228    }
229
230    // find public static void setScriptContext(ScriptContext) method, if any
231    private static Method findSetScriptContextMethod(Class<?> clazz) {
232        try {
233            Method setCtxMethod = clazz.getMethod("setScriptContext", 
234                          new Class<?>[] { ScriptContext.class });
235            int modifiers = setCtxMethod.getModifiers();
236            if (Modifier.isPublic(modifiers) && 
237                Modifier.isStatic(modifiers)) {
238                return setCtxMethod;
239            }
240        } catch (NoSuchMethodException nsme) {
241        }
242        return null;
243    }
244
245    private static String getFileName(ScriptContext ctx) {
246        int scope = ctx.getAttributesScope(ScriptEngine.FILENAME);
247        if (scope != -1) {
248            return ctx.getAttribute(ScriptEngine.FILENAME, scope).toString();
249        } else {
250            return "$unnamed.java";
251        }
252    }
253
254
255    // for certain variables, we look for System properties. This is
256    // the prefix used for such System properties
257    private static final String SYSPROP_PREFIX = "com.sun.script.java.";
258
259    private static final String[] EMPTY_STRING_ARRAY = new String[0];
260    private static final String ARGUMENTS = "arguments";
261    private static String[] getArguments(ScriptContext ctx) {
262        int scope = ctx.getAttributesScope(ARGUMENTS);
263        if (scope != -1) {
264            Object obj =  ctx.getAttribute(ARGUMENTS, scope);
265            if (obj instanceof String[]) {
266                return (String[])obj;
267            }
268        }
269        // return zero length array
270        return EMPTY_STRING_ARRAY;
271    }
272
273    private static final String SOURCEPATH = "sourcepath";
274    private static String getSourcePath(ScriptContext ctx) {
275        int scope = ctx.getAttributesScope(SOURCEPATH);
276        if (scope != -1) {
277            return ctx.getAttribute(SOURCEPATH).toString();
278        } else {
279            // look for "com.sun.script.java.sourcepath"
280            return System.getProperty(SYSPROP_PREFIX + SOURCEPATH);
281        }        
282    }
283
284    private static final String CLASSPATH = "classpath";
285    private static String getClassPath(ScriptContext ctx) {
286        int scope = ctx.getAttributesScope(CLASSPATH);
287        if (scope != -1) {
288            return ctx.getAttribute(CLASSPATH).toString();
289        } else {
290            // look for "com.sun.script.java.classpath"
291            String res = System.getProperty(SYSPROP_PREFIX + CLASSPATH);
292            if (res == null) {
293                res = System.getProperty("java.class.path");
294            }
295            return res;
296        }
297    }
298
299    private static final String MAINCLASS = "mainClass";
300    private static String getMainClassName(ScriptContext ctx) {
301        int scope = ctx.getAttributesScope(MAINCLASS);
302        if (scope != -1) {
303            return ctx.getAttribute(MAINCLASS).toString();
304        } else {
305            // look for "com.sun.script.java.mainClass"
306            return System.getProperty(SYSPROP_PREFIX + MAINCLASS);
307        }        
308    }
309
310    public static final String PARENTLOADER = "parentLoader";
311    private static ClassLoader getParentLoader(ScriptContext ctx) {
312        int scope = ctx.getAttributesScope(PARENTLOADER);
313        if (scope != -1) {
314            Object loader = ctx.getAttribute(PARENTLOADER);
315            if (loader instanceof ClassLoader) {
316                return (ClassLoader) loader;
317            } // else fall through..
318        }
319        return null;       
320    }
321
322    private static Object evalClass(Class<?> clazz, ScriptContext ctx) 
323                            throws ScriptException {
324        // JSR-223 requirement
325        ctx.setAttribute("context", ctx, ScriptContext.ENGINE_SCOPE);
326        if (clazz == null) {
327            return null;
328        }
329        try {            
330            boolean isPublicClazz = Modifier.isPublic(clazz.getModifiers());
331
332            // find the setScriptContext method
333            Method setCtxMethod = findSetScriptContextMethod(clazz);
334            // call setScriptContext and pass current ctx variable
335            if (setCtxMethod != null) {
336                if (! isPublicClazz) {
337                    // try to relax access
338                    setCtxMethod.setAccessible(true);
339                }
340                setCtxMethod.invoke(null, new Object[] { ctx });
341            }
342
343            // find the main method
344            Method mainMethod = findMainMethod(clazz);
345            if (mainMethod != null) {
346                if (! isPublicClazz) {
347                    // try to relax access
348                    mainMethod.setAccessible(true);
349                }        
350
351                // get "command line" args for the main method
352                String[] args = getArguments(ctx);
353
354                // call main method
355                mainMethod.invoke(null, new Object[] { args });
356            }
357
358            // return main class as eval's result
359            return clazz;
360        } catch (Exception exp) {
361            throw new ScriptException(exp);
362        }
363    }
364
365    // read a Reader fully and return the content as string
366    private String readFully(Reader reader) throws ScriptException { 
367        char[] arr = new char[8*1024]; // 8K at a time
368        StringBuilder buf = new StringBuilder();
369        int numChars;
370        try {
371            while ((numChars = reader.read(arr, 0, arr.length)) > 0) {
372                buf.append(arr, 0, numChars);
373            }
374        } catch (IOException exp) {
375            throw new ScriptException(exp);
376        }
377        return buf.toString();
378    }
379
380        @Override
381        public <T> T getInterface(Class<T> arg0) {
382                // TODO Auto-generated method stub
383                return null;
384        }
385
386        @Override
387        public <T> T getInterface(Object arg0, Class<T> arg1) {
388                // TODO Auto-generated method stub
389                return null;
390        }
391
392        @Override
393        public Object invokeFunction(String arg0, Object... arg1)
394                        throws ScriptException, NoSuchMethodException {
395                // TODO Auto-generated method stub
396                return null;
397        }
398
399        @Override
400        public Object invokeMethod(Object thiz, String name, Object... args)
401                        throws ScriptException, NoSuchMethodException {
402                
403                Class<?>[] classes = new Class<?>[args.length];         
404                Method method = thiz.getClass().getMethod(name, classes);
405                try {
406                        return method.invoke(thiz, args);
407                } catch (Exception e) {
408                        throw new ScriptException(e);
409                }
410        }     
411}