| /******************************************************************************* |
| * Copyright (c) 2010 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.wst.jsdt.debug.internal.rhino.debugger; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.wst.jsdt.debug.internal.rhino.transport.JSONConstants; |
| import org.mozilla.javascript.BaseFunction; |
| import org.mozilla.javascript.Context; |
| import org.mozilla.javascript.EcmaError; |
| 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.DebuggableObject; |
| import org.mozilla.javascript.debug.Debugger; |
| |
| /** |
| * Rhino implementation of {@link DebugFrame} |
| * |
| * @since 1.0 |
| */ |
| public class StackFrame implements DebugFrame { |
| |
| private final Long id; |
| private final Context context; |
| private final ContextData contextData; |
| private final ScriptSource script; |
| private FunctionSource function = null; |
| 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 StackFrame(Long frameId, Context context, FunctionSource function, ScriptSource script) { |
| this.id = frameId; |
| this.context = context; |
| this.contextData = (ContextData) context.getDebuggerContextData(); |
| this.function = function; |
| this.script = script; |
| if(function != null) { |
| this.lineNumber = function.linenumber(); |
| } |
| else { |
| this.lineNumber = script.firstLine().intValue(); |
| } |
| } |
| |
| /** |
| * 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 ScriptSource 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(); |
| this.lineNumber = 1+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, this.script, new Integer(lineNumber), 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(); |
| this.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(); |
| this.contextData.exceptionThrown(ex); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.mozilla.javascript.debug.DebugFrame#onLineChange(org.mozilla.javascript.Context, int) |
| */ |
| public void onLineChange(Context cx, int lineNumber) { |
| if (this.lineNumber == lineNumber) |
| return; |
| initializeHandles(); |
| this.lineNumber = lineNumber; |
| Integer line = new Integer(this.lineNumber); |
| if(this.script.isValid(line, getFunctionName())) { |
| this.contextData.lineChange(this.script, line); |
| } |
| } |
| |
| /** |
| * @return the name of the function backing this frame or <code>null</code> if there is no function |
| * or it has no name. |
| */ |
| String getFunctionName() { |
| return (this.function == null ? null : this.function.name()); |
| } |
| |
| /** |
| * Evaluates the given source |
| * |
| * @param source |
| * @return |
| */ |
| public Object evaluate(String source) { |
| RhinoDebuggerImpl rhinoDebugger = (RhinoDebuggerImpl) context.getDebugger(); |
| rhinoDebugger.disableThread(); |
| |
| Context evalContext = context.getFactory().enter(); |
| 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); |
| } |
| catch (EcmaError ecma) { |
| return null; |
| } |
| 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) { |
| RhinoDebuggerImpl rhinoDebugger = (RhinoDebuggerImpl) context.getDebugger(); |
| rhinoDebugger.disableThread(); |
| |
| Context evalContext = context.getFactory().enter(); |
| 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); |
| |
| RhinoDebuggerImpl rhinoDebugger = (RhinoDebuggerImpl) context.getDebugger(); |
| rhinoDebugger.disableThread(); |
| |
| Context lookupContext = context.getFactory().enter(); |
| 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)); |
| //TODO update this |
| result.put(JSONConstants.SCOPE_NAME, null); |
| 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.REF, 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, createHandle(constructorFunction)); |
| result.put(JSONConstants.PROTOTYPE_OBJECT, createHandle(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); |
| Set properties = new HashSet(); |
| properties.add(createProperty(JSONConstants.THIS, thisObj)); |
| properties.addAll(createProperties(activation)); |
| Scriptable parent = activation.getParentScope(); |
| while(parent != null) { |
| properties.addAll(createProperties(parent)); |
| parent = parent.getParentScope(); |
| } |
| result.put(JSONConstants.PROPERTIES, properties); |
| } |
| |
| /** |
| * 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(); |
| if (scriptable instanceof DebuggableObject) { |
| HashSet arrayIds = new HashSet(Arrays.asList(ids)); |
| arrayIds.addAll(Arrays.asList(((DebuggableObject)scriptable).getAllIds())); |
| ids = arrayIds.toArray(); |
| } |
| 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(); |
| } |
| } |