// Copyright (c) 2010 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.ui.actions.pinpoint;
import static org.chromium.debug.ui.DialogUtils.addModifyListener;
import static org.chromium.debug.ui.DialogUtils.createConstant;
import static org.chromium.debug.ui.DialogUtils.createErrorOptional;
import static org.chromium.debug.ui.DialogUtils.createOptional;
import static org.chromium.debug.ui.DialogUtils.createProcessor;
import static org.chromium.debug.ui.DialogUtils.handleErrors;
import static org.chromium.debug.ui.DialogUtils.mergeBranchVariables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.chromium.debug.core.model.Value;
import org.chromium.debug.ui.DialogUtils;
import org.chromium.debug.ui.DialogUtils.BranchVariableGetter;
import org.chromium.debug.ui.DialogUtils.Gettable;
import org.chromium.debug.ui.DialogUtils.Message;
import org.chromium.debug.ui.DialogUtils.MessagePriority;
import org.chromium.debug.ui.DialogUtils.NormalExpression;
import org.chromium.debug.ui.DialogUtils.OkButtonControl;
import org.chromium.debug.ui.DialogUtils.Optional;
import org.chromium.debug.ui.DialogUtils.Scope;
import org.chromium.debug.ui.DialogUtils.ScopeEnabler;
import org.chromium.debug.ui.DialogUtils.Switcher;
import org.chromium.debug.ui.DialogUtils.Updater;
import org.chromium.debug.ui.DialogUtils.ValueConsumer;
import org.chromium.debug.ui.DialogUtils.ValueProcessor;
import org.chromium.debug.ui.DialogUtils.ValueSource;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.JsEvaluateContext;
import org.chromium.sdk.JsEvaluateContext.EvaluateCallback;
import org.chromium.sdk.JsEvaluateContext.ResultOrException;
import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IExpressionManager;
import org.eclipse.debug.core.model.IWatchExpression;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
/**
* Dialog window logic implementation.
*/
class DialogLogic {
/**
* API to logic for dialog window disposals.
*/
interface Handle {
// Initialization after everything has been built.
void updateAll();
Runnable getOkRunnable();
void saveStateInStore();
}
/**
* Builds an updater-based dialog logic. This is more a function-style programming inside
* (to better deal with various states and transitions of dialog).
*/
static Handle buildDialogLogic(final DialogImpl.Elements elements,
final DialogImpl.DialogPreferencesStore dialogPreferencesStore, final Value uiValue) {
final Updater updater = new Updater();
Scope rootScope = updater.rootScope();
// A global dialog warning collection (those warnings that are not tied to the primary
// result value).
List<ValueSource<String>> warningSources = new ArrayList<ValueSource<String>>(2);
// 'Property expression' editor raw value.
final ValueSource<String> propertyExpressionEditorValue = new ValueSource<String>() {
private final Text textElement = elements.getExpressionText();
{
String text = dialogPreferencesStore.getExpressionText();
textElement.setText(text);
// Select all expression but a first dot.
int selectionStart = text.startsWith(".") ? 1 : 0; //$NON-NLS-1$
textElement.setSelection(selectionStart, text.length());
addModifyListener(textElement, this, updater);
}
public String getValue() {
return textElement.getText();
}
};
updater.addSource(rootScope, propertyExpressionEditorValue);
// A preview context value. It is constant but optional (so it's passed via updater).
final ValueSource<Optional<DialogLogic.PreviewContext>> evaluatorValue =
createConstant(PreviewContext.build(uiValue), updater);
// Property expression parsed as Expression. Parse errors are kept in Optional.
final ValueProcessor<Optional<DialogLogic.Expression>> parsedPropertyExpressionValue =
createProcessor(new Gettable<Optional<DialogLogic.Expression>>() {
@Override
public Optional<DialogLogic.Expression> getValue() {
return parseExpression(propertyExpressionEditorValue.getValue());
}
});
updater.addConsumer(rootScope, parsedPropertyExpressionValue);
updater.addSource(rootScope, parsedPropertyExpressionValue);
updater.addDependency(parsedPropertyExpressionValue, propertyExpressionEditorValue);
// 'Show preview' check box value.
final ValueSource<Boolean> previewCheckBoxValue = new ValueSource<Boolean>() {
private final Button checkBox = elements.getPreviewCheckBox();
{
checkBox.setSelection(dialogPreferencesStore.getPreviewCheck());
addModifyListener(checkBox, this, updater);
}
@Override
public Boolean getValue() {
return checkBox.getSelection();
}
};
// A conditional block that holds optional preview section.
Switcher<Boolean> checkerSwitch = rootScope.addSwitch(previewCheckBoxValue);
PreviewSwitchOutput switchBlockOutput = fillShowPreviewSwitch(checkerSwitch, updater,
rootScope, elements, evaluatorValue, parsedPropertyExpressionValue);
// Preview block may emit warning.
warningSources.add(switchBlockOutput.warningSource());
// 'Add watch expression' check box value.
final ValueSource<Boolean> addWatchExpressionValue = new ValueSource<Boolean>() {
private final Button checkBox = elements.getAddWatchCheckBox();
{
checkBox.setSelection(dialogPreferencesStore.getAddWatchExpression());
addModifyListener(checkBox, this, updater);
}
@Override
public Boolean getValue() {
return checkBox.getSelection();
}
};
updater.addSource(rootScope, addWatchExpressionValue);
// An OK button implementation that do set property in remote VM.
final ValueProcessor<Optional<? extends Runnable>> okRunnable = createProcessor(handleErrors(
new NormalExpression<Runnable>() {
@Calculate
public Runnable calculate(DialogLogic.PreviewContext previewContext,
DialogLogic.Expression expression) {
return new OkRunnable(elements.getParentShell(), expression, previewContext,
addWatchExpressionValue.getValue());
}
@DependencyGetter
public ValueSource<Optional<DialogLogic.PreviewContext>> previewContextSource() {
return evaluatorValue;
}
@DependencyGetter
public ValueSource<Optional<DialogLogic.Expression>> parsedPropertyExpressionSource() {
return parsedPropertyExpressionValue;
}
})
);
updater.addSource(rootScope, okRunnable);
updater.addConsumer(rootScope, okRunnable);
updater.addDependency(okRunnable, evaluatorValue);
updater.addDependency(okRunnable, parsedPropertyExpressionValue);
updater.addDependency(okRunnable, addWatchExpressionValue);
final OkButtonControl<Runnable> okButtonControl =
new OkButtonControl<Runnable>(okRunnable, warningSources, elements);
updater.addConsumer(rootScope, okButtonControl);
updater.addDependency(okButtonControl, okButtonControl.getDependencies());
return new Handle() {
@Override
public void updateAll() {
updater.updateAll();
}
@Override
public Runnable getOkRunnable() {
return okButtonControl.getNormalValue();
}
@Override
public void saveStateInStore() {
dialogPreferencesStore.setExpressionText(elements.getExpressionText().getText());
dialogPreferencesStore.setPreviewCheck(elements.getPreviewCheckBox().getSelection());
dialogPreferencesStore.setAddWatchExpression(
elements.getAddWatchCheckBox().getSelection());
}
};
}
private static class OkRunnable implements Runnable {
private final Shell parentShell;
private final DialogLogic.Expression expression;
private final DialogLogic.PreviewContext previewContext;
private final boolean addWatchExpression;
OkRunnable(Shell parentShell, DialogLogic.Expression expression,
DialogLogic.PreviewContext previewContext, boolean addWatchExpression) {
this.parentShell = parentShell;
this.expression = expression;
this.previewContext = previewContext;
this.addWatchExpression = addWatchExpression;
}
@Override
public void run() {
Expression.SetCallback callback = new Expression.SetCallback() {
@Override
public void done(final String errorMessage) {
if (errorMessage == null) {
if (addWatchExpression) {
IExpressionManager expressionManager =
DebugPlugin.getDefault().getExpressionManager();
IWatchExpression watchExpression =
expressionManager.newWatchExpression(expression.getWatchExpression());
expressionManager.addExpression(watchExpression);
}
} else {
parentShell.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
MessageBox messageBox = new MessageBox(parentShell);
messageBox.setText(Messages.LogicImpl_RESULT_FAILURE_TITLE);
messageBox.setMessage(errorMessage);
messageBox.open();
}
});
}
}
};
expression.execute(previewContext, callback);
}
}
/**
* Output values from preview switch block in the dialog logic.
*/
private interface PreviewSwitchOutput {
/** A warning that preview block may emit. */
@BranchVariableGetter ValueSource<String> warningSource();
}
/**
* Creates dialog logic elements for the optional preview.
* @return block output variables
*/
private static PreviewSwitchOutput fillShowPreviewSwitch(Switcher<Boolean> switcher,
Updater updater, Scope scope, final DialogImpl.Elements elements,
ValueSource<Optional<DialogLogic.PreviewContext>> evaluatorValue,
ValueProcessor<Optional<DialogLogic.Expression>> parsedPropertyNameValue) {
// Switch branch that corresponds to enabled preview.
Scope checkerScope = switcher.addScope(Boolean.TRUE, new ScopeEnabler() {
@Override
public void setEnabled(boolean enabled, boolean recursive) {
elements.getPreviewDisplay().setEnabled(enabled);
}
});
PreviewSwitchOutput checkDisplayCase = fillCheckDisplayCase(updater, checkerScope,
elements, evaluatorValue, parsedPropertyNameValue);
// Switch branch that corresponds to disabled preview.
switcher.addScope(Boolean.FALSE, null);
// Two branches merge their output.
final PreviewSwitchOutput mergedOutput = mergeBranchVariables(PreviewSwitchOutput.class,
switcher, checkDisplayCase, null);
return new PreviewSwitchOutput() {
@Override
public ValueSource<String> warningSource() {
return mergedOutput.warningSource();
}
};
}
/**
* Creates enabled preview branch in dialog logic.
* @return branch output variables
*/
private static PreviewSwitchOutput fillCheckDisplayCase(Updater updater, Scope scope,
final DialogImpl.Elements elements,
ValueSource<Optional<DialogLogic.PreviewContext>> evaluatorValue,
ValueProcessor<Optional<DialogLogic.Expression>> parsedPropertyNameValue) {
// Asynchronous processor that checks property expression in a remote VM
// and returns diagnostic.
final DialogLogic.CheckerResultProcessor checkerResultSource =
new CheckerResultProcessor(evaluatorValue, parsedPropertyNameValue, updater);
updater.addConsumer(scope, checkerResultSource);
updater.addSource(scope, checkerResultSource);
updater.addSource(scope, checkerResultSource.getWarningSource());
updater.addDependency(checkerResultSource, evaluatorValue);
updater.addDependency(checkerResultSource, parsedPropertyNameValue);
// Consumes diagnostic from above and sets it into a label.
ValueConsumer checkerResultConsumer = new ValueConsumer() {
@Override
public void update(Updater updater) {
elements.getPreviewDisplay().setText(checkerResultSource.getValue());
}
};
updater.addConsumer(scope, checkerResultConsumer);
updater.addDependency(checkerResultConsumer, checkerResultSource);
return new PreviewSwitchOutput() {
@Override
public ValueSource<String> warningSource() {
return checkerResultSource.getWarningSource();
}
};
}
/**
* Contains several objects that are necessary for expression preview or set evaluation.
* It is essentially a struct.
*/
private static class PreviewContext {
/**
* Builds PreviewContext from what was passed from the action. Takes into account
* various problems that are returned as error value of {@link Optional}.
*/
static Optional<PreviewContext> build(Value uiValue) {
if (uiValue == null) {
return createErrorOptional(new Message(Messages.LogicImpl_VALUE_IS_NOT_AVAILABLE,
MessagePriority.BLOCKING_PROBLEM));
}
JsValue jsValue = uiValue.getJsValue();
JsObject jsObject = jsValue.asObject();
if (jsObject == null) {
return createErrorOptional(
new Message(Messages.LogicImpl_NOT_FOR_PRIMITIVE, MessagePriority.BLOCKING_PROBLEM));
}
DebugContext debugContext = uiValue.getSuspendedState().getDebugContext();
// Unsafe asynchronous check.
if (uiValue.getSuspendedState().isDismissed()) {
return createErrorOptional(
new Message(Messages.LogicImpl_CONTEXT_DISMISSED, MessagePriority.BLOCKING_PROBLEM));
}
JsEvaluateContext globalEvaluateContext = debugContext.getGlobalEvaluateContext();
return createOptional(new PreviewContext(globalEvaluateContext, jsObject));
}
final JsEvaluateContext globalEvaluateContext;
final JsObject jsObject;
private PreviewContext(JsEvaluateContext globalEvaluateContext, JsObject jsObject) {
this.globalEvaluateContext = globalEvaluateContext;
this.jsObject = jsObject;
}
}
/**
* Asynchronous processor. It 'previews' property expression, i.e. checks on remote
* whether such property empty or already used and if there is some problem with it.
* Its inputs: preview context and parsed expression.
* Its outputs: diagnostic string and additional warning.
*/
private static class CheckerResultProcessor implements ValueSource<String>, ValueConsumer {
private final ValueSource<Optional<DialogLogic.PreviewContext>> previewContextValue;
private final ValueProcessor<Optional<DialogLogic.Expression>> parsedExpressionValue;
private final Updater updater;
private final CheckerResultProcessor.Monitor monitor = new Monitor();
public CheckerResultProcessor(
ValueSource<Optional<DialogLogic.PreviewContext>> previewContextValue,
ValueProcessor<Optional<DialogLogic.Expression>> parsedPropertyNameValue, Updater updater) {
this.previewContextValue = previewContextValue;
this.parsedExpressionValue = parsedPropertyNameValue;
this.updater = updater;
}
@Override
public String getValue() {
synchronized (monitor) {
return monitor.currentValue;
}
}
ValueSource<String> getWarningSource() {
return warningSource;
}
@Override
public void update(Updater updater) {
int requestNumber;
synchronized (monitor) {
requestNumber = ++monitor.requestId;
}
String newValue = calculateValue(requestNumber);
if (newValue == null) {
return;
}
synchronized (monitor) {
monitor.currentValue = newValue;
}
updater.reportChanged(this);
}
/**
* @return immediately calculated value or null if async calculation has been started
*/
private String calculateValue(final int requestNumber) {
Optional<DialogLogic.PreviewContext> evaluatorOptional = previewContextValue.getValue();
if (!evaluatorOptional.isNormal()) {
return ""; //$NON-NLS-1$
}
DialogLogic.PreviewContext previewContext = evaluatorOptional.getNormal();
Optional<DialogLogic.Expression> expressionOptional = parsedExpressionValue.getValue();
if (!expressionOptional.isNormal()) {
return ""; //$NON-NLS-1$
}
DialogLogic.Expression expression = expressionOptional.getNormal();
Expression.PreviewCallback callback = new Expression.PreviewCallback() {
@Override
public void done(String message, String additionalWarning) {
synchronized (monitor) {
if (monitor.requestId != requestNumber) {
return;
}
monitor.currentValue = message;
monitor.additionalWarning = additionalWarning;
updater.reportChanged(CheckerResultProcessor.this);
updater.reportChanged(warningSource);
updater.updateAsync();
}
}
};
expression.doPreview(previewContext, callback);
return null;
}
private static class Monitor {
String additionalWarning = null;
int requestId = 1;
String currentValue = ""; //$NON-NLS-1$
}
private final ValueSource<String> warningSource = new ValueSource<String>() {
@Override
public String getValue() {
synchronized (monitor) {
return monitor.additionalWarning;
}
}
};
}
/**
* Parsed property expression. The simplest expression could be ".foo", but implementation
* may support more sophisticated expressions as well.
* The implementation is responsible for setting or previewing the property within
* a provided {@link PreviewContext}. It also returns string representation of the property
* for the watch expression.
*/
private static abstract class Expression {
protected abstract void doPreview(DialogLogic.PreviewContext previewContext,
PreviewCallback callback);
interface PreviewCallback {
void done(String message, String additionalWarning);
}
protected abstract void execute(DialogLogic.PreviewContext previewContext, SetCallback callback);
interface SetCallback {
void done(String errorMessage);
}
/**
* @return expression string representation suitable for watch expression view.
*/
protected abstract String getWatchExpression();
}
/**
* Parses expression using {@link DotSeparatedExpression} class. More sophisticated
* parsers could be introduced later.
*/
private static Optional<DialogLogic.Expression> parseExpression(String string) {
return DotSeparatedExpression.parse(string);
}
/**
* A parser that expects expression to be in form of ".property1.property2 ... .propertyN".
* If the intermediate objects do not exist, they will be created as necessary.
* It may be not very accurate working as JavaScript lexer.
*/
private static class DotSeparatedExpression extends DialogLogic.Expression {
static Optional<DialogLogic.Expression> parse(String string) {
if (string.length() == 0) {
return createErrorOptional(new Message(Messages.LogicImpl_ENTER_EXPRESSION,
MessagePriority.BLOCKING_PROBLEM));
}
List<String> parts = new ArrayList<String>();
int pos = 0;
while (true) {
if (pos >= string.length() || string.charAt(pos) != '.') {
return createErrorOptional(new Message(Messages.LogicImpl_DOT_EXPECTED,
MessagePriority.BLOCKING_PROBLEM));
}
pos++;
int partStartPos = pos;
if (pos >= string.length()) {
return createErrorOptional(
new Message(Messages.LogicImpl_ENTER_AFTER_DOT, MessagePriority.BLOCKING_INFO));
}
if (!Character.isJavaIdentifierStart(string.codePointAt(pos))) {
return createErrorOptional(new Message(Messages.LogicImpl_INVALID_COMPONENT_START,
MessagePriority.BLOCKING_PROBLEM));
}
pos = string.offsetByCodePoints(pos, 1);
while (pos < string.length() && string.charAt(pos) != '.') {
if (!Character.isJavaIdentifierPart(string.codePointAt(pos))) {
return createErrorOptional(new Message(Messages.LogicImpl_INVALID_COMPONENT_CHAR,
MessagePriority.BLOCKING_PROBLEM));
}
pos = string.offsetByCodePoints(pos, 1);
}
parts.add(string.substring(partStartPos, pos));
if (pos >= string.length()) {
break;
}
}
return DialogUtils.<DialogLogic.Expression>createOptional(new DotSeparatedExpression(parts));
}
private final List<String> parts;
DotSeparatedExpression(List<String> parts) {
this.parts = parts;
}
protected void doPreview(DialogLogic.PreviewContext previewContext,
final PreviewCallback callback) {
// Prepare evaluate expression.
StringBuilder builder = new StringBuilder();
builder.append("(function() {\n"); //$NON-NLS-1$
// Get global object.
builder.append("var t = (function GetGlobal(){return this;})();\n"); //$NON-NLS-1$
// Iterate over property chain and check all intermediate objects.
// '+' is used in the mix with 'append' (not very effective) to keep code readable.
for (int i = 0; i < parts.size() - 1; i++) {
builder.append("if (\"" + parts.get(i) + "\" in t) { t = t." + //$NON-NLS-1$ //$NON-NLS-2$
parts.get(i) + "; } else { return '" + //$NON-NLS-1$
freePropertyCode + "'; }\n"); //$NON-NLS-1$
builder.append("if (t instanceof Object == false) { throw \"Property '" + //$NON-NLS-1$
parts.get(i) + "' contains non-object\"}\n"); //$NON-NLS-1$
}
// Check last expression component.
String lastComponent = parts.get(parts.size() - 1);
builder.append("if (\"" + lastComponent + "\" in t) { return '" + //$NON-NLS-1$
occupiedPropertyCode + //$NON-NLS-1$
"' + String(t." + lastComponent + "); } else { return '" + //$NON-NLS-1$ //$NON-NLS-2$
freePropertyCode + "'; }\n"); //$NON-NLS-1$
builder.append("})()"); //$NON-NLS-1$
String expression = builder.toString();
// The expression returns string value or throws an exception.
// It uses occupiedPropertyCode and freePropertyCode strings as special return values.
// All other return values are treated as error message.
EvaluateCallback evaluateCallback = new EvaluateCallback() {
// A message that shows when preview encountered an exception. Since preview
// is optional, the exception is reported as warning.
private final String warningMessage =
Messages.LogicImpl_WARNING_SEEMS_A_PROBLEM;
@Override
public void success(ResultOrException result) {
result.accept(new ResultOrException.Visitor<Void>() {
@Override
public Void visitResult(JsValue value) {
handleSuccess(value);
return null;
}
@Override
public Void visitException(JsValue exception) {
handleFailure(exception.getValueString());
return null;
}
});
}
@Override
public void failure(Exception cause) {
handleFailure(cause.getMessage());
}
private void handleSuccess(JsValue jsValue) {
String stringValue = jsValue.getValueString();
String value;
String warning;
if (freePropertyCode.equals(stringValue)) {
value = Messages.LogicImpl_PROPERTY_FREE;
warning = null;
} else if (stringValue.startsWith(occupiedPropertyCode)) {
String additionalString = stringValue.substring(occupiedPropertyCode.length());
value = Messages.LogicImpl_PROPERTY_WILL_BE_OVERWRITTEN + additionalString;
warning = null;
} else {
value = stringValue;
warning = warningMessage;
}
callback.done(value, warning);
}
private void handleFailure(String errorMessage) {
callback.done(Messages.LogicImpl_PROBLEM_ON_REMOTE + errorMessage, warningMessage);
}
};
previewContext.globalEvaluateContext.evaluateAsync(expression, null, evaluateCallback, null);
}
protected void execute(DialogLogic.PreviewContext previewContext, final SetCallback callback) {
// Prepare evaluate expression.
StringBuilder builder = new StringBuilder();
builder.append("(function() {\n"); //$NON-NLS-1$
// Get global object.
builder.append("var t = (function GetGlobal(){return this;})();\n"); //$NON-NLS-1$
// Iterate over property chain and check all intermediate objects.
for (int i = 0; i < parts.size() - 1; i++) {
// '+' is used in the mix with 'append' (not very effective) to keep code readable.
builder.append("if (\"" + parts.get(i) + "\" in t) { t = t." + //$NON-NLS-1$
parts.get(i) + //$NON-NLS-1$
"; } else { t = (t." + parts.get(i) + " = {}); }\n"); //$NON-NLS-1$ //$NON-NLS-2$
}
// Add a magic variable to expression that would be backed up by a handle.
String paramJsName = "__pinPointedValue"; //$NON-NLS-1$
String lastComponent = parts.get(parts.size() - 1);
builder.append("t." + lastComponent + " = " + //$NON-NLS-1$ //$NON-NLS-2$
paramJsName + ";\n"); //$NON-NLS-1$
builder.append("})()"); //$NON-NLS-1$
String expression = builder.toString();
EvaluateCallback evaluateCallback = new EvaluateCallback() {
@Override
public void success(ResultOrException result) {
String errorMessage = result.accept(new ResultOrException.Visitor<String>() {
@Override public String visitResult(JsValue value) {
return null;
}
@Override public String visitException(JsValue exception) {
return exception.getValueString();
}
});
callback.done(errorMessage);
}
@Override public void failure(Exception cause) {
callback.done(cause.getMessage());
}
};
previewContext.globalEvaluateContext.evaluateAsync(expression,
Collections.singletonMap(paramJsName, previewContext.jsObject),
evaluateCallback, null);
}
protected String getWatchExpression() {
StringBuilder builder = new StringBuilder();
for (String s : parts) {
if (builder.length() != 0) {
builder.append("."); //$NON-NLS-1$
}
builder.append(s);
}
return builder.toString();
}
private static final String freePropertyCode = "free"; //$NON-NLS-1$
private static final String occupiedPropertyCode = "occupied"; //$NON-NLS-1$
}
}