// 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.DebuggerApi.DebuggerResponseListener;
import com.google.collide.client.code.debugging.DebuggerApiTypes.BreakpointInfo;
import com.google.collide.client.code.debugging.DebuggerApiTypes.CallFrame;
import com.google.collide.client.code.debugging.DebuggerApiTypes.ConsoleMessage;
import com.google.collide.client.code.debugging.DebuggerApiTypes.Location;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnAllCssStyleSheetsResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnBreakpointResolvedResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnEvaluateExpressionResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnPausedResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnRemoteObjectPropertiesResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnRemoteObjectPropertyChanged;
import com.google.collide.client.code.debugging.DebuggerApiTypes.OnScriptParsedResponse;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectId;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.util.ScheduledCommandExecutor;
import com.google.collide.client.util.logging.Log;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonStringMap;
import com.google.collide.json.shared.JsonStringSet;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.GWT;
import javax.annotation.Nullable;
/**
* Represents debugger state on a given debugger session: whether debugger is
* active, running or paused, and etc.
*
* <p>This class also contains cached data responses from the debugger for the
* life time of a debugger session.
*/
class DebuggerState {
/**
* Listener of the "debugger is available" state changes.
*/
interface DebuggerAvailableListener {
void onDebuggerAvailableChange();
}
/**
* Listener of the debugger state changes.
*/
interface DebuggerStateListener {
void onDebuggerStateChange();
}
/**
* Listener of the {@link DebuggerApiTypes.RemoteObject} related changes.
*/
interface RemoteObjectListener {
void onRemoteObjectPropertiesResponse(OnRemoteObjectPropertiesResponse response);
void onRemoteObjectPropertyChanged(OnRemoteObjectPropertyChanged response);
}
/**
* Listener of the expression evaluation responses.
*/
interface EvaluateExpressionListener {
void onEvaluateExpressionResponse(OnEvaluateExpressionResponse response);
void onGlobalObjectChanged();
}
/**
* Listener of the CSS related responses.
*/
interface CssListener {
void onAllCssStyleSheetsResponse(OnAllCssStyleSheetsResponse response);
}
/**
* Listener of the Console related events.
*/
interface ConsoleListener {
void onConsoleMessage(ConsoleMessage message);
void onConsoleMessageRepeatCountUpdated(ConsoleMessage message, int repeatCount);
void onConsoleMessagesCleared();
}
/**
* Listener of the custom message responses.
*/
interface CustomMessageListener {
void onCustomMessageResponse(String response);
}
private final String sessionId;
private final DebuggerApi debuggerApi;
private final JsonArray<BreakpointInfoImpl> breakpointInfos = JsonCollections.createArray();
private JsonStringMap<OnScriptParsedResponse> scriptParsedResponses = JsonCollections.createMap();
private final ListenerManager<DebuggerAvailableListener> debuggerAvailableListenerManager;
private final ListenerManager<DebuggerStateListener> debuggerStateListenerManager;
private final ListenerManager<RemoteObjectListener> remoteObjectListenerManager;
private final ListenerManager<EvaluateExpressionListener> evaluateExpressionListenerManager;
private final ListenerManager<CssListener> cssListenerManager;
private final ListenerManager<ConsoleListener> consoleListenerManager;
private final ListenerManager<CustomMessageListener> customMessageListenerManager;
private final JsonStringSet expressionsToEvaluate = JsonCollections.createStringSet();
private boolean active;
private SourceMapping sourceMapping;
@Nullable
private OnPausedResponse lastOnPausedResponse;
private final ScheduledCommandExecutor expressionsEvaluateCommand =
new ScheduledCommandExecutor() {
@Override
protected void execute() {
JsonArray<String> expressions = expressionsToEvaluate.getKeys();
expressionsToEvaluate.clear();
for (int i = 0, n = expressions.size(); i < n; ++i) {
sendEvaluateExpressionRequest(expressions.get(i));
}
}
};
/**
* Index of the active {@link CallFrame} in the call stack.
*
* <p>Default value is {@code 0} that corresponds to the topmost
* {@link CallFrame} where debugger has stopped. If debugger is not paused,
* this value has no meaning.
*
* <p>This is controlled by the user from the Debugger Sidebar UI.
*/
private int activeCallFrameIndex = 0;
/**
* Last console message received from the debugger.
*/
private ConsoleMessage lastConsoleMessage;
private final DebuggerResponseListener debuggerResponseListener = new DebuggerResponseListener() {
@Override
public void onDebuggerAvailableChanged() {
debuggerAvailableListenerManager.dispatch(DEBUGGER_AVAILABLE_DISPATCHER);
}
@Override
public void onDebuggerAttached(String eventSessionId) {
if (sessionId.equals(eventSessionId)) {
setActive(true);
}
}
@Override
public void onDebuggerDetached(String eventSessionId) {
if (sessionId.equals(eventSessionId)) {
setActive(false);
}
}
@Override
public void onBreakpointResolved(String eventSessionId, OnBreakpointResolvedResponse response) {
if (sessionId.equals(eventSessionId)) {
updateBreakpointInfo(response);
}
}
@Override
public void onBreakpointRemoved(String eventSessionId, String breakpointId) {
if (sessionId.equals(eventSessionId)) {
removeBreakpointById(breakpointId);
}
}
@Override
public void onPaused(String eventSessionId, OnPausedResponse response) {
if (sessionId.equals(eventSessionId)) {
setOnPausedResponse(response);
}
}
@Override
public void onResumed(String eventSessionId) {
if (sessionId.equals(eventSessionId)) {
setOnPausedResponse(null);
}
}
@Override
public void onScriptParsed(String eventSessionId, OnScriptParsedResponse response) {
if (sessionId.equals(eventSessionId)) {
scriptParsedResponses.put(response.getScriptId(), response);
}
}
@Override
public void onRemoteObjectPropertiesResponse(String eventSessionId,
final OnRemoteObjectPropertiesResponse response) {
if (sessionId.equals(eventSessionId)) {
remoteObjectListenerManager.dispatch(new Dispatcher<RemoteObjectListener>() {
@Override
public void dispatch(RemoteObjectListener listener) {
listener.onRemoteObjectPropertiesResponse(response);
}
});
}
}
@Override
public void onRemoteObjectPropertyChanged(String eventSessionId,
final OnRemoteObjectPropertyChanged response) {
if (sessionId.equals(eventSessionId)) {
remoteObjectListenerManager.dispatch(new Dispatcher<RemoteObjectListener>() {
@Override
public void dispatch(RemoteObjectListener listener) {
listener.onRemoteObjectPropertyChanged(response);
}
});
}
}
@Override
public void onEvaluateExpressionResponse(String eventSessionId,
final OnEvaluateExpressionResponse response) {
if (sessionId.equals(eventSessionId)) {
CallFrame callFrame = getActiveCallFrame();
String callFrameId = (callFrame == null ? null : callFrame.getId());
if (!StringUtils.equalStringsOrEmpty(callFrameId, response.getCallFrameId())) {
// Maybe a late response from a previous evaluation call. The corresponding evaluation
// call for the active call frame should have been already sent to the debugger, so just
// ignore this old response and wait for the actual one.
return;
}
evaluateExpressionListenerManager.dispatch(new Dispatcher<EvaluateExpressionListener>() {
@Override
public void dispatch(EvaluateExpressionListener listener) {
listener.onEvaluateExpressionResponse(response);
}
});
}
}
@Override
public void onGlobalObjectChanged(String eventSessionId) {
if (sessionId.equals(eventSessionId)) {
evaluateExpressionListenerManager.dispatch(new Dispatcher<EvaluateExpressionListener>() {
@Override
public void dispatch(EvaluateExpressionListener listener) {
listener.onGlobalObjectChanged();
}
});
}
}
@Override
public void onAllCssStyleSheetsResponse(String eventSessionId,
final OnAllCssStyleSheetsResponse response) {
if (sessionId.equals(eventSessionId)) {
cssListenerManager.dispatch(new Dispatcher<CssListener>() {
@Override
public void dispatch(CssListener listener) {
listener.onAllCssStyleSheetsResponse(response);
}
});
}
}
@Override
public void onConsoleMessage(String eventSessionId, ConsoleMessage message) {
if (sessionId.equals(eventSessionId)) {
lastConsoleMessage = message;
consoleListenerManager.dispatch(new Dispatcher<ConsoleListener>() {
@Override
public void dispatch(ConsoleListener listener) {
listener.onConsoleMessage(lastConsoleMessage);
}
});
}
}
@Override
public void onConsoleMessageRepeatCountUpdated(String eventSessionId, final int repeatCount) {
if (sessionId.equals(eventSessionId) && lastConsoleMessage != null) {
consoleListenerManager.dispatch(new Dispatcher<ConsoleListener>() {
@Override
public void dispatch(ConsoleListener listener) {
listener.onConsoleMessageRepeatCountUpdated(lastConsoleMessage, repeatCount);
}
});
}
}
@Override
public void onConsoleMessagesCleared(String eventSessionId) {
if (sessionId.equals(eventSessionId)) {
lastConsoleMessage = null;
consoleListenerManager.dispatch(CONSOLE_MESSAGES_CLEARED_DISPATCHER);
}
}
@Override
public void onCustomMessageResponse(String eventSessionId, final String response) {
if (sessionId.equals(eventSessionId)) {
customMessageListenerManager.dispatch(new Dispatcher<CustomMessageListener>() {
@Override
public void dispatch(CustomMessageListener listener) {
listener.onCustomMessageResponse(response);
}
});
}
}
};
private static final Dispatcher<DebuggerStateListener> DEBUGGER_STATE_DISPATCHER =
new Dispatcher<DebuggerStateListener>() {
@Override
public void dispatch(DebuggerStateListener listener) {
listener.onDebuggerStateChange();
}
};
private static final Dispatcher<DebuggerAvailableListener> DEBUGGER_AVAILABLE_DISPATCHER =
new Dispatcher<DebuggerAvailableListener>() {
@Override
public void dispatch(DebuggerAvailableListener listener) {
listener.onDebuggerAvailableChange();
}
};
private static final Dispatcher<ConsoleListener> CONSOLE_MESSAGES_CLEARED_DISPATCHER =
new Dispatcher<ConsoleListener>() {
@Override
public void dispatch(ConsoleListener listener) {
listener.onConsoleMessagesCleared();
}
};
static DebuggerState create(String sessionId) {
return new DebuggerState(sessionId, GWT.<DebuggerApi>create(DebuggerApi.class));
}
@VisibleForTesting
static DebuggerState createForTest(String sessionId, DebuggerApi debuggerApi) {
return new DebuggerState(sessionId, debuggerApi);
}
private DebuggerState(String sessionId, DebuggerApi debuggerApi) {
this.sessionId = sessionId;
this.debuggerApi = debuggerApi;
this.debuggerAvailableListenerManager = ListenerManager.create();
this.debuggerStateListenerManager = ListenerManager.create();
this.remoteObjectListenerManager = ListenerManager.create();
this.evaluateExpressionListenerManager = ListenerManager.create();
this.cssListenerManager = ListenerManager.create();
this.consoleListenerManager = ListenerManager.create();
this.customMessageListenerManager = ListenerManager.create();
this.debuggerApi.addDebuggerResponseListener(debuggerResponseListener);
}
ListenerRegistrar<DebuggerAvailableListener> getDebuggerAvailableListenerRegistrar() {
return debuggerAvailableListenerManager;
}
ListenerRegistrar<DebuggerStateListener> getDebuggerStateListenerRegistrar() {
return debuggerStateListenerManager;
}
ListenerRegistrar<RemoteObjectListener> getRemoteObjectListenerRegistrar() {
return remoteObjectListenerManager;
}
ListenerRegistrar<EvaluateExpressionListener> getEvaluateExpressionListenerRegistrar() {
return evaluateExpressionListenerManager;
}
ListenerRegistrar<CssListener> getCssListenerRegistrar() {
return cssListenerManager;
}
ListenerRegistrar<ConsoleListener> getConsoleListenerRegistrar() {
return consoleListenerManager;
}
ListenerRegistrar<CustomMessageListener> getCustomMessageListenerRegistrar() {
return customMessageListenerManager;
}
/**
* @return whether debugger is available to use
*/
public boolean isDebuggerAvailable() {
return debuggerApi.isDebuggerAvailable();
}
/**
* @return URL of the browser extension that provides the debugging API,
* or {@code null} if no such extension is available
*/
public String getDebuggingExtensionUrl() {
return debuggerApi.getDebuggingExtensionUrl();
}
/**
* @return whether debugger is currently in use
*/
public boolean isActive() {
return active;
}
/**
* @return {@code true} if debugger is currently paused, otherwise debugger
* is either not active or running
*/
public boolean isPaused() {
return lastOnPausedResponse != null;
}
/**
* Sets the index of the active {@link CallFrame} (i.e. selected by the user
* in the UI).
*
* @param index index of the active call frame
*/
public void setActiveCallFrameIndex(int index) {
activeCallFrameIndex = index;
}
/**
* @return current {@link SourceMapping} object used for debugging, or
* {@code null} if debugger is not active
*/
public SourceMapping getSourceMapping() {
return sourceMapping;
}
/**
* @return last {@link OnPausedResponse} from the debugger, or {@code null}
* if debugger is not currently paused
*/
@Nullable
public OnPausedResponse getOnPausedResponse() {
return lastOnPausedResponse;
}
/**
* @return {@link OnScriptParsedResponse} for a given source ID, or
* {@code null} if undefined
*/
public OnScriptParsedResponse getOnScriptParsedResponse(String scriptId) {
return scriptParsedResponses.get(scriptId);
}
public CallFrame getActiveCallFrame() {
if (lastOnPausedResponse == null) {
return null;
}
return lastOnPausedResponse.getCallFrames().get(activeCallFrameIndex);
}
/**
* Calculates {@link PathUtil} for the active call frame of the current
* debugger call stack.
*
* @return a new instance of {@code PathUtil} if the call frame points to a
* script in a resource, served by the Collide server, or
* {@code null} if this script is served elsewhere, or is anonymous
* (result of an {@code eval()} call and etc.), or for other reasons
*/
public PathUtil getActiveCallFramePath() {
CallFrame callFrame = getActiveCallFrame();
if (callFrame == null || callFrame.getLocation() == null) {
return null;
}
Preconditions.checkNotNull(sourceMapping, "No source mapping!");
Preconditions.checkNotNull(scriptParsedResponses, "No parsed scripts!");
String scriptId = callFrame.getLocation().getScriptId();
return sourceMapping.getLocalScriptPath(scriptParsedResponses.get(scriptId));
}
public int getActiveCallFrameExecutionLineNumber() {
CallFrame callFrame = getActiveCallFrame();
if (callFrame == null || callFrame.getLocation() == null) {
return -1;
}
Preconditions.checkNotNull(sourceMapping, "No source mapping!");
Preconditions.checkNotNull(scriptParsedResponses, "No parsed scripts!");
String scriptId = callFrame.getLocation().getScriptId();
return sourceMapping.getLocalSourceLineNumber(scriptParsedResponses.get(scriptId),
callFrame.getLocation());
}
void runDebugger(SourceMapping sourceMapping, String absoluteResourceUri) {
Preconditions.checkNotNull(sourceMapping, "Source mapping is NULL!");
if (active) {
// We will be reusing current debuggee session, so do a soft reset here.
softReset();
} else {
reset();
}
active = true;
this.sourceMapping = sourceMapping;
lastOnPausedResponse = null;
activeCallFrameIndex = 0;
debuggerApi.runDebugger(sessionId, absoluteResourceUri);
debuggerStateListenerManager.dispatch(DEBUGGER_STATE_DISPATCHER);
}
void shutdown() {
debuggerApi.shutdownDebugger(sessionId);
}
void pause() {
if (active) {
debuggerApi.pause(sessionId);
}
}
void resume() {
if (active) {
debuggerApi.resume(sessionId);
}
}
void stepInto() {
if (active) {
debuggerApi.stepInto(sessionId);
}
}
void stepOut() {
if (active) {
debuggerApi.stepOut(sessionId);
}
}
void stepOver() {
if (active) {
debuggerApi.stepOver(sessionId);
}
}
void requestRemoteObjectProperties(RemoteObjectId remoteObjectId) {
if (active) {
debuggerApi.requestRemoteObjectProperties(sessionId, remoteObjectId);
}
}
void setRemoteObjectProperty(RemoteObjectId remoteObjectId, String propertyName,
String propertyValueExpression) {
if (active) {
CallFrame callFrame = getActiveCallFrame();
if (callFrame != null) {
debuggerApi.setRemoteObjectPropertyEvaluatedOnCallFrame(
sessionId, callFrame, remoteObjectId, propertyName, propertyValueExpression);
} else {
debuggerApi.setRemoteObjectProperty(
sessionId, remoteObjectId, propertyName, propertyValueExpression);
}
}
}
void removeRemoteObjectProperty(RemoteObjectId remoteObjectId, String propertyName) {
if (active) {
debuggerApi.removeRemoteObjectProperty(sessionId, remoteObjectId, propertyName);
}
}
void renameRemoteObjectProperty(RemoteObjectId remoteObjectId, String oldName, String newName) {
if (active) {
debuggerApi.renameRemoteObjectProperty(sessionId, remoteObjectId, oldName, newName);
}
}
/**
* Evaluates a given expression either on the active {@link CallFrame} if the
* debugger is currently paused, or on the global object if it is running.
*
* @param expression expression to evaluate
*/
void evaluateExpression(String expression) {
if (active) {
// Schedule-finally the evaluations to remove duplicates.
expressionsToEvaluate.add(expression);
expressionsEvaluateCommand.scheduleFinally();
}
}
private void sendEvaluateExpressionRequest(String expression) {
if (active) {
CallFrame callFrame = getActiveCallFrame();
if (callFrame != null) {
debuggerApi.evaluateExpressionOnCallFrame(sessionId, callFrame, expression);
} else {
debuggerApi.evaluateExpression(sessionId, expression);
}
}
}
void requestAllCssStyleSheets() {
if (active) {
debuggerApi.requestAllCssStyleSheets(sessionId);
}
}
void setStyleSheetText(String styleSheetId, String text) {
if (active) {
debuggerApi.setStyleSheetText(sessionId, styleSheetId, text);
}
}
void setBreakpoint(Breakpoint breakpoint) {
if (active && breakpoint.isActive()) {
BreakpointInfoImpl breakpointInfo = findBreakpointInfo(breakpoint);
if (breakpointInfo == null) {
breakpointInfo = new BreakpointInfoImpl(breakpoint,
sourceMapping.getRemoteBreakpoint(breakpoint));
breakpointInfos.add(breakpointInfo);
}
if (StringUtils.isNullOrEmpty(breakpointInfo.breakpointId)) {
// Send to the debugger if it's not yet resolved.
debuggerApi.setBreakpointByUrl(sessionId, breakpointInfo);
}
}
}
void removeBreakpoint(Breakpoint breakpoint) {
if (active && breakpoint.isActive()) {
BreakpointInfoImpl breakpointInfo = findBreakpointInfo(breakpoint);
if (breakpointInfo != null && !StringUtils.isNullOrEmpty(breakpointInfo.breakpointId)) {
debuggerApi.removeBreakpoint(sessionId, breakpointInfo.breakpointId);
} else {
Log.error(getClass(), "Breakpoint to remove not found: " + breakpoint);
}
}
}
void setBreakpointsEnabled(boolean enabled) {
if (active) {
debuggerApi.setBreakpointsActive(sessionId, enabled);
}
}
void sendCustomMessage(String message) {
if (active) {
debuggerApi.sendCustomMessage(sessionId, message);
}
}
@VisibleForTesting
BreakpointInfoImpl findBreakpointInfo(Breakpoint breakpoint) {
for (int i = 0, n = breakpointInfos.size(); i < n; ++i) {
BreakpointInfoImpl breakpointInfo = breakpointInfos.get(i);
if (breakpoint.equals(breakpointInfo.breakpoint)) {
return breakpointInfo;
}
}
return null;
}
private void setActive(boolean value) {
if (active != value) {
Preconditions.checkState(!value, "Reactivation of debugger is not supported");
reset();
active = value;
debuggerStateListenerManager.dispatch(DEBUGGER_STATE_DISPATCHER);
}
}
private void setOnPausedResponse(@Nullable OnPausedResponse response) {
if (lastOnPausedResponse != response) {
lastOnPausedResponse = response;
activeCallFrameIndex = 0;
debuggerStateListenerManager.dispatch(DEBUGGER_STATE_DISPATCHER);
}
}
private void reset() {
softReset();
active = false;
sourceMapping = null;
lastOnPausedResponse = null;
activeCallFrameIndex = 0;
breakpointInfos.clear();
}
/**
* Performs a "soft" reset to clear the data that does not survive a restart
* of an active debugger session. This happens when we choose to debug another
* application within an already open debugger session (and debuggee window).
*
* TODO: We should catch the corresponding event from the extension.
* The closest seems to be onGlobalObjectChanged, but it does not work.
*/
private void softReset() {
scriptParsedResponses = JsonCollections.createMap();
}
private void updateBreakpointInfo(OnBreakpointResolvedResponse response) {
for (int i = 0, n = breakpointInfos.size(); i < n; ++i) {
BreakpointInfoImpl breakpointInfo = breakpointInfos.get(i);
if (StringUtils.equalNonEmptyStrings(response.getBreakpointId(), breakpointInfo.breakpointId)
|| breakpointInfo.equalsTo(response.getBreakpointInfo())) {
if (!StringUtils.isNullOrEmpty(response.getBreakpointId())) {
breakpointInfo.breakpointId = response.getBreakpointId();
} else {
Log.error(getClass(), "Empty breakpointId in the response!");
}
breakpointInfo.locations.addAll(response.getLocations());
break;
}
}
}
private void removeBreakpointById(String breakpointId) {
for (int i = 0, n = breakpointInfos.size(); i < n; ++i) {
BreakpointInfoImpl breakpointInfo = breakpointInfos.get(i);
if (breakpointId.equals(breakpointInfo.breakpointId)) {
breakpointInfos.remove(i);
break;
}
}
}
/**
* Implementation of {@link BreakpointInfo} that also contains information
* received from the debugger.
*/
@VisibleForTesting
static class BreakpointInfoImpl implements BreakpointInfo {
private final Breakpoint breakpoint;
private final BreakpointInfo delegate;
// Populated from the debugger responses.
private String breakpointId;
private final JsonArray<Location> locations = JsonCollections.createArray();
private BreakpointInfoImpl(Breakpoint breakpoint, BreakpointInfo delegate) {
this.breakpoint = breakpoint;
this.delegate = delegate;
}
@Override
public String getUrl() {
return delegate.getUrl();
}
@Override
public String getUrlRegex() {
return delegate.getUrlRegex();
}
@Override
public int getLineNumber() {
return delegate.getLineNumber();
}
@Override
public int getColumnNumber() {
return delegate.getColumnNumber();
}
@Override
public String getCondition() {
return delegate.getCondition();
}
public Breakpoint getBreakpoint() {
return breakpoint;
}
public String getBreakpointId() {
return breakpointId;
}
public JsonArray<Location> getLocations() {
return locations.copy();
}
private boolean equalsTo(BreakpointInfo breakpointInfo) {
return breakpointInfo != null
&& StringUtils.equalStringsOrEmpty(getUrl(), breakpointInfo.getUrl())
&& getLineNumber() == breakpointInfo.getLineNumber()
&& getColumnNumber() == breakpointInfo.getColumnNumber()
&& StringUtils.equalStringsOrEmpty(getCondition(), breakpointInfo.getCondition());
}
}
}