package com.tinkerpop.rexster.protocol;
import org.apache.log4j.Logger;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages the list of EngineHolder items for the current ScriptEngineManager.
* By default, the ScriptEngine instance is never reset.
*
* @author Stephen Mallette (http://stephen.genoprime.com)
* @author Blake Eggleston (bdeggleston.github.com)
*/
public class EngineController {
private static final Logger logger = Logger.getLogger(EngineController.class);
public static final int RESET_NEVER = -1;
private final Map<String, EngineHolder> engines = new ConcurrentHashMap<String, EngineHolder>();
private static final List<EngineConfiguration> engineConfigurations = new ArrayList<EngineConfiguration>();
private static final Object lock = new Object();
private static final AtomicBoolean initializing = new AtomicBoolean(false);
/**
* All gremlin engines are prefixed with this value.
*/
private static final String ENGINE_NAME_PREFIX = "gremlin-";
private static EngineController engineController;
private EngineController() {
// for ruby
System.setProperty("org.jruby.embed.localvariable.behavior", "persistent");
loadEngines();
}
private void loadEngines() {
initializing.set(true);
engines.clear();
final ScriptEngineManager manager = new ScriptEngineManager();
for (ScriptEngineFactory factory : manager.getEngineFactories()) {
logger.info(String.format("ScriptEngineManager has factory for: %s", factory.getLanguageName()));
// only add engine factories for those languages that are gremlin based.
final EngineConfiguration engineConfiguration = findEngineConfiguration(factory.getLanguageName());
if (engineConfiguration != null) {
logger.info(String.format("Registered ScriptEngine for: %s", factory.getLanguageName()));
this.engines.put(factory.getLanguageName(), new EngineHolder(factory, engineConfiguration));
}
}
initializing.set(false);
}
/**
* Must call this before a call to getInstance() if the reset count is to be taken into account. Defaults
* to the gremlin groovy script engine.
*/
public static void configure(final int resetCount, final String initScriptFile){
configure(resetCount, initScriptFile, new HashSet<String>() {{
add("gremlin-groovy");
}});
}
/**
* Must call this before a call to getInstance() if the reset count is to be taken into account.
*
* @param configuredEngineNames A list of script engine names that should be exposed.
*/
public static void configure(final int resetCount, final String initScriptFile, final Set<String> configuredEngineNames){
configure(resetCount, initScriptFile, configuredEngineNames, null, null);
}
/**
* Must call this before a call to getInstance() if the reset count is to be taken into account. ScriptEngine
* imports can be defined that are included in addition to the standard imports provided by Gremlin. These
* imports only get included for the GremlinGroovyScriptEngine at this time.
*
* @param configuredEngineNames A list of script engine names that should be exposed.
* @param imports A list of imports for the script engine.
* @param staticImports A list of static imports for the script engine.
*/
public static void configure(final int resetCount, final String initScriptFile, final Set<String> configuredEngineNames,
final Set<String> imports, final Set<String> staticImports){
final List<EngineConfiguration> confs = new ArrayList<EngineConfiguration>();
for (String configuredEngineName : configuredEngineNames) {
confs.add(new EngineConfiguration(configuredEngineName, resetCount, initScriptFile, imports, staticImports));
}
configure(confs);
}
public synchronized static void configure(final List<EngineConfiguration> configurations) {
// need to null the singleton controller. this will kill script processing dead. it's as good as
// restarting the server practically.
engineController = null;
engineConfigurations.clear();
engineConfigurations.addAll(configurations);
}
public static EngineController getInstance() {
if (engineController == null) {
// hack to deal with this singleton that needs to be created in a synchronous manner without having
// to make the entire method synchronized. guess this would work, though a bit of an ugly double check
synchronized (lock) {
if (engineController == null)
engineController = new EngineController();
}
}
return engineController;
}
public List<String> getAvailableEngineLanguages() {
final List<String> languages = new ArrayList<String>();
for (String fullLanguageName : this.engines.keySet()) {
languages.add(fullLanguageName.substring(fullLanguageName.indexOf(ENGINE_NAME_PREFIX) + ENGINE_NAME_PREFIX.length()));
}
return Collections.unmodifiableList(languages);
}
public boolean isEngineAvailable(final String languageName) {
boolean available;
try {
getEngineByLanguageName(languageName);
available = true;
} catch (ScriptException se) {
available = false;
}
return available;
}
public EngineHolder getEngineByLanguageName(final String languageName) throws ScriptException {
while(initializing.get()) {
try {
Thread.sleep(100);
} catch (Exception ex) {
logger.warn("An engine was requested while the ScriptEngine was initializing.", ex);
throw new RuntimeException(ex);
}
}
final EngineHolder engine = this.engines.get(ENGINE_NAME_PREFIX + languageName);
if (engine == null) {
throw new ScriptException("No engine for the language: " + languageName);
}
return engine;
}
private static EngineConfiguration findEngineConfiguration(final String engineName) {
for (EngineConfiguration conf : engineConfigurations) {
if (conf.getScriptEngineName().equals(engineName)) {
return conf;
}
}
return null;
}
}