| /******************************************************************************* |
| * 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(); |
| } |
| } |