/*
* Copyright (C) 2004-2014 L2J Server
*
* This file is part of L2J Server.
*
* L2J Server 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 3 of the License, or
* (at your option) any later version.
*
* L2J Server 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 .
*/
package com.l2jserver.gameserver.scripting;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import javolution.util.FastMap;
import com.l2jserver.Config;
import com.l2jserver.script.jython.JythonScriptEngine;
/**
* Caches script engines and provides functionality for executing and managing scripts.
* @author KenM
*/
public final class L2ScriptEngineManager
{
private static final Logger _log = Logger.getLogger(L2ScriptEngineManager.class.getName());
public static final File SCRIPT_FOLDER = new File(Config.DATAPACK_ROOT.getAbsolutePath(), "data/scripts");
public static L2ScriptEngineManager getInstance()
{
return SingletonHolder._instance;
}
private final Map _nameEngines = new FastMap<>();
private final Map _extEngines = new FastMap<>();
private final List> _scriptManagers = new LinkedList<>();
private File _currentLoadingScript;
// Configs
// TODO move to config file
/**
* Informs(logs) the scripts being loaded.
* Apply only when executing script from files.
*/
private static final boolean VERBOSE_LOADING = false;
/**
* If the script engine supports compilation the script is compiled before execution.
*/
private static final boolean ATTEMPT_COMPILATION = true;
/**
* Clean an previous error log(if such exists) for the script being loaded before trying to load.
* Apply only when executing script from files.
*/
private static final boolean PURGE_ERROR_LOG = true;
protected L2ScriptEngineManager()
{
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
List factories = scriptEngineManager.getEngineFactories();
for (ScriptEngineFactory factory : factories)
{
try
{
ScriptEngine engine = factory.getScriptEngine();
boolean reg = false;
for (String name : factory.getNames())
{
ScriptEngine existentEngine = _nameEngines.get(name);
if (existentEngine != null)
{
double engineVer = Double.parseDouble(factory.getEngineVersion());
double existentEngVer = Double.parseDouble(existentEngine.getFactory().getEngineVersion());
if (engineVer <= existentEngVer)
{
continue;
}
}
reg = true;
_nameEngines.put(name, engine);
}
if (reg)
{
_log.info("Script Engine: " + factory.getEngineName() + " " + factory.getEngineVersion() + " - Language: " + factory.getLanguageName() + " - Language Version: " + factory.getLanguageVersion());
}
for (String ext : factory.getExtensions())
{
if (!ext.equals("java") || factory.getLanguageName().equals("java"))
{
_extEngines.put(ext, engine);
}
}
}
catch (Exception e)
{
_log.log(Level.WARNING, "Failed initializing factory: " + e.getMessage(), e);
}
}
preConfigure();
}
private void preConfigure()
{
// Jython sys.path
String dataPackDirForwardSlashes = SCRIPT_FOLDER.getPath().replaceAll("\\\\", "/");
String configScript = "import sys;sys.path.insert(0,'" + dataPackDirForwardSlashes + "');";
try
{
eval("jython", configScript);
}
catch (ScriptException e)
{
_log.severe("Failed preconfiguring jython: " + e.getMessage());
}
}
private ScriptEngine getEngineByName(String name)
{
return _nameEngines.get(name);
}
private ScriptEngine getEngineByExtension(String ext)
{
return _extEngines.get(ext);
}
public void executeScriptList(File list) throws IOException
{
File file;
if (!Config.ALT_DEV_NO_HANDLERS && Config.ALT_DEV_NO_QUESTS)
{
file = new File(SCRIPT_FOLDER, "handlers/MasterHandler.java");
try
{
executeScript(file);
_log.info("Handlers loaded, all other scripts skipped");
return;
}
catch (ScriptException se)
{
_log.log(Level.WARNING, "", se);
}
}
if (Config.ALT_DEV_NO_QUESTS)
{
return;
}
if (list.isFile())
{
try (FileInputStream fis = new FileInputStream(list);
InputStreamReader isr = new InputStreamReader(fis);
LineNumberReader lnr = new LineNumberReader(isr))
{
String line;
while ((line = lnr.readLine()) != null)
{
if (Config.ALT_DEV_NO_HANDLERS && line.contains("MasterHandler.java"))
{
continue;
}
String[] parts = line.trim().split("#");
if ((parts.length > 0) && !parts[0].isEmpty() && (parts[0].charAt(0) != '#'))
{
line = parts[0];
if (line.endsWith("/**"))
{
line = line.substring(0, line.length() - 3);
}
else if (line.endsWith("/*"))
{
line = line.substring(0, line.length() - 2);
}
file = new File(SCRIPT_FOLDER, line);
if (file.isDirectory() && parts[0].endsWith("/**"))
{
executeAllScriptsInDirectory(file, true, 32);
}
else if (file.isDirectory() && parts[0].endsWith("/*"))
{
executeAllScriptsInDirectory(file);
}
else if (file.isFile())
{
try
{
executeScript(file);
}
catch (ScriptException e)
{
reportScriptFileError(file, e);
}
}
else
{
_log.warning("Failed loading: (" + file.getCanonicalPath() + ") @ " + list.getName() + ":" + lnr.getLineNumber() + " - Reason: doesnt exists or is not a file.");
}
}
}
}
}
else
{
throw new IllegalArgumentException("Argument must be an file containing a list of scripts to be loaded");
}
}
public void executeAllScriptsInDirectory(File dir)
{
executeAllScriptsInDirectory(dir, false, 0);
}
public void executeAllScriptsInDirectory(File dir, boolean recurseDown, int maxDepth)
{
executeAllScriptsInDirectory(dir, recurseDown, maxDepth, 0);
}
private void executeAllScriptsInDirectory(File dir, boolean recurseDown, int maxDepth, int currentDepth)
{
if (dir.isDirectory())
{
for (File file : dir.listFiles())
{
if (file.isDirectory() && recurseDown && (maxDepth > currentDepth))
{
if (VERBOSE_LOADING)
{
_log.info("Entering folder: " + file.getName());
}
executeAllScriptsInDirectory(file, recurseDown, maxDepth, currentDepth + 1);
}
else if (file.isFile())
{
try
{
String name = file.getName();
int lastIndex = name.lastIndexOf('.');
String extension;
if (lastIndex != -1)
{
extension = name.substring(lastIndex + 1);
ScriptEngine engine = getEngineByExtension(extension);
if (engine != null)
{
executeScript(engine, file);
}
}
}
catch (ScriptException e)
{
reportScriptFileError(file, e);
}
}
}
}
else
{
throw new IllegalArgumentException("The argument directory either doesnt exists or is not an directory.");
}
}
public void executeScript(File file) throws ScriptException
{
String name = file.getName();
int lastIndex = name.lastIndexOf('.');
String extension;
if (lastIndex != -1)
{
extension = name.substring(lastIndex + 1);
}
else
{
throw new ScriptException("Script file (" + name + ") doesnt has an extension that identifies the ScriptEngine to be used.");
}
ScriptEngine engine = getEngineByExtension(extension);
if (engine == null)
{
throw new ScriptException("No engine registered for extension (" + extension + ")");
}
executeScript(engine, file);
}
public void executeScript(String engineName, File file) throws ScriptException
{
ScriptEngine engine = getEngineByName(engineName);
if (engine == null)
{
throw new ScriptException("No engine registered with name (" + engineName + ")");
}
executeScript(engine, file);
}
public void executeScript(ScriptEngine engine, File file) throws ScriptException
{
if (VERBOSE_LOADING)
{
_log.info("Loading Script: " + file.getAbsolutePath());
}
if (PURGE_ERROR_LOG)
{
String name = file.getAbsolutePath() + ".error.log";
File errorLog = new File(name);
if (errorLog.isFile())
{
errorLog.delete();
}
}
final String relativeName = file.getAbsolutePath().substring(SCRIPT_FOLDER.getAbsolutePath().length() + 1).replace('\\', '/');
try (FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader reader = new BufferedReader(isr))
{
if ((engine instanceof Compilable) && ATTEMPT_COMPILATION)
{
ScriptContext context = new SimpleScriptContext();
context.setAttribute("mainClass", getClassForFile(file).replace('/', '.').replace('\\', '.'), ScriptContext.ENGINE_SCOPE);
context.setAttribute(ScriptEngine.FILENAME, relativeName, ScriptContext.ENGINE_SCOPE);
context.setAttribute("classpath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
context.setAttribute("sourcepath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
context.setAttribute(JythonScriptEngine.JYTHON_ENGINE_INSTANCE, engine, ScriptContext.ENGINE_SCOPE);
setCurrentLoadingScript(file);
ScriptContext ctx = engine.getContext();
try
{
engine.setContext(context);
Compilable eng = (Compilable) engine;
CompiledScript cs = eng.compile(reader);
cs.eval(context);
}
finally
{
engine.setContext(ctx);
setCurrentLoadingScript(null);
context.removeAttribute(ScriptEngine.FILENAME, ScriptContext.ENGINE_SCOPE);
context.removeAttribute("mainClass", ScriptContext.ENGINE_SCOPE);
}
}
else
{
ScriptContext context = new SimpleScriptContext();
context.setAttribute("mainClass", getClassForFile(file).replace('/', '.').replace('\\', '.'), ScriptContext.ENGINE_SCOPE);
context.setAttribute(ScriptEngine.FILENAME, relativeName, ScriptContext.ENGINE_SCOPE);
context.setAttribute("classpath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
context.setAttribute("sourcepath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
setCurrentLoadingScript(file);
try
{
engine.eval(reader, context);
}
finally
{
setCurrentLoadingScript(null);
engine.getContext().removeAttribute(ScriptEngine.FILENAME, ScriptContext.ENGINE_SCOPE);
engine.getContext().removeAttribute("mainClass", ScriptContext.ENGINE_SCOPE);
}
}
}
catch (IOException e)
{
_log.log(Level.WARNING, "Error executing script!", e);
}
}
public static String getClassForFile(File script)
{
String path = script.getAbsolutePath();
String scpPath = SCRIPT_FOLDER.getAbsolutePath();
if (path.startsWith(scpPath))
{
int idx = path.lastIndexOf('.');
return path.substring(scpPath.length() + 1, idx);
}
return null;
}
public ScriptContext getScriptContext(ScriptEngine engine)
{
return engine.getContext();
}
public ScriptContext getScriptContext(String engineName)
{
ScriptEngine engine = getEngineByName(engineName);
if (engine == null)
{
throw new IllegalStateException("No engine registered with name (" + engineName + ")");
}
return getScriptContext(engine);
}
public Object eval(ScriptEngine engine, String script, ScriptContext context) throws ScriptException
{
if ((engine instanceof Compilable) && ATTEMPT_COMPILATION)
{
Compilable eng = (Compilable) engine;
CompiledScript cs = eng.compile(script);
return context != null ? cs.eval(context) : cs.eval();
}
return context != null ? engine.eval(script, context) : engine.eval(script);
}
public Object eval(String engineName, String script) throws ScriptException
{
return eval(engineName, script, null);
}
public Object eval(String engineName, String script, ScriptContext context) throws ScriptException
{
ScriptEngine engine = getEngineByName(engineName);
if (engine == null)
{
throw new ScriptException("No engine registered with name (" + engineName + ")");
}
return eval(engine, script, context);
}
public Object eval(ScriptEngine engine, String script) throws ScriptException
{
return eval(engine, script, null);
}
public void reportScriptFileError(File script, ScriptException e)
{
String dir = script.getParent();
String name = script.getName() + ".error.log";
if (dir != null)
{
final File file = new File(dir + "/" + name);
try (FileOutputStream fos = new FileOutputStream(file))
{
String errorHeader = "Error on: " + file.getCanonicalPath() + Config.EOL + "Line: " + e.getLineNumber() + " - Column: " + e.getColumnNumber() + Config.EOL + Config.EOL;
fos.write(errorHeader.getBytes());
fos.write(e.getMessage().getBytes());
_log.warning("Failed executing script: " + script.getAbsolutePath() + ". See " + file.getName() + " for details.");
}
catch (IOException ioe)
{
_log.log(Level.WARNING, "Failed executing script: " + script.getAbsolutePath() + Config.EOL + e.getMessage() + "Additionally failed when trying to write an error report on script directory. Reason: " + ioe.getMessage(), ioe);
}
}
else
{
_log.log(Level.WARNING, "Failed executing script: " + script.getAbsolutePath() + Config.EOL + e.getMessage() + "Additionally failed when trying to write an error report on script directory.", e);
}
}
public void registerScriptManager(ScriptManager> manager)
{
_scriptManagers.add(manager);
}
public void removeScriptManager(ScriptManager> manager)
{
_scriptManagers.remove(manager);
}
public List> getScriptManagers()
{
return _scriptManagers;
}
/**
* @param currentLoadingScript The currentLoadingScript to set.
*/
protected void setCurrentLoadingScript(File currentLoadingScript)
{
_currentLoadingScript = currentLoadingScript;
}
/**
* @return Returns the currentLoadingScript.
*/
protected File getCurrentLoadingScript()
{
return _currentLoadingScript;
}
private static class SingletonHolder
{
protected static final L2ScriptEngineManager _instance = new L2ScriptEngineManager();
}
}