/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.internal.gosu.parser;
import gw.config.CommonServices;
import gw.fs.IDirectory;
import gw.fs.IResource;
import gw.internal.gosu.module.DefaultSingleModule;
import gw.internal.gosu.module.JreModule;
import gw.internal.gosu.module.Module;
import gw.lang.cli.SystemExitIgnoredException;
import gw.lang.init.GosuPathEntry;
import gw.lang.parser.GosuParserFactory;
import gw.lang.parser.IGosuParser;
import gw.lang.parser.IGosuProgramParser;
import gw.lang.parser.ILanguageLevel;
import gw.lang.parser.IParseResult;
import gw.lang.parser.ParserOptions;
import gw.lang.reflect.IType;
import gw.lang.reflect.ITypeRef;
import gw.lang.reflect.TypeSystem;
import gw.lang.reflect.gs.BytecodeOptions;
import gw.lang.reflect.gs.GosuClassTypeLoader;
import gw.lang.reflect.java.JavaTypes;
import gw.lang.reflect.module.Dependency;
import gw.lang.reflect.module.IExecutionEnvironment;
import gw.lang.reflect.module.IModule;
import gw.lang.reflect.module.IProject;
import gw.util.GosuExceptionUtil;
import gw.util.ILogger;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
public class ExecutionEnvironment implements IExecutionEnvironment
{
private static final IProject DEFAULT_PROJECT = new DefaultSingleModuleRuntimeProject();
private static final Map<Object, ExecutionEnvironment> INSTANCES = new WeakHashMap<Object, ExecutionEnvironment>();
private static ExecutionEnvironment THE_ONE;
public static final String CLASS_REDEFINER_THREAD = "Gosu class redefiner";
/**
* "Special" java classes that will be used to locate "special" JARs which should be
* included in the classpath. This is a replacement to old SPECIAL_FILES variable.
*/
private static final List<String> SPECIAL_CLASSES = Arrays.asList(
"javax.servlet.Servlet",
"javax.servlet.http.HttpServletRequest"
);
private IProject _project;
private List<IModule> _modules;
private IModule _defaultModule;
private IModule _jreModule;
private IModule _rootModule;
private TypeSystemState _state = TypeSystemState.STOPPED;
public static ExecutionEnvironment instance()
{
if( INSTANCES.size() == 1 )
{
return THE_ONE == null
? THE_ONE = INSTANCES.values().iterator().next()
: THE_ONE;
}
IModule mod = INSTANCES.size() > 0 ? TypeSystem.getCurrentModule() : null;
if( mod != null )
{
ExecutionEnvironment execEnv = (ExecutionEnvironment)mod.getExecutionEnvironment();
if( execEnv == null )
{
throw new IllegalStateException( "Module, " + mod.getName() + ", has a null execution environment. This is bad." );
}
return execEnv;
}
if( INSTANCES.size() > 0 )
{
// Return first non-default project
// Yes, this is a guess, but we need to guess for the case where we're running tests
// and loading classes in lots of threads where the current module is not pushed
for( ExecutionEnvironment execEnv: INSTANCES.values() )
{
if( execEnv.getProject() != DEFAULT_PROJECT &&
!execEnv.getProject().isDisposed() )
{
return execEnv;
}
}
}
return instance( DEFAULT_PROJECT );
}
public static ExecutionEnvironment instance( IProject project )
{
if( project == null )
{
throw new IllegalStateException( "Project must not be null" );
}
if( project instanceof IExecutionEnvironment )
{
throw new RuntimeException( "Passed in ExecutionEnvironment as project" );
}
ExecutionEnvironment execEnv = INSTANCES.get( project );
if( execEnv == null )
{
INSTANCES.put( project, execEnv = new ExecutionEnvironment( project ) );
}
return execEnv;
}
public static Collection<? extends IExecutionEnvironment> getAll()
{
return INSTANCES.values();
}
private ExecutionEnvironment( IProject project )
{
_project = project;
_modules = new ArrayList<IModule>();
}
public IProject getProject()
{
return _project;
}
public List<? extends IModule> getModules() {
return _modules;
}
public void initializeDefaultSingleModule( List<? extends GosuPathEntry> pathEntries ) {
_state = TypeSystemState.STARTING;
try {
DefaultSingleModule singleModule = _defaultModule == null ? new DefaultSingleModule( this ) : (DefaultSingleModule)_defaultModule;
List<IDirectory> allSources = new ArrayList<IDirectory>();
List<IDirectory> allRoots = new ArrayList<IDirectory>();
for( GosuPathEntry pathEntry : pathEntries )
{
allRoots.add(pathEntry.getRoot());
allSources.addAll(pathEntry.getSources());
}
singleModule.configurePaths(createDefaultClassPath(), allSources);
singleModule.setRoots(allRoots);
_defaultModule = singleModule;
_modules = new ArrayList<IModule>(Collections.singletonList(singleModule));
// pushModule(singleModule); // Push and leave pushed (in this thread)
singleModule.initializeTypeLoaders();
CommonServices.getCoercionManager().init();
startSneakyDebugThread();
} finally {
_state = TypeSystemState.STARTED;
}
}
public void uninitializeDefaultSingleModule() {
_state = TypeSystemState.STOPPING;
try {
if (_defaultModule != null) {
DefaultSingleModule m = (DefaultSingleModule) _defaultModule;
m.getModuleTypeLoader().uninitializeTypeLoaders();
m.getModuleTypeLoader().reset();
m.setRoots(Collections.<IDirectory>emptyList());
m.configurePaths(Collections.<IDirectory>emptyList(), Collections.<IDirectory>emptyList());
}
_modules.clear();
} finally {
_state = TypeSystemState.STOPPED;
}
}
public void initializeMultipleModules(List<? extends IModule> modules) {
_state = TypeSystemState.STARTING;
try {
// noinspection unchecked
_defaultModule = null;
_rootModule = null;
_modules = (List<IModule>) modules;
for (IModule module : modules) {
TypeSystem.pushModule(module);
try {
((Module) module).initializeTypeLoaders();
} finally {
TypeSystem.popModule(module);
}
}
CommonServices.getCoercionManager().init();
FrequentUsedJavaTypeCache.instance( this ).init();
} finally {
_state = TypeSystemState.STARTED;
}
}
public void uninitializeMultipleModules() {
_state = TypeSystemState.STOPPING;
try {
TypeSystem.shutdown( this);
for (IModule module : _modules) {
((Module) module).getModuleTypeLoader().uninitializeTypeLoaders();
}
_jreModule = null;
_rootModule = null;
_modules.clear();
} finally {
_state = TypeSystemState.STOPPED;
}
}
public void addModule(IModule module) {
checkForDuplicates(module.getName());
// noinspection unchecked
_modules.add(module);
TypeSystem.pushModule(module);
try {
((Module) module).initializeTypeLoaders();
} finally {
TypeSystem.popModule(module);
}
}
void checkForDuplicates(String moduleName) {
for (IModule m : getModules()) {
if (m.getName().equals(moduleName)) {
throw new RuntimeException("Module " + moduleName + " allready exists.");
}
}
}
public void removeModule(IModule module) {
_modules.remove(module);
}
public IModule getModule(String strModuleName) {
for (IModule m : _modules) {
if (m.getName().equals(strModuleName)) {
return m;
}
}
if( isSingleModuleMode() && GLOBAL_MODULE_NAME.equals( strModuleName ) ) {
return getGlobalModule();
}
return null;
}
public IModule getModule( IResource file ) {
List<? extends IModule> modules = getModules();
if (modules.size() == 1) {
return modules.get(0); // single module
}
for ( IModule module : modules) {
if (module != _rootModule) {
if (isInModule(module, file)) {
return module;
}
}
}
if (isInModule(_rootModule, file)) {
return _rootModule;
}
return null;
}
private boolean isInModule(IModule module, IResource file) {
for (IDirectory src : module.getSourcePath()) {
if (file.equals(src) || file.isDescendantOf(src)) {
return true;
}
}
return false;
}
public IModule getModule( URL url ) {
return getModule( CommonServices.getFileSystem().getIFile( url ) );
}
@Override
public IModule createJreModule( )
{
_jreModule = new JreModule( this );
return _jreModule;
}
/**
* @return The module responsible for resolving JRE core classes e.g.,
* java.lang.* etc. Note in default single module environment this is
* the single module, otherwise this is the module create by calling createJreModule().
* This method will never return null but it will throw an NPE if the JRE module is null.
*/
public IModule getJreModule() {
if (_jreModule == null) {
if (isSingleModuleMode()) {
_jreModule = getGlobalModule();
} else {
throw new RuntimeException("The JRE module was not created. Please create it before trying to get it.");
}
}
return _jreModule;
}
public boolean isSingleModuleMode()
{
return _modules != null && _modules.size() == 1 && _modules.get(0) instanceof DefaultSingleModule;
}
public static boolean isDefaultSingleModuleMode()
{
if( INSTANCES.size() == 1 )
{
if( THE_ONE != null )
{
return THE_ONE.isSingleModuleMode();
}
Collection<ExecutionEnvironment> values = INSTANCES.values();
return values.iterator().next().isSingleModuleMode();
}
return false;
}
public IModule getGlobalModule() {
if (_rootModule == null) {
String moduleName = System.getProperty("GW_ROOT_MODULE");
if (moduleName != null) {
_rootModule = getModule(moduleName);
if (_rootModule == null) {
throw new RuntimeException("The specified root module '" + moduleName +"' does not exist.");
}
} else {
_rootModule = findRootModule();
}
}
return _rootModule;
}
public IModule findRootModule() {
List<IModule> moduleRoots = new ArrayList<IModule>(_modules);
for (IModule module : _modules) {
for (Dependency d : module.getDependencies()) {
moduleRoots.remove(d.getModule());
}
}
return moduleRoots.size() > 0 ? moduleRoots.get(0) : null;
}
public TypeSystemState getState() {
return _state;
}
public void renameModule(IModule module, String newName) {
((ExecutionEnvironment)module.getExecutionEnvironment()).checkForDuplicates(newName);
((Module) module).setName(newName);
}
public void shutdown() {
for (IModule module : _modules) {
module.getModuleTypeLoader().shutdown();
}
INSTANCES.clear();
THE_ONE = null;
}
private static class DefaultSingleModuleRuntimeProject implements IProject
{
@Override
public String getName()
{
return getClass().getSimpleName();
}
@Override
public Object getNativeProject()
{
return this;
}
@Override
public boolean isDisposed()
{
return false;
}
@Override
public String toString()
{
return "Default Single Runtime Execution Environment";
}
@Override
public boolean isHeadless()
{
return false;
}
@Override
public boolean isShadowMode() {
return false;
}
}
/**
* Detect whether or not the jdwp agent is alive in this process, if so start
* a thread that wakes up every N seconds and checks to see if the ReloadClassesIndicator
* Java class has been redefined by a debugger. If so, it reloads Gosu classes
* that have changed.
* <p>
* Why, you ask? Well since Gosu classes are not compiled to disk, the IDE hosting
* Gosu can't simply send the bytes in a conventional JDI redefineClasses() call.
* Yet it somehow needs to at least inform Gosu's type system in the target process
* that Gosu classes have changed. The JVMTI doesn't offer much help; there's no
* way to field an arbitrary call from the JDWP client, or for the client to send an
* arbitrary message. Nor is it possible to leverage the JVMTI's ability to handle
* method invocation etc. because the target thread must be suspended at a
* breakpoint, which is not necessarily the case during compilation, and certainly
* isn't the case for a thread dedicated to fielding such a call. What to do?
* <p>
* We can leverage redefineClasses() after all. The idea is for the IDE compiler
* to redefine a class (via asm) designated as the "ReloadClassIndicator". This class lives
* inside Gosu's type system. It has a single method: public static long timestamp()
* and returns a literal value. If the target process is being debugged (jdwp
* agent detection), a thread in the target process starts immediately and waits a
* few seconds before calling the timestamp() method, it does this in a forever loop.
* If the timestamp value changes, we assume the IDE redefined the class with a new
* value to indicate classes have changed. In turn we find and reload changed
* classes. What could be more straight forward?
* <p>
* An alternative approach would be for the IDE to establish an additional line
* of communication with the target process e.g., socket, memory, whatever. But
* that is messy (requires config on user's end) and error prone. One debug
* socket is plenty.
* <p>
* Improvements to this strategy include supplying not only an indication that stuff
* has changed, but also the names of the classes that have changed. This would
* releive the target process from having to keep track timestamps on all loaded
* classes. This could be implemented by having the class return an array of names.
* An even better improvement would be to include not just the names, but also the
* source of the classes. This would enable the debuger to modify in memory the classes
* during a remote debugging session.
*/
private void startSneakyDebugThread() {
if( !BytecodeOptions.JDWP_ENABLED.get() ) {
return;
}
ContextSensitiveCodeRunner blah = new ContextSensitiveCodeRunner();
Thread sneakyDebugThread =
new Thread(
new Runnable() {
public synchronized void run() {
long timestamp = ReloadClassesIndicator.timestamp();
long now = 0;
while (getState() != TypeSystemState.STOPPED) {
try {
wait(2000);
now = ReloadClassesIndicator.timestamp();
if (now > timestamp) {
String script = ReloadClassesIndicator.getScript();
if (script != null && script.length() > 0) {
runScript(script);
}
else {
refreshTypes();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
timestamp = now;
}
}
}
private void refreshTypes() {
String[] types = ReloadClassesIndicator.changedTypes();
System.out.println("Refreshing " + types.length + " types at " + new Date());
if (types.length > 0) {
for (String name : types) {
IType type = TypeSystem.getByFullNameIfValid(name);
if (type != null) {
TypeSystem.refresh((ITypeRef) type);
// Also update enhancement index if type is an enhancement
if( type instanceof IGosuEnhancementInternal ) {
((GosuClassTypeLoader)type.getTypeLoader()).getEnhancementIndex().addEntry(
((IGosuEnhancementInternal)type).getEnhancedType(), (IGosuEnhancementInternal)type );
}
}
}
}
CommonServices.getEntityAccess().reloadedTypes(types);
}
private void runScript( String strScript ) {
String[] result = evaluate(strScript);
if( result[0] != null && result[0].length() > 0 )
{
System.out.print( result[0] );
}
if( result[1] != null && result[1].length() > 0 )
{
System.err.print( result[1] );
}
}
public String[] evaluate( String strScript )
{
IGosuParser scriptParser = GosuParserFactory.createParser(strScript);
try
{
IGosuProgramParser programParser = GosuParserFactory.createProgramParser();
ParserOptions options = new ParserOptions().withParser( scriptParser );
IParseResult parseResult = programParser.parseExpressionOrProgram( strScript, scriptParser.getSymbolTable(), options );
Object result = parseResult.getProgram().evaluate( null );
if( result != null )
{
System.out.println( "Return Value: " + CommonServices.getCoercionManager().convertValue( result, JavaTypes.STRING() ) );
}
}
catch( Exception e )
{
boolean print = true;
Throwable t = e;
while( t != null )
{
if( t instanceof SystemExitIgnoredException)
{
print = false;
}
t = t.getCause();
}
if( print )
{
assert e != null;
e.printStackTrace();
}
}
return new String[]{null, null};
}
}, CLASS_REDEFINER_THREAD);
sneakyDebugThread.setDaemon(true);
sneakyDebugThread.start();
}
public static List<IDirectory> createDefaultClassPath( ) {
List<String> vals = new ArrayList<String>();
vals.add(System.getProperty("java.class.path", ""));
vals.add(CommonServices.getEntityAccess().getWebServerPaths());
vals.addAll(getJarsContainingSpecialClasses());
vals.add(System.getProperty("sun.boot.class.path", ""));
vals.add(System.getProperty("java.ext.dirs", ""));
vals.add(CommonServices.getEntityAccess().getPluginRepositories().toString());
return expand(vals);
}
private static List<IDirectory> expand( List<String> paths )
{
LinkedHashSet<IDirectory> expanded = new LinkedHashSet<IDirectory>();
for( String path : paths )
{
for( String pathElement : path.split( File.pathSeparator ) )
{
if( pathElement.length() > 0 )
{
IDirectory resource = CommonServices.getFileSystem().getIDirectory(new File(pathElement));
expanded.add(resource);
}
}
}
return new ArrayList<IDirectory>( expanded );
}
@Override
public boolean isShadowingMode() {
return _project.isShadowMode();
}
/**
* This method is a hack to resolve "special" system-like classes provided by execution environment.
* This is the replacement of old addSpecialJars() method
*/
private static Set<String> getJarsContainingSpecialClasses() {
Set<String> paths = new HashSet<String>();
for (String className : SPECIAL_CLASSES) {
getLogger().debug("Searching JAR that provides " + className + ".");
Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
if( !ILanguageLevel.Util.STANDARD_GOSU() ) {
getLogger().error("Class " + className
+ " could not be found. Gosu code might fail to compile at runtime.");
}
continue;
}
CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
if (codeSource == null) {
if( !ILanguageLevel.Util.STANDARD_GOSU() ) {
getLogger().error("Code source for " + clazz.getName()
+ " is null. Gosu code might fail to compile at runtime.");
}
continue;
}
// url might be jar:<url>!/, e.g. jar:file:/gitmo/jboss-5.1.2/common/lib/servlet-api.jar!/
// or vfszip:<url> on JBoss
// or wsjar:<url> on WebSphere
URL jarUrl = codeSource.getLocation();
// in case of complex URL the path might be like this: "file:/gitmo/jboss-5.1.2/common/lib/servlet-api.jar!/"
String path = jarUrl.getPath();
// So removing optional "!/" suffix and "file:" prefix
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
if (path.endsWith("!")) {
path = path.substring(0, path.length() - 1);
}
if (path.startsWith("file:")) {
path = path.substring("file:".length());
}
// URLDecoder.decode() decodes string from application/x-www-form-urlencoded MIME format
// while we need to decode from RFC2396 format.
// I think the only difference between formats that application/x-www-form-urlencoded decodes "+"
// to space while RFC2396 does not.
// So before using URLDecoder.decode() encode "+" to its ASCII representation
// that will be decoded back to "+" by URLDecoder.decode()
path = path.replaceAll("\\+", "%2B");
try {
String decodedPath = URLDecoder.decode(path, "UTF-8");
if (new File(decodedPath).exists()) {
paths.add(path);
} else {
getLogger().error("Could not extract filesystem path from the url " + jarUrl.getPath()
+ ". Gosu code that requires classes from that JAR might fail to compile at runtime.");
}
} catch (UnsupportedEncodingException ex) {
// impossible
throw GosuExceptionUtil.forceThrow(ex);
}
}
return paths;
}
private static ILogger getLogger() {
return CommonServices.getEntityAccess().getLogger();
}
}