/*
* Freeplane - mind map editor
* Copyright (C) 2012 Dimitry
*
* This file author is Dimitry
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.freeplane.plugin.script;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import org.apache.commons.io.FilenameUtils;
import org.freeplane.core.resources.ResourceController;
import org.freeplane.core.util.FileUtils;
import org.freeplane.features.map.NodeModel;
import org.freeplane.features.mode.Controller;
import org.freeplane.main.application.FreeplaneSecurityManager;
import org.freeplane.plugin.script.proxy.ProxyFactory;
/**
* Implements scripting via JSR233 implementation for all other languages except Groovy.
*/
public class GenericScript implements IScript {
public static final class ScriptSource {
private final File file;
private final String script;
private String cachedFileContent;
public ScriptSource(String script) {
this.script = script;
this.file = null;
}
public ScriptSource(File file) {
this.script = null;
this.file = file;
this.cachedFileContent = slurpFile(file);
}
public boolean isFile() {
return file != null;
}
public String rereadFile() {
cachedFileContent = slurpFile(file);
return cachedFileContent;
}
public String getScript() {
return isFile() ? cachedFileContent : script;
}
}
final private ScriptSource scriptSource;
private final ScriptingPermissions specificPermissions;
private CompiledScript compiledScript;
private Throwable errorsInScript;
private IFreeplaneScriptErrorHandler errorHandler;
private PrintStream outStream;
private ScriptContext scriptContext;
private final static Object scriptEngineManagerMutex = new Object();
private static ScriptEngineManager scriptEngineManager;
private static URLClassLoader classLoader;
private final ScriptEngine engine;
private boolean compilationEnabled = true;
private CompileTimeStrategy compileTimeStrategy;
private GenericScript(ScriptSource scriptSource, ScriptEngine engine, ScriptingPermissions permissions) {
this.scriptSource = scriptSource;
this.specificPermissions = permissions;
this.engine = engine;
compiledScript = null;
errorsInScript = null;
errorHandler = ScriptResources.IGNORING_SCRIPT_ERROR_HANDLER;
outStream = System.out;
scriptContext = null;
}
public GenericScript(String script, ScriptEngine engine, ScriptingPermissions permissions) {
this(new ScriptSource(script), engine, permissions);
compileTimeStrategy = new CompileTimeStrategy(null);
}
public GenericScript(String script, String scriptEngineName, ScriptingPermissions permissions) {
this(script, findScriptEngine(scriptEngineName), permissions);
}
public GenericScript(File scriptFile, ScriptingPermissions permissions) {
this(new ScriptSource(scriptFile), findScriptEngine(scriptFile), permissions);
engine.put(ScriptEngine.FILENAME, scriptFile.toString());
compilationEnabled = !disableScriptCompilation(scriptFile);
compileTimeStrategy = new CompileTimeStrategy(scriptFile);
}
private static String slurpFile(File scriptFile) {
try {
return FileUtils.slurpFile(scriptFile);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public IScript setErrorHandler(IFreeplaneScriptErrorHandler pErrorHandler) {
this.errorHandler = pErrorHandler;
return this;
}
@Override
public IScript setOutStream(PrintStream outStream) {
this.outStream = outStream;
return this;
}
@Override
public IScript setScriptContext(ScriptContext scriptContext) {
this.scriptContext = scriptContext;
return this;
}
@Override
public Object getScript() {
return scriptSource;
}
@Override
public Object execute(final NodeModel node) {
try {
if (errorsInScript != null && compileTimeStrategy.canUseOldCompiledScript()) {
throw new ExecuteScriptException(errorsInScript.getMessage(), errorsInScript);
}
final ScriptingSecurityManager scriptingSecurityManager = createScriptingSecurityManager();
final ScriptingPermissions originalScriptingPermissions = new ScriptingPermissions(ResourceController
.getResourceController().getProperties());
final FreeplaneSecurityManager securityManager = (FreeplaneSecurityManager) System.getSecurityManager();
final boolean needToSetFinalSecurityManager = securityManager.needToSetFinalSecurityManager();
final PrintStream oldOut = System.out;
try {
final SimpleScriptContext context = createScriptContext(node);
if (compilationEnabled && engine instanceof Compilable) {
compileAndCache((Compilable) engine);
if (needToSetFinalSecurityManager)
securityManager.setFinalSecurityManager(scriptingSecurityManager);
System.setOut(outStream);
return compiledScript.eval(context);
}
else {
if (needToSetFinalSecurityManager)
securityManager.setFinalSecurityManager(scriptingSecurityManager);
System.setOut(outStream);
return engine.eval(scriptSource.getScript(), context);
}
}
finally {
System.setOut(oldOut);
if (needToSetFinalSecurityManager && securityManager.hasFinalSecurityManager())
securityManager.removeFinalSecurityManager(scriptingSecurityManager);
/* restore preferences (and assure that the values are unchanged!). */
originalScriptingPermissions.restorePermissions();
}
}
catch (final ScriptException e) {
handleScriptRuntimeException(e);
// :fixme: This throw is only reached, if handleScriptRuntimeException
// does not raise an exception. Should it be here at all?
// And if: Shouldn't it raise an ExecuteScriptException?
throw new RuntimeException(e);
}
catch (final Throwable e) {
if (Controller.getCurrentController().getSelection() != null)
Controller.getCurrentModeController().getMapController().select(node);
throw new ExecuteScriptException(e.getMessage(), e);
}
}
private ScriptingSecurityManager createScriptingSecurityManager() {
return new ScriptSecurity(scriptSource, specificPermissions, outStream).getScriptingSecurityManager();
}
private boolean disableScriptCompilation(File scriptFile) {
return FilenameUtils.isExtension(scriptFile.getName(), ScriptResources.SCRIPT_COMPILATION_DISABLED_EXTENSIONS);
}
private SimpleScriptContext createScriptContext(final NodeModel node) {
final SimpleScriptContext context = new SimpleScriptContext();
final OutputStreamWriter outWriter = new OutputStreamWriter(outStream);
context.setWriter(outWriter);
context.setErrorWriter(outWriter);
context.setBindings(createBinding(node), javax.script.ScriptContext.ENGINE_SCOPE);
return context;
}
private Bindings createBinding(final NodeModel node) {
final Bindings binding = engine.createBindings();
binding.put("c", ProxyFactory.createController(scriptContext));
binding.put("node", ProxyFactory.createNode(node, scriptContext));
binding.putAll(ScriptingConfiguration.getStaticProperties());
return binding;
}
static ScriptEngineManager getScriptEngineManager() {
synchronized (scriptEngineManagerMutex) {
if (scriptEngineManager == null) {
final ClassLoader classLoader = createClassLoader();
scriptEngineManager = new ScriptEngineManager(classLoader);
}
return scriptEngineManager;
}
}
private static ClassLoader createClassLoader() {
if (classLoader == null) {
final List<String> classpath = ScriptResources.getClasspath();
final List<URL> urls = new ArrayList<URL>();
for (String path : classpath) {
urls.add(pathToUrl(path));
}
classLoader = URLClassLoader.newInstance(urls.toArray(new URL[urls.size()]),
GenericScript.class.getClassLoader());
}
return classLoader;
}
private static URL pathToUrl(String path) {
try {
return new File(path).toURI().toURL();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private void compileAndCache(Compilable engine) throws Throwable {
if (compileTimeStrategy.canUseOldCompiledScript())
return;
compiledScript = null;
errorsInScript = null;
try {
scriptSource.rereadFile();
compileTimeStrategy.scriptCompileStart();
compiledScript = engine.compile(scriptSource.getScript());
compileTimeStrategy.scriptCompiled();
}
catch (Throwable e) {
errorsInScript = e;
throw e;
}
}
private static ScriptEngine findScriptEngine(String scriptEngineName) {
final ScriptEngineManager manager = getScriptEngineManager();
return checkNotNull(manager.getEngineByName(scriptEngineName), "name", scriptEngineName);
}
private static ScriptEngine findScriptEngine(File scriptFile) {
final ScriptEngineManager manager = getScriptEngineManager();
final String extension = FilenameUtils.getExtension(scriptFile.getName());
return checkNotNull(manager.getEngineByExtension(extension), "extension", extension);
}
private static ScriptEngine checkNotNull(final ScriptEngine motor, String what, String detail) {
if (motor == null)
throw new RuntimeException("can't load script engine by " + what + ": " + detail);
return motor;
}
private void handleScriptRuntimeException(final ScriptException e) {
outStream.print("message: " + e.getMessage());
int lineNumber = e.getLineNumber();
outStream.print("Line number: " + lineNumber);
errorHandler.gotoLine(lineNumber);
throw new ExecuteScriptException(e.getMessage() + " at line " + lineNumber,
// The ScriptException should have a cause. Use
// that, it is what we want to know.
(e.getCause() == null) ? e : e.getCause());
}
@Override
public boolean permissionsEquals(ScriptingPermissions permissions) {
if (this.specificPermissions == null)
return this.specificPermissions == permissions;
else
return this.specificPermissions.equals(permissions);
}
}