// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.debug.core.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.chromium.debug.core.util.JsValueStringifier;
import org.chromium.sdk.FunctionScopeExtension;
import org.chromium.sdk.JsArray;
import org.chromium.sdk.JsEvaluateContext;
import org.chromium.sdk.JsFunction;
import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.debug.ui.IValueDetailListener;
/**
* A generic (non-array) implementation of IValue using a JsValue instance.
*/
public class Value extends ValueBase.ValueWithLazyVariables {
public static Value create(EvaluateContext evaluateContext, JsValue value,
ValueBase.ValueAsHostObject hostObject) {
if (JsValue.Type.TYPE_ARRAY == value.getType()) {
return new ArrayValue(evaluateContext, (JsArray) value, hostObject);
}
return new Value(evaluateContext, value, hostObject);
}
private final JsValue value;
private final ValueBase.ValueAsHostObject hostObject;
private final DetailBuilder detailBuilder = new DetailBuilder();
protected Value(EvaluateContext evaluateContext, JsValue value,
ValueBase.ValueAsHostObject hostObject) {
super(evaluateContext);
this.value = value;
this.hostObject = hostObject;
}
public String getReferenceTypeName() throws DebugException {
return value.getType().toString();
}
public String getValueString() {
String valueText = JsValueStringifier.toVisibleString(value);
if (value.asObject() != null) {
String ref = value.asObject().getRefId();
if (ref != null) {
valueText = valueText + " (id=" + ref + ")";
}
}
return valueText;
}
// This method could be blocking -- it gets called from a worker thread.
// All data should be prepared here.
protected IVariable[] calculateVariables() {
JsObject asObject = value.asObject();
if (asObject == null) {
return EMPTY_VARIABLES;
}
List<Variable> functionScopes = calculateFunctionScopesVariable(asObject);
return StackFrame.wrapVariables(getEvaluateContext(),
asObject.getProperties(), Collections.<String>emptySet(),
asObject.getInternalProperties(), hostObject, functionScopes);
}
/**
* Returns 'function scopes' node packed as a list or an empty list if the input is not
* a function. The 'function scopes' node holds actual scope variables as its children.
* @return list of 0 or 1 elements
*/
private List<Variable> calculateFunctionScopesVariable(JsObject jsObject) {
JsFunction asFunction = jsObject.asFunction();
if (asFunction == null) {
return null;
}
FunctionScopeExtension functionScopeExtension =
getConnectedData().getJavascriptVm().getFunctionScopeExtension();
if (functionScopeExtension == null) {
return null;
}
if (functionScopeExtension.getScopes(asFunction).isEmpty()) {
return null;
}
Variable functionScopesVariable =
Variable.forFunctionScopes(getEvaluateContext(), asFunction, functionScopeExtension);
if (functionScopesVariable == null) {
return null;
}
return Collections.singletonList(functionScopesVariable);
}
public boolean hasVariables() throws DebugException {
return value.asObject() != null;
}
public boolean isAllocated() throws DebugException {
return true;
}
@Override public Value asRealValue() {
return this;
}
public JsValue getJsValue() {
return value;
}
/**
* Called from Worker thread.
* @param listener will be called from various threads (its implementation is thread-safe)
*/
public void computeDetailAsync(IValueDetailListener listener) {
detailBuilder.buildDetailAsync(listener);
}
public boolean isTruncated() {
return this.value.isTruncated() || detailBuilder.getCurrentDetailWrapper().isTruncated();
}
protected ValueBase.ValueAsHostObject getHostObject() {
return hostObject;
}
public void reloadBiggerValue(final ReloadValueCallback callback) {
List<JsValue> jsValueList = new ArrayList<JsValue>(2);
if (this.value.isTruncated()) {
jsValueList.add(this.value);
}
JsValue detailValue = detailBuilder.getCurrentDetailWrapper().getJsValue();
if (detailValue != null && detailValue.isTruncated() && !jsValueList.contains(detailValue)) {
jsValueList.add(detailValue);
}
ReloadBiggerValueProcess process = new ReloadBiggerValueProcess(callback);
process.start(jsValueList);
}
public interface ReloadValueCallback {
void done(boolean changed);
}
/**
* An implementation of process that conducts reloading of several JsValue's.
* We need such class because technically the Value contains the actual value and
* a value of its detail (toString representation), both of which may have been truncated.
*/
private static class ReloadBiggerValueProcess {
private final ReloadValueCallback callback;
private int counter;
private boolean somethingChanged;
ReloadBiggerValueProcess(ReloadValueCallback callback) {
this.callback = callback;
}
void start(List<JsValue> jsValueList) {
if (jsValueList.isEmpty()) {
callback.done(false);
return;
}
this.counter = jsValueList.size();
this.somethingChanged = false;
for (final JsValue jsValue : jsValueList) {
final String originalValue = jsValue.getValueString();
jsValue.reloadHeavyValue(new JsValue.ReloadBiggerCallback() {
public void done() {
String newValueString = jsValue.getValueString();
boolean changed = newValueString != null && !newValueString.equals(originalValue);
boolean weAreLast;
boolean somethingChangedSaved;
synchronized (ReloadBiggerValueProcess.this) {
counter--;
somethingChanged |= changed;
somethingChangedSaved = somethingChanged;
weAreLast = counter == 0;
}
if (weAreLast) {
callback.done(somethingChangedSaved);
}
}
}, null);
}
}
}
/**
* A small abstraction over detail value. Internally the value may be a plain string
* or backed by a JsValue instance.
*/
private abstract static class DetailWrapper {
abstract boolean isTruncated();
abstract JsValue getJsValue();
abstract String getStringValue();
}
private static final DetailWrapper NO_DETAILS_WRAPPER = new DetailWrapper() {
boolean isTruncated() {
return false;
}
JsValue getJsValue() {
return null;
}
String getStringValue() {
return null;
}
};
/**
* Builds the string detail, possibly asynchronously. The details may be truncated
* and reloaded in full later.
*/
private class DetailBuilder {
private volatile DetailWrapper detailWrapper = NO_DETAILS_WRAPPER;
private static final String TO_STRING_ARGUMENT = "object";
private static final String TO_STRING_EXPRESSION = "String(" + TO_STRING_ARGUMENT + ")";
DetailWrapper getCurrentDetailWrapper() {
return detailWrapper;
}
void buildDetailAsync(final IValueDetailListener listener) {
DetailWrapper alreadyCalculatedDetail = this.detailWrapper;
if (alreadyCalculatedDetail != NO_DETAILS_WRAPPER) {
listener.detailComputed(Value.this, alreadyCalculatedDetail.getStringValue());
return;
}
JsObject jsObject = getJsValue().asObject();
if (jsObject == null) {
jsValueDetailIsBuilt(getJsValue(), listener);
return;
}
String objectRefId = jsObject.getRefId();
if (objectRefId == null) {
stringDetailIsBuilt("", listener);
return;
}
if (getSuspendedState().isDismissed()) {
stringDetailIsBuilt("", listener);
return;
}
Map<String, String> additionalContext =
Collections.singletonMap(TO_STRING_ARGUMENT, objectRefId);
JsEvaluateContext.EvaluateCallback evaluateCallback =
new JsEvaluateContext.EvaluateCallback() {
public void success(JsVariable variable) {
jsValueDetailIsBuilt(variable.getValue(), listener);
}
public void failure(String errorMessage) {
stringDetailIsBuilt(errorMessage, listener);
}
};
JsEvaluateContext evaluateContext =
getSuspendedState().getDebugContext().getGlobalEvaluateContext();
evaluateContext.evaluateAsync(TO_STRING_EXPRESSION, additionalContext,
evaluateCallback, null);
}
private void stringDetailIsBuilt(final String detailString, IValueDetailListener listener) {
DetailWrapper detailWrapper = new DetailWrapper() {
boolean isTruncated() {
return false;
}
String getStringValue() {
return detailString;
}
JsValue getJsValue() {
return null;
}
};
detailIsBuiltImpl(detailWrapper, listener);
}
private void jsValueDetailIsBuilt(final JsValue detailValue, IValueDetailListener listener) {
DetailWrapper detailWrapper = new DetailWrapper() {
boolean isTruncated() {
return detailValue.isTruncated();
}
String getStringValue() {
return detailValue.getValueString();
}
JsValue getJsValue() {
return detailValue;
}
};
detailIsBuiltImpl(detailWrapper, listener);
}
private void detailIsBuiltImpl(DetailWrapper detailWrapper, IValueDetailListener listener) {
// We may override value concurrently, but it's ok.
this.detailWrapper = detailWrapper;
listener.detailComputed(Value.this, detailWrapper.getStringValue());
}
}
}