/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.debug.core.webkit;
import com.google.dart.tools.debug.core.DartDebugCorePlugin;
import com.google.dart.tools.debug.core.webkit.WebkitConnection.Callback;
import com.google.dart.tools.debug.core.webkit.WebkitConnection.NotificationHandler;
import com.google.dart.tools.debug.core.webkit.WebkitNode.WebkitAttribute;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
// TODO: additional DOM functionality we could expose:
// moveTo requestNode resolveNode setAttributesAsText setNodeName
// TODO: we need to track all DOM nodeIds returned to us; the node info is only sent once
// TODO: create a DomDocument model object. It will be in charge of populating itself when queried.
// It will fire events when it is changed (from notifications from the browser). It will probably
// also expose some manipulation methods that will call through to the WebkitDom class.
/**
* A WIP DOM domain object.
* <p>
* This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror
* object that has an id. This id can be used to get additional information on the Node, resolve it
* into the JavaScript object wrapper, etc. It is important that client receives DOM events only for
* the nodes that are known to the client. Backend keeps track of the nodes that were sent to the
* client and never sends the same node twice. It is client's responsibility to collect information
* about the nodes that were sent to the client.
* <p>
* Note that iframe owner elements will return corresponding document elements as their child nodes.
*/
public class WebkitDom extends WebkitDomain {
public static interface DomListener {
/**
* Fired when Document has been totally updated. Node ids are no longer valid.
*/
public void documentUpdated();
}
public static class HighlightConfig {
/**
* The border highlight fill color (default: transparent).
*/
public RGBA borderColor;
/**
* The content box highlight fill color (default: transparent).
*/
public RGBA contentColor;
/**
* The margin highlight fill color (default: transparent).
*/
public RGBA marginColor;
/**
* The padding highlight fill color (default: transparent).
*/
public RGBA paddingColor;
/**
* Whether the node info tooltip should be shown (default: false).
*/
public Boolean showInfo;
public HighlightConfig() {
}
public JSONObject toJSONObject() throws JSONException {
JSONObject obj = new JSONObject();
if (borderColor != null) {
obj.put("borderColor", borderColor.toJSONObject());
}
if (contentColor != null) {
obj.put("contentColor", contentColor.toJSONObject());
}
if (marginColor != null) {
obj.put("marginColor", marginColor.toJSONObject());
}
if (paddingColor != null) {
obj.put("paddingColor", paddingColor.toJSONObject());
}
if (showInfo != null) {
obj.put("showInfo", showInfo.booleanValue());
}
return obj;
}
}
public static interface InspectorListener {
public void detached(String reason);
public void targetCrashed();
}
public static class RGBA {
/**
* The alpha component, in the [0-1] range (default: 1).
*/
public Double a;
/**
* The red component, in the [0-255] range.
*/
public int r;
/**
* The green component, in the [0-255] range.
*/
public int g;
/**
* The blue component, in the [0-255] range.
*/
public int b;
public RGBA(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
public RGBA(int r, int g, int b, double a) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
public JSONObject toJSONObject() throws JSONException {
JSONObject obj = new JSONObject();
if (a != null) {
obj.put("a", a);
}
obj.put("r", r);
obj.put("g", g);
obj.put("b", b);
return obj;
}
}
public static HighlightConfig DEFAULT_HIGHLIGHT;
static {
DEFAULT_HIGHLIGHT = new HighlightConfig();
DEFAULT_HIGHLIGHT.borderColor = new RGBA(0, 0, 0);
DEFAULT_HIGHLIGHT.contentColor = new RGBA(255, 255, 204, 0.4d);
DEFAULT_HIGHLIGHT.showInfo = true;
}
private final static String DOM_DOCUMENT_UPDATED = "DOM.documentUpdated";
private static final String INSPECTOR_DETACHED = "Inspector.detached";
private static final String INSPECTOR_TARGET_CRASHED = "Inspector.targetCrashed";
private List<DomListener> domListeners = new ArrayList<DomListener>();
private List<InspectorListener> inspectorListeners = new ArrayList<InspectorListener>();
public WebkitDom(WebkitConnection connection) {
super(connection);
connection.registerNotificationHandler("DOM.", new NotificationHandler() {
@Override
public void handleNotification(String method, JSONObject params) throws JSONException {
handleDOMNotification(method, params);
}
});
connection.registerNotificationHandler("Inspector.", new NotificationHandler() {
@Override
public void handleNotification(String method, JSONObject params) throws JSONException {
handleInspectorNotification(method, params);
}
});
}
public void addDomListener(DomListener listener) {
domListeners.add(listener);
}
public void addInspectorListener(InspectorListener listener) {
inspectorListeners.add(listener);
}
/**
* Focuses the given element.
*
* @param nodeId Id of the node to focus.
* @throws IOException
*/
@WebkitUnsupported
public void focus(int nodeId) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.focus");
request.put("params", new JSONObject().put("nodeId", nodeId));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Returns attributes for the specified node.
*
* @param nodeId Id of the node to retrieve attributes for.
* @param callback
* @throws IOException
*/
public void getAttributes(int nodeId, final WebkitCallback<List<WebkitAttribute>> callback)
throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.getAttributes");
request.put("params", new JSONObject().put("nodeId", nodeId));
connection.sendRequest(request, new Callback() {
@Override
public void handleResult(JSONObject result) throws JSONException {
callback.handleResult(convertGetAttributesResult(result));
}
});
} catch (JSONException exception) {
throw new IOException(exception);
}
}
public void getDocument(final WebkitCallback<WebkitNode> callback) throws IOException {
sendSimpleCommand("DOM.getDocument", new Callback() {
@Override
public void handleResult(JSONObject result) throws JSONException {
callback.handleResult(convertGetDocumentResult(result));
}
});
}
public WebkitNode getDocumentSync() throws IOException {
@SuppressWarnings("unchecked")
final WebkitResult<WebkitNode>[] result = new WebkitResult[1];
final CountDownLatch latch = new CountDownLatch(1);
getDocument(new WebkitCallback<WebkitNode>() {
@Override
public void handleResult(WebkitResult<WebkitNode> r) {
result[0] = r;
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
}
if (result[0].isError()) {
throw new IOException(result[0].getErrorMessage());
} else {
return result[0].getResult();
}
}
public void getOuterHtml(int nodeId, final WebkitCallback<String> callback) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.getOuterHtml");
request.put("params", new JSONObject().put("nodeId", nodeId));
connection.sendRequest(request, new Callback() {
@Override
public void handleResult(JSONObject result) throws JSONException {
callback.handleResult(convertGetOuterHtmlResult(result));
}
});
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Hides DOM node highlight.
*
* @throws IOException
*/
public void hideHighlight() throws IOException {
sendSimpleCommand("DOM.hideHighlight");
}
/**
* Highlights DOM node with given id.
*
* @param nodeId
* @param highlightConfig
*/
public void highlightNode(int nodeId, HighlightConfig highlightConfig) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.highlightNode");
request.put(
"params",
new JSONObject().put("nodeId", nodeId).put(
"highlightConfig",
highlightConfig.toJSONObject()));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.
*
* @param x
* @param y
* @param width
* @param height
* @param color
* @param outlineColor
* @throws IOException
*/
public void highlightRect(int x, int y, int width, int height, RGBA color, RGBA outlineColor)
throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.highlightRect");
request.put(
"params",
new JSONObject().put("x", x).put("y", y).put("width", width).put("height", height).put(
"color",
color.toJSONObject()).put("outlineColor", outlineColor.toJSONObject()));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Executes querySelector on a given node.
*
* @param nodeId
* @param selector
* @param callback
* @throws IOException
*/
public void querySelector(int nodeId, String selector, final WebkitCallback<Integer> callback)
throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.querySelector");
request.put("params", new JSONObject().put("nodeId", nodeId).put("selector", selector));
connection.sendRequest(request, new Callback() {
@Override
public void handleResult(JSONObject result) throws JSONException {
callback.handleResult(convertQuerySelectorResult(result));
}
});
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Executes querySelectorAll on a given node.
*
* @param nodeId
* @param selector
* @param callback
* @throws IOException
*/
public void querySelectorAll(int nodeId, String selector,
final WebkitCallback<List<Integer>> callback) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.querySelectorAll");
request.put("params", new JSONObject().put("nodeId", nodeId).put("selector", selector));
connection.sendRequest(request, new Callback() {
@Override
public void handleResult(JSONObject result) throws JSONException {
callback.handleResult(convertQuerySelectorAllResult(result));
}
});
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Removes attribute with given name from an element with given id.
*
* @param nodeId Id of the element to remove attribute from.
* @param name Name of the attribute to remove.
* @throws IOException
*/
public void removeAttribute(int nodeId, String name) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.removeAttribute");
request.put("params", new JSONObject().put("nodeId", nodeId).put("name", name));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
public void removeDomListener(DomListener listener) {
domListeners.remove(listener);
}
public void removeInspectorListener(InspectorListener listener) {
inspectorListeners.remove(listener);
}
/**
* Removes node with given id.
*
* @param nodeId
* @throws IOException
*/
public void removeNode(int nodeId) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.removeNode");
request.put("params", new JSONObject().put("nodeId", nodeId));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Requests that children of the node with given id are returned to the caller in form of
* setChildNodes events.
*
* @param nodeId Id of the node to get children for
* @throws IOException
*/
public void requestChildNodes(int nodeId) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.requestChildNodes");
request.put("params", new JSONObject().put("nodeId", nodeId));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Sets attribute for an element with given id.
*
* @param nodeId
* @param name
* @param value
* @throws IOException
*/
public void setAttributeValue(int nodeId, String name, String value) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.setAttributeValue");
request.put(
"params",
new JSONObject().put("nodeId", nodeId).put("name", name).put("value", value));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Enters the 'inspect' mode. In this mode, elements that user is hovering over are highlighted.
* Backend then generates 'inspect' command upon element selection.
*
* @param enabled true to enable inspection mode, false to disable it
* @param highlightConfig a descriptor for the highlight appearance of hovered-over nodes. May be
* omitted if <code>enabled == false</code>
* @throws IOException
*/
@WebkitUnsupported
public void setInspectModeEnabled(boolean enabled, HighlightConfig highlightConfig)
throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.setInspectModeEnabled");
request.put(
"params",
new JSONObject().put("enabled", enabled).put(
"highlightConfig",
highlightConfig.toJSONObject()));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Sets node value for a node with given id.
*
* @param nodeId Id of the node to set value for.
* @param value New node's value.
* @throws IOException
*/
public void setNodeValue(int nodeId, String value) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.setNodeValue");
request.put("params", new JSONObject().put("nodeId", nodeId).put("value", value));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
/**
* Sets node HTML markup, returns new node id.
*
* @param nodeId Id of the node to set value for.
* @param outerHTML Outer HTML markup to set.
* @throws IOException
*/
public void setOuterHTML(int nodeId, String outerHTML) throws IOException {
try {
JSONObject request = new JSONObject();
request.put("method", "DOM.setOuterHTML");
request.put("params", new JSONObject().put("nodeId", nodeId).put("outerHTML", outerHTML));
connection.sendRequest(request);
} catch (JSONException exception) {
throw new IOException(exception);
}
}
protected void handleDOMNotification(String method, JSONObject params) {
if (method.equals(DOM_DOCUMENT_UPDATED)) {
for (DomListener listener : domListeners) {
listener.documentUpdated();
}
} else {
DartDebugCorePlugin.logInfo("unhandled notification: " + method);
}
}
protected void handleInspectorNotification(String method, JSONObject params) {
if (method.equals(INSPECTOR_DETACHED)) {
// {"method":"Inspector.detached","params":{"reason":"target_closed"}}
String reason = params == null ? "" : params.optString("reason", null);
for (InspectorListener listener : inspectorListeners) {
listener.detached(reason);
}
} else if (method.equals(INSPECTOR_TARGET_CRASHED)) {
for (InspectorListener listener : inspectorListeners) {
listener.targetCrashed();
}
} else {
DartDebugCorePlugin.logInfo("unhandled notification: " + method);
}
}
private WebkitResult<List<WebkitAttribute>> convertGetAttributesResult(JSONObject object)
throws JSONException {
WebkitResult<List<WebkitAttribute>> result = WebkitResult.createFrom(object);
if (object.has("result")) {
JSONArray arr = object.getJSONObject("result").getJSONArray("attributes");
List<WebkitAttribute> attributes = WebkitAttribute.createFrom(arr);
result.setResult(attributes);
}
return result;
}
private WebkitResult<WebkitNode> convertGetDocumentResult(JSONObject object) throws JSONException {
WebkitResult<WebkitNode> result = WebkitResult.createFrom(object);
if (object.has("result")) {
JSONObject rootNode = object.getJSONObject("result").getJSONObject("root");
result.setResult(WebkitNode.createFrom(rootNode));
}
return result;
}
private WebkitResult<String> convertGetOuterHtmlResult(JSONObject object) throws JSONException {
WebkitResult<String> result = WebkitResult.createFrom(object);
if (object.has("result")) {
String html = object.getJSONObject("result").getString("outerHTML");
result.setResult(html);
}
return result;
}
private WebkitResult<List<Integer>> convertQuerySelectorAllResult(JSONObject object)
throws JSONException {
WebkitResult<List<Integer>> result = WebkitResult.createFrom(object);
if (object.has("result")) {
JSONArray arr = object.getJSONObject("result").getJSONArray("nodeIds");
List<Integer> idArray = new ArrayList<Integer>(arr.length());
for (int i = 0; i < arr.length(); i++) {
idArray.add(arr.getInt(i));
}
result.setResult(idArray);
}
return result;
}
private WebkitResult<Integer> convertQuerySelectorResult(JSONObject object) throws JSONException {
WebkitResult<Integer> result = WebkitResult.createFrom(object);
if (object.has("result")) {
int nodeId = object.getJSONObject("result").getInt("nodeId");
result.setResult(nodeId);
}
return result;
}
}