// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
//
// 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.collide.client.code.debugging;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObject;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectSubType;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectType;
import com.google.collide.client.ui.tree.TreeNodeElement;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.SortedList;
import com.google.collide.shared.util.StringUtils;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
/**
* Represents a {@link RemoteObject} node in the tree UI.
*
*/
class RemoteObjectNode implements Comparable<RemoteObjectNode> {
private static final String GETTER_PROPERTY_PREFIX = "get ";
private static final String SETTER_PROPERTY_PREFIX = "set ";
private static final String PROTO_PROPERTY_NAME = "__proto__";
private static final RegExp CHUNK_FROM_BEGINNING = RegExp.compile("(^\\d+)|(^\\D+)");
private static final SortedList.Comparator<RemoteObjectNode> SORTING_FUNCTION =
new SortedList.Comparator<RemoteObjectNode>() {
@Override
public int compare(RemoteObjectNode a, RemoteObjectNode b) {
return a.compareTo(b);
}
};
private String name;
private final int orderIndex;
private final SortedList<RemoteObjectNode> children;
private final RemoteObject remoteObject;
private final boolean wasThrown;
private final boolean isDeletable;
private final boolean isWritable;
private final boolean isEnumerable;
private final String getterOrSetterName;
private final boolean isTransient;
private boolean shouldRequestChildren;
public static RemoteObjectNode createRoot() {
return new Builder("/")
.setHasChildren(true)
.setDeletable(false)
.build();
}
public static RemoteObjectNode createGetterProperty(String name, RemoteObject getterFunction) {
return new Builder(GETTER_PROPERTY_PREFIX + name, getterFunction)
.setGetterOrSetterName(name)
.build();
}
public static RemoteObjectNode createSetterProperty(String name, RemoteObject setterFunction) {
return new Builder(SETTER_PROPERTY_PREFIX + name, setterFunction)
.setGetterOrSetterName(name)
.build();
}
public static RemoteObjectNode createBeingEdited() {
return new Builder("")
.setOrderIndex(Integer.MAX_VALUE)
.build();
}
private static RemoteObjectNode createNoPropertiesPlaceholder() {
// TODO: i18n?
return new Builder("No Properties")
.setDeletable(false)
.setWritable(false)
.build();
}
private RemoteObjectNode(Builder builder) {
this.name = builder.name;
this.orderIndex = builder.orderIndex;
this.children = (builder.hasChildren && !builder.wasThrown) ?
new SortedList<RemoteObjectNode>(SORTING_FUNCTION) : null;
this.remoteObject = builder.remoteObject;
this.wasThrown = builder.wasThrown;
this.getterOrSetterName = builder.getterOrSetterName;
this.isTransient = builder.isTransient;
boolean isDeletable = builder.isDeletable;
boolean isWritable = builder.isWritable;
boolean isEnumerable = builder.isEnumerable;
if (PROTO_PROPERTY_NAME.equals(name)) {
// The __proto__ property can not be deleted, although can be changed.
isDeletable = false;
isEnumerable = false;
}
if (getterOrSetterName != null) {
// TODO: Maybe allow editing and/or deleting getters and setters?
isDeletable = false;
isWritable = false;
}
this.isDeletable = isDeletable;
this.isWritable = isWritable;
this.isEnumerable = isEnumerable;
this.shouldRequestChildren = (!wasThrown && remoteObject != null && remoteObject.hasChildren());
}
/**
* Tears down the object in order to prevent leaks. Do not use the object once
* this method is called.
*/
public void teardown() {
if (children != null) {
children.clear();
}
setParent(null);
setRenderedTreeNode(null);
}
public String getName() {
return name;
}
public void setName(String name) {
RemoteObjectNode parent = getParent();
if (parent != null) {
parent.removeChild(this);
}
this.name = name;
if (parent != null) {
parent.addChild(this);
}
}
public int getOrderIndex() {
return orderIndex;
}
public String getNodeId() {
return name + "#" + orderIndex;
}
public native final RemoteObjectNode getParent() /*-{
return this.__parentRef;
}-*/;
private native void setParent(RemoteObjectNode parent) /*-{
this.__parentRef = parent;
}-*/;
public boolean hasChildren() {
return children != null;
}
public boolean wasThrown() {
return wasThrown;
}
public boolean canAddRemoteObjectProperty() {
if (isTransient || remoteObject == null || !hasChildren()) {
return false;
}
RemoteObjectType type = remoteObject.getType();
RemoteObjectSubType subType = remoteObject.getSubType();
return type == RemoteObjectType.FUNCTION
|| (type == RemoteObjectType.OBJECT && subType != RemoteObjectSubType.NULL);
}
/**
* @return true if this property can be deleted from the parent object
*/
public boolean isDeletable() {
return isDeletable;
}
/**
* @return true if the value of this property can be changed
*/
public boolean isWritable() {
return isWritable;
}
public boolean isEnumerable() {
return isEnumerable;
}
/**
* @return true if the object represented by this node refers to an artificial
* transient remote object
* @see DebuggerApiTypes.Scope#isTransient
*/
public boolean isTransient() {
return isTransient;
}
public boolean shouldRequestChildren() {
return shouldRequestChildren && hasChildren();
}
public void setAllChildrenRequested() {
shouldRequestChildren = false;
}
public JsonArray<RemoteObjectNode> getChildren() {
if (children == null) {
return JsonCollections.createArray();
}
// Some remote objects may have no children. In this case we return a
// special "placeholder" child to display this fact in the UI.
if (children.size() == 0 && remoteObject != null && !shouldRequestChildren) {
addChild(createNoPropertiesPlaceholder());
}
return children.toArray(); // Returns copy.
}
public RemoteObject getRemoteObject() {
return remoteObject;
}
/**
* @return the associated rendered {@link TreeNodeElement}. If there is no
* tree node element rendered yet, then {@code null} is returned
*/
public final native TreeNodeElement<RemoteObjectNode> getRenderedTreeNode() /*-{
return this.__renderedNode;
}-*/;
/**
* Associates this RemoteObjectNode with the supplied {@link TreeNodeElement}
* as the rendered node in the tree. This allows us to go from model ->
* rendered tree element in order to reflect model mutations in the tree.
*/
public final native void setRenderedTreeNode(TreeNodeElement<RemoteObjectNode> renderedNode) /*-{
this.__renderedNode = renderedNode;
}-*/;
public boolean isRootChild() {
return getParent() != null && getParent().getParent() == null;
}
@Override
public int compareTo(RemoteObjectNode that) {
if (this == that) {
return 0;
}
int orderIndexDiff = this.orderIndex - that.orderIndex;
if (orderIndexDiff != 0) {
return orderIndexDiff;
}
String a = this.getName();
String b = that.getName();
if (PROTO_PROPERTY_NAME.equals(a)) {
return 1;
}
if (PROTO_PROPERTY_NAME.equals(b)) {
return -1;
}
// Sort by digits/non-digits chunks.
while (true) {
boolean emptyA = StringUtils.isNullOrEmpty(a);
boolean emptyB = StringUtils.isNullOrEmpty(b);
if (emptyA && emptyB) {
return 0;
}
if (emptyA && !emptyB) {
return -1;
}
if (emptyB && !emptyA) {
return 1;
}
MatchResult resultA = CHUNK_FROM_BEGINNING.exec(a);
MatchResult resultB = CHUNK_FROM_BEGINNING.exec(b);
String chunkA = resultA.getGroup(0);
String chunkB = resultB.getGroup(0);
boolean isNumA = !StringUtils.isNullOrEmpty(resultA.getGroup(1));
boolean isNumB = !StringUtils.isNullOrEmpty(resultB.getGroup(1));
if (isNumA && !isNumB) {
return -1;
}
if (isNumB && !isNumA) {
return 1;
}
if (isNumA && isNumB) {
// Must be parseDouble to handle big integers.
double valueA = Double.parseDouble(chunkA);
double valueB = Double.parseDouble(chunkB);
if (valueA != valueB) {
return valueA < valueB ? -1 : 1;
}
int diff = chunkA.length() - chunkB.length();
if (diff != 0) {
if (valueA == 0) {
// "file_0" should precede "file_00".
return diff;
} else {
// "file_015" should precede "file_15".
return -diff;
}
}
} else {
int diff = chunkA.compareTo(chunkB);
if (diff != 0) {
return diff;
}
}
a = a.substring(chunkA.length());
b = b.substring(chunkB.length());
}
}
public void addChild(RemoteObjectNode remoteObjectNode) {
assert (hasChildren()) : "Adding children to a leaf node is not allowed!";
remoteObjectNode.setParent(this);
children.add(remoteObjectNode);
}
public void removeChild(RemoteObjectNode remoteObjectNode) {
assert (hasChildren()) : "Removing a child from a leaf node?!";
children.remove(remoteObjectNode);
}
public RemoteObjectNode getFirstChildByName(String name) {
if (children == null) {
return null;
}
for (int i = 0, n = children.size(); i < n; ++i) {
RemoteObjectNode child = children.get(i);
if (name.equals(child.getName())) {
return child;
}
}
return null;
}
public RemoteObjectNode getLastChild() {
if (children == null || children.size() == 0) {
return null;
}
return children.get(children.size() - 1);
}
/**
* Builder class for the {@link RemoteObjectNode}.
*/
public static class Builder {
private final String name;
private final RemoteObject remoteObject;
private int orderIndex;
private boolean hasChildren;
private boolean wasThrown;
private boolean isDeletable = true;
private boolean isWritable = true;
private boolean isEnumerable = true;
private boolean isTransient;
private String getterOrSetterName;
public Builder(String name) {
this(name, null);
}
public Builder(String name, RemoteObject remoteObject) {
this(name, remoteObject, null);
}
public Builder(String name, RemoteObject remoteObject, RemoteObjectNode proto) {
this.name = name;
this.remoteObject = remoteObject;
this.hasChildren = (remoteObject != null && remoteObject.hasChildren());
if (proto != null) {
this.orderIndex = proto.orderIndex;
this.wasThrown = proto.wasThrown;
this.isDeletable = proto.isDeletable;
this.isWritable = proto.isWritable;
this.isEnumerable = proto.isEnumerable;
this.isTransient = proto.isTransient;
this.getterOrSetterName = proto.getterOrSetterName;
}
}
public Builder setOrderIndex(int orderIndex) {
this.orderIndex = orderIndex;
return this;
}
public Builder setHasChildren(boolean hasChildren) {
this.hasChildren = hasChildren;
return this;
}
public Builder setWasThrown(boolean wasThrown) {
this.wasThrown = wasThrown;
return this;
}
public Builder setDeletable(boolean deletable) {
isDeletable = deletable;
return this;
}
public Builder setWritable(boolean writable) {
isWritable = writable;
return this;
}
public Builder setEnumerable(boolean enumerable) {
isEnumerable = enumerable;
return this;
}
public Builder setTransient(boolean aTransient) {
this.isTransient = aTransient;
return this;
}
private Builder setGetterOrSetterName(String name) {
getterOrSetterName = name;
return this;
}
public RemoteObjectNode build() {
return new RemoteObjectNode(this);
}
}
}