001/*
002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.webengine.scripting;
021
022import groovy.lang.GroovyRuntimeException;
023
024import java.io.File;
025import java.io.FileReader;
026import java.io.IOException;
027import java.io.Reader;
028import java.util.Map;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.ConcurrentMap;
031
032import javax.script.Compilable;
033import javax.script.CompiledScript;
034import javax.script.ScriptContext;
035import javax.script.ScriptEngine;
036import javax.script.ScriptEngineManager;
037import javax.script.ScriptException;
038import javax.script.SimpleBindings;
039import javax.script.SimpleScriptContext;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.webengine.loader.WebLoader;
044
045/**
046 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
047 */
048public class Scripting {
049
050    private static final Log log = LogFactory.getLog(Scripting.class);
051
052    private final ConcurrentMap<File, Entry> cache = new ConcurrentHashMap<File, Entry>();
053
054    // this will be lazy initialized
055    private ScriptEngineManager scriptMgr;
056
057    private final WebLoader loader;
058
059    public Scripting(WebLoader loader) {
060        this.loader = loader;
061    }
062
063    public static CompiledScript compileScript(ScriptEngine engine, File file) throws ScriptException {
064        if (engine instanceof Compilable) {
065            Compilable comp = (Compilable) engine;
066            try {
067                Reader reader = new FileReader(file);
068                try {
069                    return comp.compile(reader);
070                } finally {
071                    reader.close();
072                }
073            } catch (IOException e) {
074                throw new ScriptException(e);
075            }
076        } else {
077            return null;
078        }
079    }
080
081    /**
082     * Lazy init scripting manager to avoid loading script engines when no scripting is used.
083     * <p>
084     * Javax Scripting is not used by default in WebWengine, we are using directly the Groovy engine. This also fixes an
085     * annoying pb on Mac in java5 due to AppleScripting which is failing to register.
086     *
087     * @return the scriptMgr
088     */
089    public ScriptEngineManager getEngineManager() {
090        if (scriptMgr == null) {
091            scriptMgr = new ScriptEngineManager();
092        }
093        return scriptMgr;
094    }
095
096    public boolean isScript(String ext) {
097        return getEngineManager().getEngineByExtension(ext) != null;
098    }
099
100    public Object runScript(ScriptFile script) throws ScriptException {
101        return runScript(script, null);
102    }
103
104    public Object runScript(ScriptFile script, Map<String, Object> args) throws ScriptException {
105        if (log.isDebugEnabled()) {
106            log.debug("## Running Script: " + script.getFile());
107        }
108        if ("groovy".equals(script.getExtension())) {
109            try {
110                return loader.getGroovyScripting().eval(script.file, args);
111            } catch (GroovyRuntimeException e) {
112                throw new ScriptException(e);
113            }
114        } else {
115            return _runScript(script, args);
116        }
117    }
118
119    // TODO: add an output stream to use as arg?
120    protected Object _runScript(ScriptFile script, Map<String, Object> args) throws ScriptException {
121        SimpleBindings bindings = new SimpleBindings();
122        if (args != null) {
123            bindings.putAll(args);
124        }
125        String ext = script.getExtension();
126        // check for a script engine
127        ScriptEngine engine = getEngineManager().getEngineByExtension(ext);
128        if (engine != null) {
129            ScriptContext ctx = new SimpleScriptContext();
130            ctx.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
131            CompiledScript comp = getCompiledScript(engine, script.getFile()); // use cache for compiled scripts
132            if (comp != null) {
133                return comp.eval(ctx);
134            } // compilation not supported - eval it on the fly
135            try {
136                Reader reader = new FileReader(script.getFile());
137                try { // TODO use __result__ to pass return value for engine that doesn't returns like jython
138                    Object result = engine.eval(reader, ctx);
139                    if (result == null) {
140                        result = bindings.get("__result__");
141                    }
142                    return result;
143                } finally {
144                    reader.close();
145                }
146            } catch (IOException e) {
147                throw new ScriptException(e);
148            }
149        }
150        return null;
151    }
152
153    public CompiledScript getCompiledScript(ScriptEngine engine, File file) throws ScriptException {
154        Entry entry = cache.get(file);
155        long tm = file.lastModified();
156        if (entry != null) {
157            if (entry.lastModified < tm) { // recompile
158                entry.script = compileScript(engine, file);
159                entry.lastModified = tm;
160            }
161            return entry.script;
162        }
163        CompiledScript script = compileScript(engine, file);
164        if (script != null) {
165            cache.putIfAbsent(file, new Entry(script, tm));
166            return script;
167        }
168        return null;
169    }
170
171    class Entry {
172        public CompiledScript script;
173
174        public long lastModified;
175
176        Entry(CompiledScript script, long lastModified) {
177            this.lastModified = lastModified;
178            this.script = script;
179        }
180    }
181
182}