/**
* Copyright (c) 2005-2008 Aptana, Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html. If redistributing this code,
* this entire header must remain intact.
*/
package org.eclipse.eclipsemonkey.lang.javascript;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.eclipsemonkey.utils.StringUtils;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.MessageConsoleStream;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.osgi.framework.Bundle;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Provides global functions to JavaScript environment
*
* @author Paul Colton
* @author Kevin Lindsey
*/
public class JavaScriptGlobal extends ScriptableObject
{
/**
* The "location" property name
*/
public static final String LOCATION_PROPERTY = "location"; //$NON-NLS-1$
/**
* The "classLoader" property name
*/
public static final String CLASS_LOADER_PROPERTY = "classLoader"; //$NON-NLS-1$
private static final String INCLUDES_PROPERTY = "includes"; //$NON-NLS-1$
private static final long serialVersionUID = -8969608471837413334L;
private static JavaScriptConsole _console;
private static MessageConsoleStream _consoleStream;
private JavaScriptPrintStream _err;
private Map _runningSetTimeouts = new HashMap();
private int _setTimeoutIndex;
private static ImageDescriptor scriptingDescriptor = JavaScriptPlugin.getImageDescriptor("icons/js_file.gif"); //$NON-NLS-1$
/**
* Provides global functions to the JavaScript environment
*
* @param cx
* The currently active script context
*/
public JavaScriptGlobal(Context cx)
{
// create unsealed standard objects
cx.initStandardObjects(this, false);
// create global properties
this.createAllProperties();
}
/**
* @see org.mozilla.javascript.ScriptableObject#getClassName()
*/
public String getClassName()
{
return "JavaScriptGlobal"; //$NON-NLS-1$
}
/**
* Returns a reference to the current console, initializing it if it's not created
*
* @return A console stream
*/
public static MessageConsoleStream getConsoleStream()
{
if (_console == null)
{
_console = new JavaScriptConsole(Messages.JavaScriptGlobal_TTL_javascript_console, scriptingDescriptor);
_consoleStream = _console.newMessageStream();
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable()
{
public void run()
{
_consoleStream.setColor(PlatformUI.getWorkbench().getDisplay().getSystemColor(SWT.COLOR_BLUE));
}
});
ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { _console });
_consoleStream.println(Messages.JavaScriptGlobal_MSG_javascript_console_started);
}
return _consoleStream;
}
/**
* Return a list of all property names. All names return by this method must appear within the definition of this
* class. Sub-classes should override this method to augment the return value to include functions defined within
* the sub-class itself.
*
* @return Returns a list of function property names to add to this global
*/
protected String[] getFunctionPropertyNames()
{
return new String[] {
"alert", //$NON-NLS-1$
"clearTimeout", //$NON-NLS-1$
"confirm", //$NON-NLS-1$
"execute", //$NON-NLS-1$
"getProperty", //$NON-NLS-1$
"include", //$NON-NLS-1$
"loadBundle", //$NON-NLS-1$
"parseXML", //$NON-NLS-1$
"prompt", //$NON-NLS-1$
"runOnUIThread", //$NON-NLS-1$
"runOnUIThreadAsync", //$NON-NLS-1$
"setClassLoader", //$NON-NLS-1$
"setTimeout", //$NON-NLS-1$
};
}
/**
* Get a list of the full paths of all files in the Active Library view
*
* @param property
* The system property name to retrieve
* @return Returns a string array of full paths
*/
public String getProperty(String property)
{
String result = null;
if (property != null && property.length() > 0)
{
result = System.getProperty(property, "undefined"); //$NON-NLS-1$
}
return result;
}
/**
* Get all text from the given input stream
*
* @param stream
* The input stream
* @return Returns all text from the input stream
* @throws IOException
*/
protected static String getText(InputStream stream) throws IOException
{
// create output buffer
StringWriter sw = new StringWriter();
// read contents into a string buffer
try
{
// get buffered reader
InputStreamReader isr = new InputStreamReader(stream);
BufferedReader reader = new BufferedReader(isr);
// create temporary buffer
char[] buf = new char[1024];
// fill buffer
int numRead = reader.read(buf);
// keep reading until the end of the stream
while (numRead != -1)
{
// output temp buffer to output buffer
sw.write(buf, 0, numRead);
// fill buffer
numRead = reader.read(buf);
}
}
finally
{
if (stream != null)
{
stream.close();
}
}
// return string buffer's content
return sw.toString();
}
/**
* Execute a command-line in the system shell
*
* @param cx
* @param thisObj
* @param args
* The string command to execute
* @param funObj
* @return Returns an object array where the first object is the return code, the second object is the text from
* stdout, and the third object is the text from stderr
*/
public static Scriptable execute(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
Scriptable result = null;
if (args.length > 0)
{
String command = args[0].toString();
String input = StringUtils.EMPTY;
if (args.length > 1)
{
input = args[1].toString();
}
Process p = null;
int retCode = 0;
String stdout = StringUtils.EMPTY;
String stderr = StringUtils.EMPTY;
try
{
p = Runtime.getRuntime().exec(command);
p.getOutputStream().write(input.getBytes());
p.getOutputStream().flush();
p.getOutputStream().close();
retCode = p.waitFor();
// retCode = p.exitValue();
stdout = getText(p.getInputStream());
stderr = getText(p.getErrorStream());
}
catch (IOException e)
{
if (p != null)
{
retCode = p.exitValue();
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
// create Object
Scriptable scope = ScriptableObject.getTopLevelScope(thisObj);
result = cx.newObject(scope, "Object"); //$NON-NLS-1$
// set property values
result.put("code", result, new Integer(retCode)); //$NON-NLS-1$
result.put("stdout", result, stdout); //$NON-NLS-1$
result.put("stderr", result, stderr); //$NON-NLS-1$
}
// return result
return result;
}
/**
* Popup an alert box with the specified message
*
* @param message
* The message to display in an alert dialog
*/
public void alert(final String message)
{
final Display currentDisplay = Display.getCurrent();
if (currentDisplay != null)
{
currentDisplay.syncExec(new Runnable()
{
public void run()
{
Shell shell = currentDisplay.getActiveShell();
if (shell != null)
{
MessageDialog.openWarning(shell, Messages.JavaScriptGlobal_TTL_Alert_dialog, message);
}
}
});
}
}
/**
* Stop a setTimeout from firing its associated function
*
* @param timeoutId
* The setTimeout id returned when setTimeout was invoked. If the id is not recognized, this method does
* nothing
*/
public void clearTimeout(int timeoutId)
{
synchronized (this._runningSetTimeouts)
{
Integer id = new Integer(timeoutId);
if (this._runningSetTimeouts.containsKey(id))
{
this._runningSetTimeouts.remove(id);
}
}
}
/**
* Popup an confirm box with the specified message
*
* @param message
* The message to display at the confirmation prompt
* @return boolean
*/
public boolean confirm(final String message)
{
/**
* inner class for result
*/
class Answer
{
public boolean result = false;
}
// create instance of inner class
final Answer a = new Answer();
// get reply from user
final Display currentDisplay = Display.getCurrent();
if (currentDisplay != null)
{
currentDisplay.syncExec(new Runnable()
{
public void run()
{
Shell shell = currentDisplay.getActiveShell();
if (shell != null)
{
a.result = MessageDialog.openConfirm(shell, Messages.JavaScriptGlobal_TTL_Confirm_dialog, message);
}
}
});
}
return a.result;
}
/**
* Create all properties for this global instance
*/
protected void createAllProperties()
{
this.createProperties();
this.createFunctionProperties();
}
/**
* Create all function properties for this global. Sub-classes can override this method to modify how function
* properties are added to global; however, most sub-classes will need only to override getFunctionPropertyNames to
* include the additional function names provided by that instance.
*/
protected void createFunctionProperties()
{
String[] propertyNames = this.getFunctionPropertyNames();
if (propertyNames != null)
{
this.defineFunctionProperties(propertyNames, this.getClass(), READONLY | PERMANENT);
}
}
/**
* Create all non-function properties for this global. Sub-classes should override this method to add their own
* non-function properties to this global
*/
protected void createProperties()
{
// create standard error stream, cache, and add to global
this._err = new JavaScriptPrintStream(this, System.err);
this.defineProperty("err", _err, READONLY | PERMANENT); //$NON-NLS-1$
// get standard out stream and add to global
MessageConsoleStream stream = getConsoleStream();
this.defineProperty("out", stream, READONLY | PERMANENT); //$NON-NLS-1$
}
/**
* Load a script into a currently executing library
*
* @param cx
* The scripting context
* @param thisObj
* The object that activated this function call
* @param args
* The arguments passed to this function call
* @param funObj
* The function object that invoked this method
*/
public static void include(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
if (args.length > 0)
{
String libraryName = Context.toString(args[0]);
java.io.File library = new java.io.File(libraryName);
if (library.exists() == false)
{
String location = Context.toString(thisObj.get(LOCATION_PROPERTY, thisObj));
java.io.File parent = new java.io.File(location).getParentFile();
String name = parent.getAbsolutePath() + java.io.File.separator + libraryName;
library = new java.io.File(name);
}
if (library.exists())
{
String absolutePath = library.getAbsolutePath();
Scriptable includes;
if (thisObj.has(INCLUDES_PROPERTY, thisObj))
{
includes = (Scriptable) thisObj.get(INCLUDES_PROPERTY, thisObj);
}
else
{
includes = cx.newObject(thisObj);
((ScriptableObject) thisObj).defineProperty(INCLUDES_PROPERTY, includes, READONLY | PERMANENT);
}
// only compile and execute script if we haven't already
if (includes.has(absolutePath, includes) == false)
{
try
{
// grab the script's source
String source = JavaScriptGlobal.getText(new FileInputStream(library));
// compile the script
Script script = cx.compileString(source, absolutePath, 1, null);
// tag as loaded
includes.put(absolutePath, includes, ""); //$NON-NLS-1$
// exec
script.exec(cx, thisObj);
}
catch (IOException e)
{
// I/O error reading library
}
}
}
else
{
// cannot locate include
}
}
else
{
// no include file defined
}
}
/**
* Load the specified bundle and add it to the active JS class loader
*
* @param cx
* The scripting context
* @param thisObj
* The object that activated this function call
* @param args
* The arguments passed to this function call
* @param funObj
* The function object that invoked this method
*/
public static void loadBundle(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
if (args.length > 0)
{
String bundleName = Context.toString(args[0]);
Bundle bundle = Platform.getBundle(bundleName);
if (bundle == null)
{
throw new RuntimeException("Global_Bundle_Not_Found: " + bundleName); //$NON-NLS-1$
}
ClassLoader c = cx.getApplicationClassLoader();
if (c instanceof JavaScriptClassLoader)
{
JavaScriptClassLoader classLoader = (JavaScriptClassLoader) c;
classLoader.addBundle(bundle);
}
else
{
throw new RuntimeException("JavaScriptClassLoader not the application Classloader."); //$NON-NLS-1$
}
}
}
/**
* Parse the specified XML string and return a W3C DOM as a result
*
* @param xml
* The source XML string
* @return Document The resulting Document element or null
*/
public Document parseXML(String xml)
{
Document result = null;
if (xml != null && xml.length() > 0)
{
try
{
// get the document builder factory
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// create the document builder
DocumentBuilder builder = factory.newDocumentBuilder();
// create a stream from our XML string
InputStream in = new ByteArrayInputStream(xml.getBytes());
// parse XML to create our document element
result = builder.parse(in);
}
catch (ParserConfigurationException e)
{
e.printStackTrace();
}
catch (SAXException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
}
return result;
}
/**
* Popup an prompt box with the specified message
*
* @param cx
* The scripting context
* @param thisObj
* The object that activated this function call
* @param args
* The message to display in the prompt dialog
* @param funObj
* The function object that invoked this method
* @return boolean
*/
public static Object prompt(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
final Display currentDisplay = Display.getCurrent();
String messageArg = Messages.JavaScriptGlobal_MSG_Prompt_dialog_default;
String defaultValueArg = ""; //$NON-NLS-1$
if (args.length > 0)
{
messageArg = Context.toString(args[0]);
}
if (args.length > 1)
{
defaultValueArg = Context.toString(args[1]);
}
/**
* Answer
*/
class Answer
{
public Object result = ""; //$NON-NLS-1$
}
final String message = messageArg;
final String defaultValue = defaultValueArg;
final Answer a = new Answer();
if (currentDisplay != null)
{
currentDisplay.syncExec(new Runnable()
{
public void run()
{
Shell shell = currentDisplay.getActiveShell();
if (shell != null)
{
InputDialog dialog = new InputDialog(null, Messages.JavaScriptGlobal_TTL_Prompt_dialog, message, defaultValue, null);
int dialogResult = dialog.open();
if (dialogResult == Window.OK)
{
a.result = dialog.getValue();
}
else
{
a.result = Context.getUndefinedValue();
}
}
}
});
}
return a.result;
}
/**
* Execute the specified function on the UI thread
*
* @param cx
* The scripting context
* @param thisObj
* The object that activated this function call
* @param args
* The message to display in the prompt dialog
* @param funObj
* The function object that invoked this method
*/
public static void runOnUIThread(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
if (args.length > 0 && args[0] instanceof Function)
{
Function f = (Function) args[0];
// get display
final IWorkbench workbench = PlatformUI.getWorkbench();
Display display = workbench.getDisplay();
// execute callback in the correct thread
display.syncExec(new JavaScriptThread(f.getParentScope(), f, new Object[0]));
}
}
/**
* Execute the specified function on the UI thread, asynchronously
*
* @param cx
* The scripting context
* @param thisObj
* The object that activated this function call
* @param args
* The message to display in the prompt dialog
* @param funObj
* The function object that invoked this method
*/
public static void runOnUIThreadAsync(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
if (args.length > 0 && args[0] instanceof Function)
{
Function f = (Function) args[0];
// get display
final IWorkbench workbench = PlatformUI.getWorkbench();
Display display = workbench.getDisplay();
// execute callback in the correct thread
display.asyncExec(new JavaScriptThread(f.getParentScope(), f, new Object[0]));
}
}
/**
* setClassLoader
*
* @param cx
* @param thisObj
* @param args
* @param funObj
* @return new class loader or class loader passed into method
*/
public static ClassLoader setClassLoader(Context cx, Scriptable thisObj, Object[] args, Function funObj)
{
ClassLoader result = null;
if (args.length > 0)
{
Object arg = args[0];
if (arg instanceof ClassLoader)
{
result = cx.getApplicationClassLoader();
cx.setApplicationClassLoader((ClassLoader) arg);
}
}
// else
// {
// result = cx.getApplicationClassLoader();
//
// cx.setApplicationClassLoader(new JavaScriptClassLoader());
// }
return result;
}
/**
* setTimeout
*
* @param function
* The function to invoke once this timer completes
* @param timeout
* The amount of time to wait in milliseconds before firing the specified function
* @return int Returns a handle to this specific timeout instance. This handle can be used in clearTimeout to cancel
* the timer before it fires
*/
public int setTimeout(final Function function, final int timeout)
{
final JavaScriptGlobal self = this;
final int timeoutIndex = self._setTimeoutIndex++;
Thread thread = new Thread(new Runnable()
{
public void run()
{
boolean active = true;
try
{
// sleep
Thread.sleep(timeout);
// check if this was canceled while sleeping
synchronized (self._runningSetTimeouts)
{
active = self._runningSetTimeouts.containsKey(new Integer(timeoutIndex));
}
// call the associated function if this is still active
if (active)
{
// get display
final IWorkbench workbench = PlatformUI.getWorkbench();
if (workbench == null)
{
return;
}
Display display = workbench.getDisplay();
if (display == null || display.isDisposed())
{
return;
}
// execute callback in the correct thread
display.asyncExec(new Runnable()
{
public void run()
{
Scriptable scope = function.getParentScope();
Context cx = Context.enter();
function.call(cx, scope, scope, new Object[0]);
Context.exit();
self.clearTimeout(timeoutIndex);
}
});
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}, "Aptana Scripting JavaScript setTimeout"); //$NON-NLS-1$
// save reference to this thread to indicate that it should run. Note that clearTimeout will remove this entry
// and if that is done before the thread wakes, then the action in the thread will not execute
this._runningSetTimeouts.put(new Integer(timeoutIndex), thread);
thread.setDaemon(true);
// start the thread
thread.start();
// return the timeout index in case the thread wants to abort this timeout
return timeoutIndex;
}
}