// Copyright (c) 2011 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.sdk.internal.wip;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.BreakpointTypeExtension;
import org.chromium.sdk.IgnoreCountBreakpointExtension;
import org.chromium.sdk.JavascriptVm.BreakpointCallback;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.internal.ScriptRegExpBreakpointTarget;
import org.chromium.sdk.internal.wip.protocol.input.WipCommandResponse.Success;
import org.chromium.sdk.internal.wip.protocol.input.debugger.LocationValue;
import org.chromium.sdk.internal.wip.protocol.input.debugger.SetBreakpointByUrlData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.SetBreakpointData;
import org.chromium.sdk.internal.wip.protocol.output.WipParamsWithResponse;
import org.chromium.sdk.internal.wip.protocol.output.debugger.LocationParam;
import org.chromium.sdk.internal.wip.protocol.output.debugger.RemoveBreakpointParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.SetBreakpointByUrlParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.SetBreakpointParams;
import org.chromium.sdk.util.GenericCallback;
import org.chromium.sdk.util.RelaySyncCallback;
/**
* Wip-based breakpoint implementation.
* The implementation is based on volatile fields and expects client code to do some
* synchronization (serialize calls to setters, {@link #flush} and {@link #clear}).
*/
public class WipBreakpointImpl implements Breakpoint {
private final WipBreakpointManager breakpointManager;
private final Target target;
private final int lineNumber;
private final int columnNumber;
private final int sdkId;
private volatile String protocolId = null;
private volatile String condition;
private volatile boolean enabled;
private volatile boolean isDirty;
// Access only from Dispatch thread.
private Set<ActualLocation> actualLocations = new HashSet<ActualLocation>(2);
public WipBreakpointImpl(WipBreakpointManager breakpointManager, int sdkId, Target target,
int lineNumber, int columnNumber, String condition, boolean enabled) {
this.breakpointManager = breakpointManager;
this.sdkId = sdkId;
this.target = target;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.condition = condition;
this.enabled = enabled;
this.isDirty = false;
}
@Override
public Target getTarget() {
return target;
}
@Override
public long getId() {
return sdkId;
}
public static final BreakpointTypeExtension TYPE_EXTENSION = new BreakpointTypeExtension() {
@Override
public FunctionSupport getFunctionSupport() {
return null;
}
@Override
public ScriptRegExpSupport getScriptRegExpSupport() {
return scriptRegExpSupport;
}
private final ScriptRegExpSupport scriptRegExpSupport = new ScriptRegExpSupport() {
@Override
public Target createTarget(String regExp) {
return new ScriptRegExpBreakpointTarget(regExp);
}
};
};
@Override
public long getLineNumber() {
return lineNumber;
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public void setEnabled(boolean enabled) {
if (enabled == this.enabled) {
return;
}
this.enabled = enabled;
isDirty = true;
}
@Override
public String getCondition() {
return condition;
}
@Override
public void setCondition(String condition) {
if (eq(this.condition, condition)) {
return;
}
this.condition = condition;
isDirty = true;
}
@Override
public IgnoreCountBreakpointExtension getIgnoreCountBreakpointExtension() {
return getIgnoreCountBreakpointExtensionImpl();
}
public static IgnoreCountBreakpointExtension getIgnoreCountBreakpointExtensionImpl() {
// TODO(peter.rybin): implement when protocol supports.
return null;
}
void setRemoteData(String protocolId, Collection<ActualLocation> actualLocations) {
this.protocolId = protocolId;
this.actualLocations.clear();
this.actualLocations.addAll(actualLocations);
this.breakpointManager.getDb().setIdMapping(this, protocolId);
}
void addResolvedLocation(LocationValue locationValue) {
ActualLocation location = locationFromProtocol(locationValue);
actualLocations.add(location);
}
Set<ActualLocation> getActualLocations() {
return actualLocations;
}
void clearActualLocations() {
actualLocations.clear();
}
void deleteSelfFromDb() {
if (protocolId != null) {
breakpointManager.getDb().setIdMapping(this, null);
}
breakpointManager.getDb().removeBreakpoint(this);
}
@Override
public RelayOk clear(final BreakpointCallback callback, SyncCallback syncCallback) {
// TODO: make sure this is thread-safe.
if (protocolId == null) {
breakpointManager.getDb().removeBreakpoint(this);
callback.success(this);
return RelaySyncCallback.finish(syncCallback);
}
RemoveBreakpointParams params = new RemoveBreakpointParams(protocolId);
WipCommandCallback commandCallback;
if (callback == null) {
commandCallback = null;
} else {
commandCallback = new WipCommandCallback.Default() {
@Override protected void onSuccess(Success success) {
breakpointManager.getDb().setIdMapping(WipBreakpointImpl.this, null);
breakpointManager.getDb().removeBreakpoint(WipBreakpointImpl.this);
callback.success(WipBreakpointImpl.this);
}
@Override protected void onError(String message) {
callback.failure(message);
}
};
}
return breakpointManager.getCommandProcessor().send(params, commandCallback, syncCallback);
}
@Override
public RelayOk flush(final BreakpointCallback callback, final SyncCallback syncCallback) {
final RelaySyncCallback relay = new RelaySyncCallback(syncCallback);
if (!isDirty) {
if (callback != null) {
callback.success(this);
}
return RelaySyncCallback.finish(syncCallback);
}
isDirty = false;
if (protocolId == null) {
// Breakpoint was disabled, it doesn't exist in VM, immediately start step 2.
return recreateBreakpointAsync(callback, relay);
} else {
// Call syncCallback if something goes wrong after we sent request.
final RelaySyncCallback.Guard guard = relay.newGuard();
WipCommandCallback removeCallback = new WipCommandCallback.Default() {
@Override
protected void onSuccess(Success success) {
setRemoteData(null, Collections.<ActualLocation>emptyList());
RelayOk relayOk = recreateBreakpointAsync(callback, relay);
guard.discharge(relayOk);
}
@Override
protected void onError(String message) {
throw new RuntimeException("Failed to remove breakpoint: " + message);
}
};
// Call syncCallback if something goes wrong.
return breakpointManager.getCommandProcessor().send(new RemoveBreakpointParams(protocolId),
removeCallback, guard.asSyncCallback());
}
}
static class ActualLocation {
private final String sourceId;
private final long lineNumber;
private final Long columnNumber;
ActualLocation(String sourceId, long lineNumber, Long columnNumber) {
this.sourceId = sourceId;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
}
@Override
public boolean equals(Object obj) {
ActualLocation other = (ActualLocation) obj;
return this.sourceId.equals(other.sourceId) &&
this.lineNumber == other.lineNumber &&
eq(this.columnNumber, other.columnNumber);
}
@Override
public int hashCode() {
int column;
if (columnNumber == null) {
column = 0;
} else {
column = columnNumber.intValue();
}
return sourceId.hashCode() + 31 * (int) lineNumber + column;
}
@Override
public String toString() {
return "<sourceId=" + sourceId + ", line=" + lineNumber + ", column=" + columnNumber + ">";
}
}
private RelayOk recreateBreakpointAsync(final BreakpointCallback flushCallback,
RelaySyncCallback relay) {
if (enabled) {
SetBreakpointCallback setCommandCallback = new SetBreakpointCallback() {
@Override
public void onSuccess(String protocolId, Collection<ActualLocation> actualLocations) {
setRemoteData(protocolId, actualLocations);
if (flushCallback != null) {
flushCallback.success(WipBreakpointImpl.this);
}
}
@Override
public void onFailure(Exception exception) {
if (flushCallback != null) {
flushCallback.failure(exception.getMessage());
}
}
};
RelaySyncCallback.Guard guard = relay.newGuard();
if (condition == null) {
condition = "";
}
return sendSetBreakpointRequest(target, lineNumber, columnNumber, condition,
setCommandCallback, guard.asSyncCallback(),
breakpointManager.getCommandProcessor());
} else {
// Breakpoint is disabled, do not create it.
RelayOk relayOk;
try {
if (flushCallback != null) {
flushCallback.success(WipBreakpointImpl.this);
}
} finally {
relayOk = relay.finish();
}
return relayOk;
}
}
interface SetBreakpointCallback {
void onSuccess(String breakpointId, Collection<ActualLocation> actualLocations);
void onFailure(Exception cause);
}
/**
* @param callback a generic callback that receives breakpoint protocol id
* @return
*/
static RelayOk sendSetBreakpointRequest(Target target, final int lineNumber,
int columnNumber, final String condition,
final SetBreakpointCallback callback, final SyncCallback syncCallback,
final WipCommandProcessor commandProcessor) {
final Long columnNumberParam;
if (columnNumber == Breakpoint.EMPTY_VALUE) {
columnNumberParam = null;
} else {
columnNumberParam = Long.valueOf(columnNumber);
}
return target.accept(new BreakpointTypeExtension.ScriptRegExpSupport.Visitor<RelayOk>() {
@Override
public RelayOk visitScriptName(String scriptName) {
return sendRequest(scriptName, RequestHandler.FOR_URL);
}
@Override
public RelayOk visitRegExp(String regExp) {
return sendRequest(regExp, RequestHandler.FOR_REGEXP);
}
@Override
public RelayOk visitScriptId(Object scriptId) {
String scriptIdString = WipScriptManager.convertAlienSourceId(scriptId);
return sendRequest(scriptIdString, RequestHandler.FOR_ID);
}
@Override
public RelayOk visitUnknown(Target target) {
throw new IllegalArgumentException();
}
private <T, DATA, PARAMS extends WipParamsWithResponse<DATA>> RelayOk sendRequest(
T parameter, final RequestHandler<T, DATA, PARAMS> handler) {
PARAMS requestParams =
handler.createRequestParams(parameter, lineNumber, columnNumberParam, condition);
GenericCallback<DATA> wrappedCallback;
if (callback == null) {
wrappedCallback = null;
} else {
wrappedCallback = new GenericCallback<DATA>() {
@Override
public void success(DATA data) {
String breakpointId = handler.getBreakpointId(data);
Collection<LocationValue> locationValues = handler.getActualLocations(data);
List<ActualLocation> locationList =
new ArrayList<ActualLocation>(locationValues.size());
for (LocationValue value : locationValues) {
locationList.add(locationFromProtocol(value));
}
callback.onSuccess(breakpointId, locationList);
}
@Override
public void failure(Exception exception) {
callback.onFailure(exception);
}
};
}
return commandProcessor.send(requestParams, wrappedCallback, syncCallback);
}
});
}
private static abstract class RequestHandler<T,
DATA, PARAMS extends WipParamsWithResponse<DATA>> {
abstract PARAMS createRequestParams(T parameter, long lineNumber, Long columnNumberOpt,
String conditionOpt);
abstract String getBreakpointId(DATA data);
abstract Collection<LocationValue> getActualLocations(DATA data);
static abstract class ForUrlOrRegExp
extends RequestHandler<String, SetBreakpointByUrlData, SetBreakpointByUrlParams> {
@Override
String getBreakpointId(SetBreakpointByUrlData data) {
return data.breakpointId();
}
@Override
Collection<LocationValue> getActualLocations(SetBreakpointByUrlData data) {
return data.locations();
}
}
static final ForUrlOrRegExp FOR_URL = new ForUrlOrRegExp() {
@Override
SetBreakpointByUrlParams createRequestParams(String url,
long lineNumber, Long columnNumber, String condition) {
return new SetBreakpointByUrlParams(lineNumber, url, null, columnNumber, condition);
}
};
static final ForUrlOrRegExp FOR_REGEXP = new ForUrlOrRegExp() {
@Override
SetBreakpointByUrlParams createRequestParams(String url,
long lineNumber, Long columnNumber, String condition) {
return new SetBreakpointByUrlParams(lineNumber, null, url, columnNumber, condition);
}
};
static final RequestHandler<String, SetBreakpointData, SetBreakpointParams> FOR_ID =
new RequestHandler<String, SetBreakpointData, SetBreakpointParams>() {
@Override
SetBreakpointParams createRequestParams(String sourceId,
long lineNumber, Long columnNumber, String condition) {
LocationParam locationParam =
new LocationParam(sourceId, lineNumber, columnNumber);
return new SetBreakpointParams(locationParam, condition);
}
@Override
String getBreakpointId(SetBreakpointData data) {
return data.breakpointId();
}
@Override
Collection<LocationValue> getActualLocations(SetBreakpointData data) {
return Collections.singletonList(data.actualLocation());
}
};
}
private static ActualLocation locationFromProtocol(LocationValue locationValue) {
return new ActualLocation(locationValue.scriptId(), locationValue.lineNumber(),
locationValue.columnNumber());
}
private static <T> boolean eq(T left, T right) {
return left == right || (left != null && left.equals(right));
}
}