/* * Copyright (c) 2009 Armando Blancas. All rights reserved. * * The use and distribution terms for this software are covered by the * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) * which can be found in the file epl-v10.html at the root of this distribution. * * By using this software in any fashion, you are agreeing to be bound by * the terms of this license. * * You must not remove this notice, or any other, from this software. */ package clojure.contrib.jsr223; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.util.Map; import javax.script.AbstractScriptEngine; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.Invocable; import javax.script.ScriptContext; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; import javax.script.SimpleBindings; import clojure.lang.Compiler; import clojure.lang.IMapEntry; import clojure.lang.ISeq; import clojure.lang.LineNumberingPushbackReader; import clojure.lang.Namespace; import clojure.lang.RT; import clojure.lang.Symbol; import clojure.lang.Var; /** * Implementation of a {@code ScriptEngine} for Clojure. * * @author Armando Blancas * @version 1.2 */ class ClojureScriptEngine extends AbstractScriptEngine implements Invocable, Compilable { private static final Symbol USER_SYM = Symbol.create("user"); private static final Var IN_NS = RT.var("clojure.core", "in-ns"); private static final String SOURCE_PATH_KEY = "clojure.source.path"; private static final String COMPILE_PATH_KEY = "clojure.compile.path"; private static final String WARN_REFLECTION_KEY = "clojure.compile.warn-on-reflection"; private static final String CLASSPATH = System.getProperty("java.class.path"); private final ScriptEngineFactory factory; /** * Default Constructor. * * @param sef The Script Engine Factory that created this instance. */ ClojureScriptEngine(ScriptEngineFactory sef) { if (sef == null) throw new NullPointerException("factory is null"); factory = sef; Bindings engineScope = getBindings(ScriptContext.ENGINE_SCOPE); engineScope.put(ENGINE, "Clojure Scripting Engine"); engineScope.put(ENGINE_VERSION, "1.2"); engineScope.put(NAME, "Clojure"); engineScope.put(LANGUAGE, "Clojure"); engineScope.put(LANGUAGE_VERSION, "1.2"); // Defaults used for compiling Clojure sources. engineScope.put(SOURCE_PATH_KEY, null); engineScope.put(COMPILE_PATH_KEY, "classes"); engineScope.put(WARN_REFLECTION_KEY, Boolean.valueOf(false)); } /* * Bindings are interned according to the format namespace/var, * or user/var if only the var is given. */ private void applyBindings(Bindings bindings) { for (Map.Entry<String, Object> entry : bindings.entrySet()) { String key = entry.getKey(); if (key.indexOf('.') == -1) { String nsName = "user"; if (key.indexOf('/') >= 0) { String[] names = key.split("/"); nsName = names[0]; key = names[1]; } Object value = entry.getValue(); Var.intern(Namespace.findOrCreate(Symbol.create(nsName.intern())), Symbol.create(key.intern()), value); } } } /* * Bindings are collected in the format namespace/var. */ private void collectBindings(Bindings bindings) { for (ISeq seq = Namespace.all(); seq != null; seq = seq.next()) { Namespace ns = (Namespace) seq.first(); String nsName = ns.toString(); if (nsName.startsWith("clojure")) continue; for (ISeq mseq = ns.getMappings().seq(); mseq != null; mseq = mseq.next()) { IMapEntry e = (IMapEntry) mseq.first(); String k = e.getKey().toString(); Object val = e.getValue(); if (val.toString().startsWith("#'clojure")) continue; if (val instanceof Var) { val = ((Var) val).deref(); bindings.put(nsName + "/" + k, val); } } } } /** * {@inheritDoc} * <p> * Returns an instance of SimpleBindings. */ public Bindings createBindings() { return new SimpleBindings(); } /** * {@inheritDoc} * <p> * The Clojure runtime will keep its state between {@code eval()} calls. * Previous to running the script: * <p> * 1- All Engine and Global bindings are applied to the {@code user} namespace. * <p> * 2- The Clojure standard streams {@code*in*}, {@code *out*} and {@code *err*} are * redirected to their corresponding values in the passed {@code context}. The defaults * are {@code System.in}, {@code System.out} and {@code System.err}, respectively. * <p> * 3- The Clojure runtime is set to the {@code user} namespace in order to provide * consistency with the REPL. * <p> * For consistency with the REPL, redirect {@code *err* } to a {@code PrintWriter}. */ public Object eval(String script, ScriptContext context) throws ScriptException { if (script == null) throw new NullPointerException("script is null"); return eval(new StringReader(script), context); } /** * {@inheritDoc} * <p> * The Clojure runtime will keep its state between {@code eval()} calls. * Previous to running the script: * <p> * 1- All Engine and Global bindings are applied to the {@code user} namespace. * <p> * 2- The Clojure standard streams {@code*in*}, {@code *out*} and {@code *err*} are * redirected to their corresponding values in the passed {@code context}. The defaults * are {@code System.in}, {@code System.out} and {@code System.err}, respectively. * <p> * 3- The Clojure runtime is set to the {@code user} namespace in order to provide * consistency with the REPL. * <p> * For consistency with the REPL, redirect {@code *err* } to a {@code PrintWriter}. */ public Object eval(Reader reader, ScriptContext context) throws ScriptException { if (reader == null) throw new NullPointerException("reader is null"); if (context == null) throw new NullPointerException("context is null"); Object result = null; try { Bindings globalScope = context.getBindings(ScriptContext.GLOBAL_SCOPE); if (globalScope != null) applyBindings(globalScope); Bindings engineScope = context.getBindings(ScriptContext.ENGINE_SCOPE); if (engineScope != null) applyBindings(engineScope); Var.pushThreadBindings( RT.map(RT.CURRENT_NS, RT.CURRENT_NS.deref(), RT.IN, new LineNumberingPushbackReader(context.getReader()), RT.OUT, context.getWriter(), RT.ERR, context.getErrorWriter())); IN_NS.invoke(USER_SYM); result = Compiler.load(reader); if (globalScope != null) collectBindings(engineScope); } catch (Exception e) { throw new ScriptException(e); } finally { Var.popThreadBindings(); } return result; } /** * {@inheritDoc} * <p> * The returned factory is an instance of {@code ClojureScriptEngineFactory}. */ public ScriptEngineFactory getFactory() { return factory; } /****************************************************************** * * * Implementation of interface Invocable. * * * ******************************************************************/ /** * {@inheritDoc} * <p> * To implement a Java interface use the {@code proxy} macro. This method * expects a var in the {@code user} namespace named after the interface * with the suffix "Impl". For example, to implement an {@code ActionListener} * the Clojure code could be: * <pre> * (import java.awt.event.ActionListener) * * (def ActionListenerImpl * (proxy [ActionListener] [] * (actionPerformed [evt] (println "button pushed")))) * </pre> * Then get that implementation by calling: * <p> * {@code engine.getInterface(EventListener.class)} * <p> */ @SuppressWarnings("unchecked") public <T> T getInterface(Class<T> clasz) { if (clasz == null) throw new NullPointerException("clasz is null"); String ns = "user"; Var var = RT.var(ns, clasz.getSimpleName()+"Impl"); return (var == null) ? null : (T) var.deref(); } /** * {@inheritDoc} * <p> * To implement a Java interface use the {@code proxy} macro. This method * looks for a var in the namespace named indicated by {@code thiz} and * named after the interface with the suffix "Impl". For example, to * implement an {@code ActionListener} in the {@code actions} namespace * the Clojure code could be: * <pre> * (ns actions * (:import java.awt.event.ActionListener)) * * (def ActionListenerImpl * (proxy [ActionListener] [] * (actionPerformed [evt] (println "button pushed")))) * </pre> * Then get that implementation by calling: * <p> * {@code engine.getInterface("actions", EventListener.class)} * <p> */ @SuppressWarnings("unchecked") public <T> T getInterface(Object thiz, Class<T> clasz) { if (thiz == null) throw new NullPointerException("thiz is null"); if (clasz == null) throw new NullPointerException("clasz is null"); if (!(thiz instanceof String)) throw new IllegalArgumentException("thiz is not a string"); String ns = (String) thiz; Var var = RT.var(ns, clasz.getSimpleName()+"Impl"); return (var == null) ? null : (T) var.deref(); } /** * {@inheritDoc} * <p> * If the function name is not qualified with a namespace, this method * looks for it in the {@code user} namespace, thus {@code foo} is * equivalent to {@code user/foo}. Functions in other namespaces must * used their fully-qualified names. * <p> * As in the {@code eval()} calls, bindings and redirections are applied * prior to invoking the function. */ public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { if (name == null) throw new NullPointerException("name is null"); Object result = null; String format = "Function %s not found in namespace %s"; try { Bindings globalScope = context.getBindings(ScriptContext.GLOBAL_SCOPE); if (globalScope != null) applyBindings(globalScope); Bindings engineScope = context.getBindings(ScriptContext.ENGINE_SCOPE); if (engineScope != null) applyBindings(engineScope); Var.pushThreadBindings( RT.map(RT.CURRENT_NS, RT.CURRENT_NS.deref(), RT.IN, new LineNumberingPushbackReader(context.getReader()), RT.OUT, context.getWriter(), RT.ERR, context.getErrorWriter())); if (name.indexOf('/') == -1) { String ns = "user"; Var var = RT.var(ns, name); if (var == null) { String msg = String.format(format, name, ns); throw new NoSuchMethodException(msg); } result = var.applyTo(RT.seq(args)); } else { String[] names = name.split("/"); Var var = RT.var(names[0], names[1]); if (var == null) { String msg = String.format(format, names[1], names[0]); throw new NoSuchMethodException(msg); } result = var.applyTo(RT.seq(args)); } if (globalScope != null) collectBindings(engineScope); } catch (Exception e) { throw new ScriptException(e); } finally { Var.popThreadBindings(); } return result; } /** * {@inheritDoc} * <p> * This method works just like {@code invoke(String name, Object... args)}. * The parameter {@code thiz} is ignored. */ public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { return invokeFunction(name, args); } /****************************************************************** * * * Implementation of interface Compilable. * * * ******************************************************************/ /** * {@inheritDoc} * <p> * Clojure code always runs compiled. This method will compile a Clojure * library to .class files for AOT compilation. The argument is expected * to be the namespace defined by the library. The filename and location * should be as expected by the Clojure runtime. * <p> * This engine will recognize and pass on these properties to the Clojure * compiler: * <p> * {@code clojure.source.path} Additional locations of Clojure source files, * to be appended to the value of "java.class.path". This is an optional * property and defaults to {@code null}. * <p> * {@code clojure.compile.path} The location for the generated .class files. * Defaults to {@code "classes"}. * <p> * {@code clojure.compile.warn-on-reflection} Whether to get a warning when * Clojure will use Java reflection. Defaults to {@code Boolean false}. */ public CompiledScript compile(String script) throws ScriptException { if (script == null) throw new NullPointerException("script is null"); String library = script.trim(); if (library.length() == 0) return null; try { /* * Each compilation takes place in its own process with a clean * slate in the Clojure RT. No bindings nor redirections are * applied from the host Java code. */ StringBuffer classpath = new StringBuffer(CLASSPATH); String cmp = (String) get(COMPILE_PATH_KEY); if (cmp != null && cmp.length() > 0) classpath.append(File.pathSeparatorChar).append(cmp); String src = (String) get(SOURCE_PATH_KEY); if (src != null && src.length() > 0) classpath.append(File.pathSeparatorChar).append(src); String compile = String.format("java -D%s=%s -D%s=%b -cp %s clojure.lang.Compile %s", COMPILE_PATH_KEY, (String) get(COMPILE_PATH_KEY), WARN_REFLECTION_KEY, (Boolean) get(WARN_REFLECTION_KEY), classpath.toString(), library); Process process = Runtime.getRuntime().exec(compile); BufferedReader reader = new BufferedReader( new InputStreamReader(process.getErrorStream())); StringBuffer buffer = new StringBuffer(); String line = reader.readLine(); while (line != null) { buffer.append(line).append('\n'); line = reader.readLine(); } reader.close(); process.waitFor(); if (process.exitValue() != 0) throw new ScriptException(buffer.toString()); } catch (IOException e) { throw new ScriptException(e); } catch (InterruptedException e) { throw new ScriptException(e); } return null; } /** * {@inheritDoc} * <p> * <b>NOTE:</b> * No Clojure code is expected from the {@code Reader}. * <p> * This method expects to read library names in separate lines * from the passed reader. It will compile each library in turn. * The actual Clojure code to compile should be in source files * with the name and locations as expected by the Clojure compiler. */ public CompiledScript compile(Reader script) throws ScriptException { if (script == null) throw new NullPointerException("script is null"); BufferedReader bf = new BufferedReader(script); try { String library = bf.readLine(); while (library != null) { compile(library); library = bf.readLine(); } } catch (IOException e) { throw new ScriptException(e); } finally { try { bf.close(); } catch (IOException e) { throw new ScriptException(e); } } return null; } }