blob: d9437ebf3d768ffd8e5ac469d4f09f6785d8ea77 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009 IBM Corporation and others 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
*
* Contributors: IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.e4.languages.javascript.debug.rhino;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.e4.languages.javascript.debug.connect.JSONConstants;
import org.eclipse.e4.languages.javascript.debug.rhino.ScriptImpl.ScriptRecord;
import org.mozilla.javascript.BaseFunction;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.debug.DebugFrame;
import org.mozilla.javascript.debug.DebuggableScript;
import org.mozilla.javascript.debug.Debugger;
/**
* Rhino implementation of {@link DebugFrame}
*
* @since 1.0
*/
public class DebugFrameImpl implements DebugFrame {
private final Long id;
private final Context context;
private final ContextData contextData;
private final DebuggableScript debuggableScript;
private final ScriptImpl script;
private final ScriptRecord record;
private final HashMap handles = new HashMap();
private final IdentityHashMap handledObjects = new IdentityHashMap();
private Scriptable activation;
private Scriptable thisObj;
private int lineNumber;
/**
* Constructor
*
* @param frameId
* @param context
* @param debuggableScript
* @param script
*/
public DebugFrameImpl(Long frameId, Context context, DebuggableScript debuggableScript, ScriptImpl script) {
this.id = frameId;
this.context = context;
this.contextData = (ContextData) context.getDebuggerContextData();
this.debuggableScript = debuggableScript;
this.script = script;
this.record = script.getRecord(debuggableScript);
this.lineNumber = record.firstLine;
}
/**
* Returns the id of the frame
*
* @return the frame id
*/
public Long getId() {
return id;
}
/**
* Returns the underlying {@link Script}
*
* @return the underlying {@link Script}
*/
public ScriptImpl getScript() {
return script;
}
/**
* Returns the line number for the frame
*
* @return the frame line number
*/
public Integer getLineNumber() {
return new Integer(lineNumber);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.DebugFrame#onDebuggerStatement(org.mozilla.javascript.Context)
*/
public void onDebuggerStatement(Context cx) {
initializeHandles();
lineNumber++;
contextData.debuggerStatement(script, new Integer(lineNumber));
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.DebugFrame#onEnter(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, org.mozilla.javascript.Scriptable, java.lang.Object[])
*/
public void onEnter(Context cx, Scriptable activation, Scriptable thisObj, Object[] args) {
this.activation = activation;
this.thisObj = thisObj;
initializeHandles();
contextData.pushFrame(this, script, new Integer(lineNumber), debuggableScript.getFunctionName());
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.DebugFrame#onExit(org.mozilla.javascript.Context, boolean, java.lang.Object)
*/
public void onExit(Context cx, boolean byThrow, Object resultOrException) {
this.activation = null;
this.thisObj = null;
clearHandles();
contextData.popFrame(byThrow, resultOrException);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.DebugFrame#onExceptionThrown(org.mozilla.javascript.Context, java.lang.Throwable)
*/
public void onExceptionThrown(Context cx, Throwable ex) {
initializeHandles();
contextData.exceptionThrown(ex);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.DebugFrame#onLineChange(org.mozilla.javascript.Context, int)
*/
public void onLineChange(Context cx, int lineNumber) {
initializeHandles();
this.lineNumber = 1 + lineNumber;
contextData.lineChange(script, new Integer(this.lineNumber));
}
/**
* Evaluates the given source
*
* @param source
* @return
*/
public Object evaluate(String source) {
RhinoDebugger rhinoDebugger = (RhinoDebugger) context.getDebugger();
rhinoDebugger.disableThread();
Context evalContext = context.getFactory().enterContext();
Debugger debugger = evalContext.getDebugger();
Object debuggerContextData = evalContext.getDebuggerContextData();
evalContext.setDebugger(null, null);
try {
Object result = ScriptRuntime.evalSpecial(evalContext, activation, thisObj, new Object[] { source }, "eval", 0); //$NON-NLS-1$
Long handle = createHandle(result);
return serialize(handle, result);
} finally {
evalContext.setDebugger(debugger, debuggerContextData);
Context.exit();
rhinoDebugger.enableThread();
}
}
/**
* Evaluates the given condition
*
* @param condition
* @return the status of the condition evaluation
*/
public boolean evaluateCondition(String condition) {
RhinoDebugger rhinoDebugger = (RhinoDebugger) context.getDebugger();
rhinoDebugger.disableThread();
Context evalContext = context.getFactory().enterContext();
Debugger debugger = evalContext.getDebugger();
Object debuggerContextData = evalContext.getDebuggerContextData();
evalContext.setDebugger(null, null);
try {
Object result = ScriptRuntime.evalSpecial(evalContext, activation, thisObj, new Object[] { condition }, JSONConstants.CONDITION, 0);
return ScriptRuntime.toBoolean(result);
} finally {
evalContext.setDebugger(debugger, debuggerContextData);
Context.exit();
rhinoDebugger.enableThread();
}
}
/**
* Look up the given handle in the known list of handles for this frame
*
* @param handle
* @return the serialized handle never <code>null</code>
*/
public Object lookup(Long handle) {
Object result = handles.get(handle);
RhinoDebugger rhinoDebugger = (RhinoDebugger) context.getDebugger();
rhinoDebugger.disableThread();
Context lookupContext = context.getFactory().enterContext();
Debugger debugger = lookupContext.getDebugger();
Object debuggerContextData = lookupContext.getDebuggerContextData();
lookupContext.setDebugger(null, null);
try {
return serialize(handle, result);
} finally {
lookupContext.setDebugger(debugger, debuggerContextData);
Context.exit();
rhinoDebugger.enableThread();
}
}
/**
* Returns a JSON map
*
* @return a new JSON object
*/
public Object toJSON() {
Map result = new HashMap();
result.put(JSONConstants.THREAD_ID, contextData.getThreadId());
result.put(JSONConstants.CONTEXT_ID, contextData.getId());
result.put(JSONConstants.FRAME_ID, id);
result.put(JSONConstants.SCRIPT_ID, script.getId());
result.put(JSONConstants.LINE, new Integer(lineNumber));
result.put(JSONConstants.REF, new Integer(0));
result.put(JSONConstants.SCOPE_NAME, record.name);
return result;
}
/**
* Serializes a handle object for this frame
*
* @param handle
* @param object
* @return the serialized handle, never <code>null</code>
*/
public Object serialize(Long handle, Object object) {
Map result = new HashMap();
result.put(JSONConstants.HANDLE, handle);
// "undefined", "null", "boolean", "number", "string", "object", "function" or "frame"
if (object == Undefined.instance) {
serializeUndefined(result);
} else if (object == null) {
serializeNull(result);
} else if (object instanceof Boolean) {
serializeSimpleType(object, JSONConstants.BOOLEAN, result);
} else if (object instanceof Number) {
Object value = (object == ScriptRuntime.NaNobj) ? null : object;
serializeSimpleType(value, JSONConstants.NUMBER, result);
} else if (object instanceof String) {
serializeSimpleType(object, JSONConstants.STRING, result);
} else if (object instanceof Scriptable) {
Scriptable scriptable = (Scriptable) object;
serializeFunctionOrObject(scriptable, result);
} else if (object == this) {
serializeFrame(result);
} else {
serializeUndefined(result);
}
return result;
}
/**
* Serialize the undefined value
*
* @param result
* @see JSONConstants#UNDEFINED
*/
private void serializeUndefined(Map result) {
result.put(JSONConstants.TYPE, JSONConstants.UNDEFINED);
}
/**
* Serialize the null value
*
* @param result
* @see JSONConstants#NULL
*/
private void serializeNull(Map result) {
result.put(JSONConstants.TYPE, JSONConstants.NULL);
}
/**
* Serialize the given simple type
*
* @param object
* @param type
* @param result
*/
private void serializeSimpleType(Object object, String type, Map result) {
result.put(JSONConstants.TYPE, type);
result.put(JSONConstants.VALUE, object);
}
/**
* Serialize a function or object
*
* @param scriptable
* @param result
* @see JSONConstants#FUNCTION
* @see JSONConstants#OBJECT
*/
private void serializeFunctionOrObject(Scriptable scriptable, Map result) {
if (scriptable instanceof BaseFunction) {
result.put(JSONConstants.TYPE, JSONConstants.FUNCTION);
result.put(JSONConstants.NAME, ((BaseFunction) scriptable).getFunctionName());
} else if (scriptable instanceof NativeArray) {
result.put(JSONConstants.TYPE, JSONConstants.ARRAY);
} else {
result.put(JSONConstants.TYPE, JSONConstants.OBJECT);
}
result.put(JSONConstants.CLASS_NAME, scriptable.getClassName());
Object constructorFunction = null;
if (ScriptableObject.hasProperty(scriptable, JSONConstants.CONSTRUCTOR)) {
constructorFunction = ScriptableObject.getProperty(scriptable, JSONConstants.CONSTRUCTOR);
}
result.put(JSONConstants.CONSTRUCTOR_FUNCTION, createRef(constructorFunction));
Scriptable protoObject = findProtoObject(scriptable);
result.put(JSONConstants.PROTO_OBJECT, createRef(protoObject));
result.put(JSONConstants.PROTOTYPE_OBJECT, createRef(scriptable.getPrototype()));
if (scriptable instanceof NativeJavaObject)
result.put(JSONConstants.PROPERTIES, createJavaObjectProperties((NativeJavaObject) scriptable));
else
result.put(JSONConstants.PROPERTIES, createProperties(scriptable));
}
/**
* @param javaObject
* @return
*/
private Object createJavaObjectProperties(NativeJavaObject javaObject) {
ArrayList properties = new ArrayList();
// TODO: The problem here is Rhino treats getters and setters differently and in some cases will call these methods
// we need to sort out what's reasonable to display without modifying state
return properties;
}
/**
* Serialize a frame
*
* @param result
* @see JSONConstants#FRAME
* @see JSONConstants#THIS
*/
private void serializeFrame(Map result) {
result.put(JSONConstants.TYPE, JSONConstants.FRAME);
List properties = createProperties(activation);
properties.add(createProperty(JSONConstants.THIS, thisObj));
result.put(JSONConstants.PROPERTIES, properties);
}
/**
* Finds the {@link Scriptable} object from the objects' prototype. Walks the parent hierarchy to find the first non-null {@link Scriptable} object
*
* @param scriptable
* @return the first non-null {@link Scriptable} object
*/
private Scriptable findProtoObject(Scriptable scriptable) {
// TODO: cache this more sensibly
Scriptable protoObject = scriptable.getPrototype();
while (protoObject != null) {
Scriptable parent = protoObject.getPrototype();
if (parent == null || parent.equals(protoObject)) {
break;
}
protoObject = parent;
}
return protoObject;
}
/**
* Creates the list of properties from the given {@link Scriptable}
*
* @param scriptable
* @return the live list of properties from the given {@link Scriptable}
*/
private List createProperties(Scriptable scriptable) {
ArrayList properties = new ArrayList();
Object[] ids = scriptable.getIds();
for (int i = 0; i < ids.length; i++) {
Object id = ids[i];
Object value = null;
if (id instanceof String) {
value = ScriptableObject.getProperty(scriptable, (String) id);
} else if (id instanceof Number) {
value = ScriptableObject.getProperty(scriptable, ((Number) id).intValue());
} else
continue;
Map property = createProperty(id, value);
properties.add(property);
}
return properties;
}
/**
* Create a new property map for the given id and value
*
* @param id
* @param value
* @return a new property map
* @see JSONConstants#NAME
*/
private Map createProperty(Object id, Object value) {
Map property = createRef(value);
property.put(JSONConstants.NAME, id);
return property;
}
/**
* Create a new ref map for the given object
*
* @param object
* @return a new ref map
* @see JSONConstants#REF
*/
private Map createRef(Object object) {
Map map = new HashMap(2);
map.put(JSONConstants.REF, createHandle(object));
return map;
}
/**
* Clears all cached handles from this frame
*/
private void clearHandles() {
handles.clear();
handledObjects.clear();
}
/**
* Initializes the set of handles
*/
private void initializeHandles() {
if (handles.size() != 1) {
clearHandles();
createHandle(this);
}
}
/**
* Creates a new handle for the given object and caches it
*
* @param object
* @return the id of the new handle
*/
private Long createHandle(Object object) {
Long handle = (Long) handledObjects.get(object);
if (handle == null) {
handle = new Long(nextHandle());
handles.put(handle, object);
handledObjects.put(object, handle);
}
return handle;
}
/**
* @return the next handle to use when creating handles
*/
private int nextHandle() {
return handles.size();
}
/**
* @return the thread id for the underlying {@link ContextData}
*/
public Object getThreadId() {
return contextData.getThreadId();
}
}