package xtc.lang.blink;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import xtc.lang.blink.CallStack.LocalVariable;
import xtc.lang.blink.CallStack.NativeCallFrame;
import xtc.lang.blink.CallStack.NativeCallFrame.NativeFrameType;
import xtc.lang.blink.Event.DummyCallCompletionEvent;
import xtc.lang.blink.Event.Java2NativeCallEvent;
import xtc.lang.blink.Event.Java2NativeReturnEvent;
import xtc.lang.blink.Event.Native2JavaCallEvent;
import xtc.lang.blink.Event.J2CBreakPointHitEvent;
import xtc.lang.blink.Event.Native2JavaCompletionEvent;
import xtc.lang.blink.Event.Native2JavaReturnEvent;
import xtc.lang.blink.Event.NativeBreakPointHitEvent;
import xtc.lang.blink.Event.NativeJNIWarningEvent;
import xtc.lang.blink.Event.NativeStepCompletionEvent;
import xtc.lang.blink.Event.RawTextMessageEvent;
import xtc.lang.blink.EventLoop.ReplyHandler;
import xtc.lang.blink.SymbolMapper.SourceFileAndLine;
import xtc.lang.blink.agent.AgentNativeDeclaration;
/** Microsoft CDB driver as a native component debugger. */
public class NativeCDB extends StdIOProcess
implements NativeDebugger, AgentNativeDeclaration {
/** CDB user command prompt. */
private static final String PROMPT_PATTERN = "\\d+\\:\\d+\\> ";
/** The pattern cache. */
private static final HashMap<String,Pattern> patternCache =
new HashMap<String,Pattern>();
/**
* Find or create regular expression pattern.
* @param patternString The pattern string.
* @return The regular expression pattern.
*/
private static final Pattern p(String patternString) {
Pattern pattern = patternCache.get(patternString);
if (pattern == null) {
pattern = Pattern.compile(patternString);
patternCache.put(patternString, pattern);
}
return pattern;
}
/** CDB internal event to notify that CDB is attached to the target JVM. */
private static class CDBAttachEvent extends Event {
CDBAttachEvent(NativeCDB g) {
super(g, EventConsumer.BlinkController);
}
public String getName() {return "CDBAttachEvent";}
}
/** CDB internal output message event. */
private static class CDBRawMessageEvent extends Event {
private final String message;
CDBRawMessageEvent(NativeCDB g, String message) {
super(g, EventConsumer.BlinkController);
this.message = message;
}
public String getMessage() {return message;}
public String getName() {return "CDBRawMessageEvent";}
public String toString() {
return super.toString() + message;
}
}
/**
* Debug agent's internal breakpoint hit event. This internal event could be
* interpreted as on one of the DebugContextSwitching events and Language
* transition events.
*/
private static class AgentBreakPointHitEvent extends Event {
private final int bpID; //CDB break point identifier.
private final String message; //CDB's detailed message.
AgentBreakPointHitEvent(NativeCDB g, int bpID, String message) {
super(g, EventConsumer.NativerDebugger);
this.bpID = bpID;
this.message = message;
}
/** Getters. */
public int getBpID() {return bpID;}
public String getMessage() {return message;}
public String getName() {return "AgentInternalBreakPointHit";}
}
/** Internal step completion event. */
private static class InternalStepCompletionEvent extends Event {
InternalStepCompletionEvent(NativeCDB g) {
super(g, EventConsumer.NativerDebugger);
}
public String getName() {return "InternalStepCompletionEvent";}
}
/** The internal native breakpoint within Blink agent. */
private int cbpBreakPointId;
/** Beginning of the Agent native shared library. */
private long agent_address_begin;
/** Ending of the Agent native shared library. */
private long agent_address_end;
/** The next break point identifier. */
private int nextBreakPointIdentifier;
/** Whether or not the CDB is attached to the JVM debugee. */
private boolean cdbAttached = false;
/** Whether or not this CDB driver activated xtc.lang.agent.Agent.dummyJava. */
private volatile boolean callDummyRequested = false;
/**
* Whether or not this CDB driver yielded to Java debugger by activating a
* call to xtc.lang.agent.Agent.c2j().
*/
private volatile boolean callNative2JavaRequested = false;
/**
* Whether or not the CDB source-level single-stepping was requested and is
* being processed.
*/
private volatile boolean stepRequested = false;
/**
* Internal raw message buffer to capture interesting event such as break
* point hit. See processMeaageEvent. This buffer chops CDB's raw output text
* message by CDB's prompt.
*/
private final StringBuffer sbStdout = new StringBuffer();
/**
* Constructor.
*
* @param dbg The Blink debugger.
* @param name The user friendly name.
*/
public NativeCDB(Blink dbg, String name) {
super(dbg, name);
}
/**
* Process message for internal processing.
*
* @param e The event.
*/
void processMessageEvent(RawTextMessageEvent e) {
sbStdout.append(e.getMessage());
Pattern p1 = Pattern.compile(PROMPT_PATTERN);
Matcher m1 = p1.matcher(sbStdout);
if (m1.find()) {
boolean hasReminder = !Pattern.compile("(?:.*\n)*" + PROMPT_PATTERN + "$")
.matcher(sbStdout).matches();
String[] frags = p1.split(sbStdout);
for(int i = 0; i < frags.length-1;i++) {
processCDBRawMessage(frags[i]);
}
String lastOne = frags.length > 0 ? frags[frags.length-1] :"";
if (hasReminder) {
sbStdout.setLength(0);
sbStdout.append(lastOne);
} else {
processCDBRawMessage(lastOne);
sbStdout.setLength(0);
}
}
}
/**
* Process a CDB raw message.
*
* @param s The message.
*/
private void processCDBRawMessage(String s) {
if (!cdbAttached) {
dbg.enqueEvent(new CDBAttachEvent(this));
cdbAttached = true;
return;
}
//now check any interesting event.
Pattern breakpointHitPattern = p("(?:.*\n)*Breakpoint (\\d+) hit\n" + "((?:.*\n)*)");
Matcher breakpointHitMatcher = breakpointHitPattern.matcher(s);
if (breakpointHitMatcher.find()) {
int bpid = Integer.parseInt(breakpointHitMatcher.group(1));
String msg = breakpointHitMatcher.group(2);
stepRequested = false;
//check internal break point hit.
if (bpid == cbpBreakPointId) {
dbg.enqueEvent(new AgentBreakPointHitEvent(this, bpid, msg));
} else {
// visible native break point.
dbg.enqueEvent(new NativeBreakPointHitEvent(this, bpid, msg));
}
return;
}
if (callDummyRequested || callNative2JavaRequested) {
Pattern callReturnPattern = p(".call returns:\n" + "(.+)\\n" + "(?:.*\n)*");
Matcher callReturnMatcher = callReturnPattern.matcher(s);
if (callReturnMatcher.find()) {
assert callDummyRequested ^ callNative2JavaRequested;
if (callDummyRequested) {
callDummyRequested = false;
dbg.enqueEvent(new DummyCallCompletionEvent(this));
} else if (callNative2JavaRequested) {
callNative2JavaRequested = false;
dbg.enqueEvent(new Native2JavaCompletionEvent(this));
}
return;
}
}
if (stepRequested) {
dbg.enqueEvent(new InternalStepCompletionEvent(this));
stepRequested = false;
return;
}
dbg.enqueEvent(new CDBRawMessageEvent(this, s));
}
/**
* Process an event in the main thread.
*
* @param e The event.
*/
public void dispatch(Event e) {
if (e instanceof AgentBreakPointHitEvent) {
dispatch((AgentBreakPointHitEvent)e);
} else if (e instanceof InternalStepCompletionEvent) {
try {
String eipString = raeLine("r eip\n", "eip=([0-9a-f]+)")[1];
long eip = Long.parseLong(eipString, 16);
assert agent_address_begin != 0L && agent_address_end != 0L;
if (isInAgentLibrary(eip)) {
sendMessage("G\n");
} else {
dbg.enqueEvent(new NativeStepCompletionEvent(this));
}
} catch(IOException ioe) {
dbg.err("can not correctly handle step completion.\n");
}
}
}
/**
* Process an internal agent breakpoint hit event in the event handler
* thread.
*
* @param e The event.
*/
private void dispatch(AgentBreakPointHitEvent e) {
try {
if (e.getBpID() == cbpBreakPointId) {
String bptype = readEnum(BDA_CBP_BPTYPE);
if (bptype.equals(BDA_BPTYPE_J2C_DEBUGGER)) {
dbg.enqueEvent(new J2CBreakPointHitEvent(this));
} else if (bptype.equals(BDA_BPTYPE_J2C_JNI_CALL)) {
long native_target_address = readAddressValue(BDA_CBP_TARGET_NATIVE_ADDRESS);
advance(native_target_address);
rae("t\n");
dbg.enqueEvent(new Java2NativeCallEvent(this));
} else if (bptype.equals(BDA_BPTYPE_J2C_JNI_RETURN)) {
String cname = readStringValue(BDA_CBP_TARGET_CNAME);
int lineNumber = readIntValue(BDA_CBP_TARGET_LINE_NUMBER);
dbg.enqueEvent(new Java2NativeReturnEvent(this, cname, lineNumber));
} else if (bptype.equals(BDA_BPTYPE_C2J_JNI_CALL)) {
String cname = readStringValue(BDA_CBP_TARGET_CNAME);
int lineNumber = readIntValue(BDA_CBP_TARGET_LINE_NUMBER);
dbg.enqueEvent(new Native2JavaCallEvent(this, cname, lineNumber));
} else if (bptype.equals(BDA_BPTYPE_C2J_JNI_RETURN)) {
rae("gu\n"); // bda_cbp -> jni_state_c2j_return
rae("gu\n"); // jni_state_c2j_return -> c2j_proxyCallXXXMethod
rae("gu\n"); // caller of the (*env)->CallXXXMethod
dbg.enqueEvent(new Native2JavaReturnEvent(this));
} else if (bptype.equals(BDA_BPTYPE_JNI_WARNING)) {
String message = readStringValue(BDA_CBP_STATE_MESSAGE);
dbg.enqueEvent(new NativeJNIWarningEvent(this, message));
} else {
assert false : "can not recognize an internal break point";
}
} else {
assert false : "can not recognize an internal break point";
}
} catch(IOException ioe) {
dbg.err("can not correctly handle internal break point.");
}
}
/**
* Read a string typed value.
* @param name The variable name.
* @return The string value.
*/
private String readStringValue(String name) throws IOException {
String s = raeLine("?? " + name + "\n", " \"(.+)\"")[1];
return s;
}
/**
* Read an integer typed value.
* @param name The variable name.
* @return The integer value.
*/
private int readIntValue(String name) throws IOException {
String s = raeLine("?? " + name + "\n", "int ([0-9]+)")[1];
return Integer.parseInt(s);
}
/**
* Read an address typed value.
* @param name The variable name.
* @return The address.
*/
private long readAddressValue(String name) throws IOException {
String s = raeLine("?? " + name + "\n", "^.+0x([0-9a-f]+)$")[1];
return Long.parseLong(s, 16);
}
/**
* Read an enumeration typed value.
* @param name The variable name.
* @return The enumeration name.
*/
private String readEnum(String name) throws IOException {
return raeLine("?? " + name + "\n", "^.+ (.+) .*$")[1];
}
/**
* Attach CDB to the debugee JVM.
* @param pid The process id of the debbugee.
*/
public void attach(int pid) throws IOException {
final String[] cdbCommandArray = new String[] {
"cdb",
"-lines",
"-G",
"-pid", String.valueOf(pid),
};
begin(cdbCommandArray);
EventLoop.subLoop(dbg, new ReplyHandler() {
public boolean dispatch(Event e) {
if (e instanceof CDBAttachEvent) {
return true;
} else {
return false;
}
}
});
if (dbg.options.getVerboseLevel() >= 1) {
dbg.out("cdb is initialized.\n");
}
cbpBreakPointId = createSymbolBreakPoint(BDA_CBP);
nextBreakPointIdentifier = cbpBreakPointId + 1;
Pattern p = Pattern.compile("([0-9a-f]+) ([0-9a-f]+) +([^ ]+) +.+");
String sharedLibries = rae("lm\n");
for (StringTokenizer t = new StringTokenizer(sharedLibries, "\n"); t
.hasMoreElements();) {
Matcher m = p.matcher(t.nextToken());
if (m.matches()) {
String name = m.group(3);
if (name.equals("jdwp")) {
String begin = m.group(1);
String end = m.group(2);
setVariable(BDA_JDWP_REGION_BEGIN_VARIABLE, begin);
setVariable(BDA_JDWP_REGION_END_VARIABLE, end);
} else if (name.equals(BDA_SHARED_LIBRARY_NAME)) {
String begin = m.group(1);
String end = m.group(2);
agent_address_begin = Long.parseLong(begin, 16);
agent_address_end = Long.parseLong(end, 16);
}
}
}
sendMessage("G\n");
}
/** Detach and terminate the CDB. */
public void detach() throws IOException {
sendMessage(".detach\nQ\n");
}
/** Active a native-to-Java transition. */
public void callNative2Java() throws IOException {
String callCommand = ".call " + BDA_C2J +"()\n";
rae(callCommand);
callNative2JavaRequested = true;
sendMessage("G\n");
}
/** Continue the debugee JVM. */
public void cont() throws IOException {
sendMessage("G\n");
}
/** Abruptly termindate the debugee JVM. */
public void quit() throws IOException {
sendMessage("Q\n");
}
/**
* Run a CDB command and return the output message. This is for
* internal-debugging purpose.
*
* @param command The CDB command.
*/
public String runCommand(String command) throws IOException {
return rae(command + "\n");
}
/**
* Create a native breakpoint, and return the breaakpoint identifier.
*
* @param sourceFile The source file.
* @param line The line number.
* @return The breakpoint identifier.
*/
public int createBreakpoint(String sourceFile, int line) throws IOException {
int bpid = nextBreakPointID();
String cmd ="bp" + bpid + " `" + sourceFile + ":" + line +"`\n";
String rst = rae(cmd);
if (rst != null && rst.length() > 0) {
dbg.out(rst); // perhaps some problem.
}
return bpid;
}
public int createBreakpoint(String symbol) throws IOException {
int bpid = nextBreakPointID();
String cmd ="bp" + bpid + " " + symbol + "\n";
String rst = rae(cmd);
if (rst != null && rst.length() > 0) {
dbg.out(rst); // perhaps some problem.
}
return bpid;
}
/**
* Create a native breakpiont, and return the breakpoint identifier.
*
* @param symbol The symbol name.
* @return The breakpoint identifier.
*/
private int createSymbolBreakPoint(String symbol) throws IOException {
String[] rsts = raeLine(
"bm " + symbol + "\n",
"^\\s*(\\d+)\\: \\p{XDigit}+ \\@\\!\"(.+)\"$"
);
int bpid = Integer.valueOf(rsts[1]);
return bpid;
}
/**
* Get a new breakpiont identifier.
*
* @return The breakpint identifier.
*/
private int nextBreakPointID() {
return nextBreakPointIdentifier++;
}
/**
* Advance the program counter to a specified address.
*
* @param address The target native address.
*/
private void advance(long address) throws IOException {
final int bpid = nextBreakPointID();
rae("bp" + bpid + " /1 " + Long.toHexString(address) + "\n");
sendMessage("g\n");
EventLoop.subLoop(dbg, new ReplyHandler() {
public boolean dispatch(Event e) {
if (e instanceof NativeBreakPointHitEvent) {
NativeBreakPointHitEvent ne = (NativeBreakPointHitEvent)e;
if (bpid == ne.getDebuggerBreakpointID()) {
return true;
}
}
return false;
}
});
}
/**
* Enable a breakpoint.
*
* @param bpid The breakpoint identifier.
*/
public void enableBreakpoint(int bpid) throws IOException {
rae("be " + bpid + "\n");
}
/**
* Disable a breakpoint.
*
* @param bpid The breakpoint identifier.
*/
public void disableBreakpoint(int bpid) throws IOException {
rae("bd " + bpid + "\n");
}
/**
* Delete a breakpoint.
* @param bpid The breakpoint identifier.
*/
public void deleteBreakpoint(int bpid) throws IOException {
assert false : "Not implemented";
}
public String getJNIEnv() throws IOException {
rae(".call " + BDA_ENSURE_JNIENV + "()\n");
String jnienv = raeLine("g\n",
"struct JNINativeInterface_ \\*\\* (0x\\p{XDigit}+)")[1];
return jnienv;
}
/** Followings are for inspecting calling context and program state.*/
public List<NativeCallFrame> getFrames() throws IOException {
// extract frames
String framesText = rae("kn\n");
LinkedList<NativeCallFrame> frames = new LinkedList<NativeCallFrame>();
for (StringTokenizer t = new StringTokenizer(framesText, "\n"); t
.hasMoreElements();) {
String l = t.nextToken();
Pattern p = p("^(\\p{XDigit}+) (\\p{XDigit}+) (\\p{XDigit}+) (\\S+)(?: (\\[.+\\]))?");
Matcher m = p.matcher(l);
if (m.matches()) {
int frameID = Integer.valueOf(m.group(1), 16);
String position = m.group(4);
String sourceInfo = m.group(5);
//parse position
String functionName;
NativeFrameType frameType = NativeFrameType.NORMAL;
Pattern posPattern = p("(.+)\\!(.+)(?:\\+0x(\\p{XDigit}+))?|(.+)\\+0x(\\p{XDigit}+)|0x(\\p{XDigit}+)");
Matcher posMatcher = posPattern.matcher(position);
boolean matched = posMatcher.matches();
assert matched == true : "can not parse position: " + position + "\n";
if (posMatcher.group(1) != null) {
String moduleName = posMatcher.group(1);
String funcName = posMatcher.group(2);
functionName = funcName;
if (moduleName.equals(BDA_SHARED_LIBRARY_NAME)) {
if (funcName.startsWith("bda_j2c_proxy")) {
frameType = NativeFrameType.J2C_PROXY;
} else if (funcName.startsWith("bda_c2j_proxy")) {
frameType = NativeFrameType.C2J_PROXY;
}
}
} else if (posMatcher.group(4) != null) {
String moduleName = posMatcher.group(4);
int moduleOffset = Integer.parseInt(posMatcher.group(5), 16);
functionName = moduleName + "+0x" + Integer.toHexString(moduleOffset);
} else {
//raw address
assert posMatcher.group(6) != null;
functionName = "0x" + posMatcher.group(6);
}
//parse sourceInfo
String sourceFile = null;
int lineno = 1;
if (sourceInfo!= null) {
Pattern frameSourceInfoPattern = p("\\[(.+) \\@ ([0-9]+)\\]");
Matcher frameSourceInfoMatcher = frameSourceInfoPattern.matcher(sourceInfo);
if (frameSourceInfoMatcher.matches()) {
sourceFile = frameSourceInfoMatcher.group(1);
lineno = Integer.parseInt(frameSourceInfoMatcher.group(2));
}
}
NativeCallFrame frame = new NativeCallFrame(frameID, functionName, sourceFile, lineno,
frameType);
frames.addLast(frame);
} else if (l.matches("^WARNING: .+$")) {
if (dbg.options.getVerboseLevel() >= 1) {
dbg.err("ignoring CDB output: " + l + "\n");
}
} else if (l.matches("\\s+#\\s+ChildEBP\\s+RetAddr\\s*")) {
//skip header.
} else {
assert false :"can not recognize CDB output: " + l;
}
}
return frames;
}
public SourceFileAndLine getCurrentLocation() throws IOException {
SourceFileAndLine loc = null;
String frames = rae("kn1\n");
Pattern p = Pattern.compile("0+ [0-9a-f]+ [0-9a-f]+ .+ \\[(.+) @ ([0-9]+)\\]");
StringTokenizer t = new StringTokenizer(frames, "\n");
while(t.hasMoreTokens()) {
String s = t.nextToken();
Matcher m = p.matcher(s);
if (m.matches()) {
String file = m.group(1);
int line = Integer.parseInt(m.group(2));
loc = new SourceFileAndLine(file, line);
}
}
return loc;
}
public String getSourceLines(String filename, int line, int count) throws IOException {
assert filename != null && line >=0 && count >=0;
rae("lsf " + filename + "\n");
String lines = rae("ls " + line + ", " + count + "\n");
return lines;
}
public List<LocalVariable> getLocals(NativeCallFrame f) throws IOException {
LinkedList<LocalVariable> localList = new LinkedList<LocalVariable>();
rae(".frame " + f.getFrameID() + "\n");
String localsText = rae("dv\n");
Pattern localVariablePattern = Pattern.compile("^\\s*(.+) = (.+)$");
for(StringTokenizer t = new StringTokenizer(localsText, "\n");
t.hasMoreElements();) {
String l = t.nextToken();
Matcher m = localVariablePattern.matcher(l);
if (m.matches()) {
String name = m.group(1);
String value = m.group(2);
LocalVariable v = new LocalVariable(name, value);
localList.addLast(v);
} else
assert false : "CDB: can not recognize: " + l + "\n";
}
return localList;
}
/* Followings are for inter-language stepping. */
public void step() throws IOException {
rae("l+t\n");
stepRequested = true;
sendMessage("t\n");
}
public void next() throws IOException {
rae("l+t\n");
stepRequested = true;
sendMessage("p\n");
}
public int getLanguageTransitionCount() throws IOException {
rae(".call " + BDA_GET_CURRENT_TRANSITION_COUNT + "()\n");
String countString = raeLine("g\n", "int (\\d+)")[1];
return Integer.parseInt(countString);
}
private static String getBreakpointControlVariable(LanguageTransitionEventType bptype) {
switch (bptype) {
case J2C_CALL:
return BDA_J2C_CALL_BREAKPOINT_VARIABLE;
case J2C_RETURN:
return BDA_J2C_RETURN_BREAKPOINT_VARIABLE;
case C2J_CALL:
return BDA_C2J_CALL_BREAKPOINT_VARIABLE;
case C2J_RETURN:
return BDA_C2J_RETURN_BREAKPOINT_VARIABLE;
default:
assert false : "not reachable";
return "";
}
}
public void setTransitionBreakPoint(LanguageTransitionEventType bptype, int transitionCount)
throws IOException {
String controlVariable = getBreakpointControlVariable(bptype);
rae("ed " + controlVariable + " 1\n");
rae("ed " + BDA_TRANSITION_COUNT + " " + transitionCount +"\n");
}
public void clearTransitionBreakPoint(LanguageTransitionEventType bptype)
throws IOException {
String controlVariable = getBreakpointControlVariable(bptype);
rae("ed " + controlVariable + " 0\n");
}
public void callJavaDummy() throws IOException {
rae(".call " + BDA_DUMMY_JAVA + "()" + "\n");
callDummyRequested = true;
sendMessage("g\n");
}
private boolean isInAgentLibrary(long addr) {
return agent_address_begin <= addr && addr < agent_address_end;
}
/** Following are for expression evaluation. */
public String eval(NativeCallFrame f, String expr) throws IOException {
assert false : "Not implemented";
return null;
}
public void setVariable(NativeCallFrame f, String name, String expr)
throws IOException {
assert false : "Not implemented";
}
public String whatis(NativeCallFrame f, String expr) throws IOException {
assert false : "Not implemented";
return null;
}
private void setVariable(String name, String value) throws IOException {
rae("ed " + name + " " + value +"\n");
}
/**
* Run a CDB command, and get text message from the CDB.
*
* @param cmd The command.
* @return The result message.
*/
private String rae(final String cmd) throws IOException {
sendMessage(cmd);
return (String) EventLoop.subLoop(dbg, new ReplyHandler() {
public boolean dispatch(Event e) {
if (e instanceof CDBRawMessageEvent) {
CDBRawMessageEvent ge =(CDBRawMessageEvent)e;
setResult(ge.getMessage());
return true;
}
return false;
}
});
}
/**
* Run a CDB command and search for a expected line until the CDB prompt. At
* the end, return an array of Strings from the matching regular expression.
* If multiple CDB output lines match the expected message, take the last one.
*
* @param cmd The command.
* @param expect The expected line.
* @return The matcher object.
*/
private String[] raeLine(final String cmd, final String expect) throws IOException {
final Pattern p = Pattern.compile(expect);
sendMessage(cmd);
return (String[]) EventLoop.subLoop(dbg, new ReplyHandler() {
public boolean dispatch(Event e) {
if (e instanceof CDBRawMessageEvent) {
CDBRawMessageEvent ge =(CDBRawMessageEvent)e;
String[] lines = ge.getMessage().split("\n");
for(String l :lines) {
Matcher m = p.matcher(l);
if (m.matches()) {
String[] frags = new String[m.groupCount()+1];
for(int i = 0; i <= m.groupCount();i++) {
frags[i] = m.group(i);
}
setResult(frags);
return true;
}
}
setResult(null);
return true;
}
return false;
}
});
}
}