/******************************************************************************* * Copyright (c) 2000, 2009 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * QNX Software Systems - Initial API and implementation * Wind River Systems - Modified for new DSF Reference Implementation *******************************************************************************/ package org.eclipse.cdt.dsf.mi.service.command.output; import java.util.ArrayList; import java.util.List; /** <pre> `OUTPUT :' `( OUT-OF-BAND-RECORD )* [ RESULT-RECORD ] "(gdb)" NL' `RESULT-RECORD :' ` [ TOKEN ] "^" RESULT-CLASS ( "," RESULT )* NL' `OUT-OF-BAND-RECORD :' `ASYNC-RECORD | STREAM-RECORD' `ASYNC-RECORD :' `EXEC-ASYNC-OUTPUT | STATUS-ASYNC-OUTPUT | NOTIFY-ASYNC-OUTPUT' `EXEC-ASYNC-OUTPUT :' `[ TOKEN ] "*" ASYNC-OUTPUT' `STATUS-ASYNC-OUTPUT :' `[ TOKEN ] "+" ASYNC-OUTPUT' `NOTIFY-ASYNC-OUTPUT :' `[ TOKEN ] "=" ASYNC-OUTPUT' `ASYNC-OUTPUT :' `ASYNC-CLASS ( "," RESULT )* NL' `RESULT-CLASS :' `"done" | "running" | "connected" | "error" | "exit"' `ASYNC-CLASS :' `"stopped" | OTHERS' (where OTHERS will be added depending on the needs--this is still in development). `RESULT :' ` VARIABLE "=" VALUE' `VARIABLE :' ` STRING ' `VALUE :' ` CONST | TUPLE | LIST ' `CONST :' `C-STRING' `TUPLE :' ` "{}" | "{" RESULT ( "," RESULT )* "}" ' `LIST :' ` "[]" | "[" VALUE ( "," VALUE )* "]" | "[" RESULT ( "," RESULT )* "]" ' `STREAM-RECORD :' `CONSOLE-STREAM-OUTPUT | TARGET-STREAM-OUTPUT | LOG-STREAM-OUTPUT' `CONSOLE-STREAM-OUTPUT :' `"~" C-STRING' `TARGET-STREAM-OUTPUT :' `"@" C-STRING' `LOG-STREAM-OUTPUT :' `"&" C-STRING' `NL :' `CR | CR-LF' `TOKEN :' _any sequence of digits_. `C-STRING :' `""" SEVEN-BIT-ISO-C-STRING-CONTENT """' </pre> */ public class MIParser { public enum RecordType { ResultRecord, OOBRecord, PrimaryPrompt } public String primaryPrompt = "(gdb)"; //$NON-NLS-1$ public String cliPrompt = primaryPrompt; public String secondaryPrompt = ">"; //$NON-NLS-1$ public RecordType getRecordType(String line) { int i = 0; if (Character.isDigit(line.charAt(0))) { i = 1; while (i < line.length() && Character.isDigit(line.charAt(i))) { i++; } } if (i < line.length() && line.charAt(i) == '^') { return RecordType.ResultRecord; } else if (line.startsWith(primaryPrompt, i)) { return RecordType.PrimaryPrompt; //break; // Do nothing. } else { return RecordType.OOBRecord; } } /** * */ public MIResultRecord parseMIResultRecord(String line) { StringBuffer buffer = new StringBuffer(line); // Fetch the Token/Id int id = parseToken(buffer); // Consume the '^' buffer.deleteCharAt(0); MIResultRecord rr = new MIResultRecord(); rr.setToken(id); if (buffer.toString().startsWith(MIResultRecord.DONE)) { rr.setResultClass(MIResultRecord.DONE); buffer.delete(0, MIResultRecord.DONE.length()); } else if (buffer.toString().startsWith(MIResultRecord.ERROR)) { rr.setResultClass(MIResultRecord.ERROR); buffer.delete(0, MIResultRecord.ERROR.length()); } else if (buffer.toString().startsWith(MIResultRecord.EXIT)) { rr.setResultClass(MIResultRecord.EXIT); buffer.delete(0, MIResultRecord.EXIT.length()); } else if (buffer.toString().startsWith(MIResultRecord.RUNNING)) { rr.setResultClass(MIResultRecord.RUNNING); buffer.delete(0, MIResultRecord.RUNNING.length()); } else if (buffer.toString().startsWith(MIResultRecord.CONNECTED)) { rr.setResultClass(MIResultRecord.CONNECTED); buffer.delete(0, MIResultRecord.CONNECTED.length()); } else { // Error throw an exception? } // Results are separated by commas. if (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); MIResult[] res = processMIResults(new FSB(buffer)); rr.setMIResults(res); } return rr; } /** * Find OutOfBand Records depending on the starting token. */ public MIOOBRecord parseMIOOBRecord(String line) { StringBuffer buffer = new StringBuffer(line); int id = parseToken(buffer); MIOOBRecord oob = null; char c = buffer.length() != 0 ? buffer.charAt(0) : 0; if (c == '*' || c == '+' || c == '=') { // Consume the first char buffer.deleteCharAt(0); MIAsyncRecord async = null; switch (c) { case '*' : async = new MIExecAsyncOutput(); break; case '+' : async = new MIStatusAsyncOutput(); break; case '=' : async = new MINotifyAsyncOutput(); break; } async.setToken(id); // Extract the Async-Class int i = buffer.toString().indexOf(','); if (i != -1) { String asyncClass = buffer.substring(0, i); async.setAsyncClass(asyncClass); // Consume the async-class and the comma buffer.delete(0, i + 1); } else { async.setAsyncClass(buffer.toString().trim()); buffer.setLength(0); } MIResult[] res = processMIResults(new FSB(buffer)); async.setMIResults(res); oob = async; } else if (c == '~' || c == '@' || c == '&') { // Consume the first char buffer.deleteCharAt(0); MIStreamRecord stream = null; switch (c) { case '~' : stream = new MIConsoleStreamOutput(); break; case '@' : stream = new MITargetStreamOutput(); break; case '&' : stream = new MILogStreamOutput(); break; } // translateCString() assumes that the leading " is deleted if (buffer.length() > 0 && buffer.charAt(0) == '"') { buffer.deleteCharAt(0); } stream.setCString(translateCString(new FSB(buffer))); oob = stream; } else { // Badly format MI line, just pass it to the user as target stream MIStreamRecord stream = new MITargetStreamOutput(); stream.setCString(line + "\n"); //$NON-NLS-1$ oob = stream; } return oob; } private int parseToken(StringBuffer buffer) { int id = -1; // Fetch the Token/Id if (Character.isDigit(buffer.charAt(0))) { int i = 1; while (i < buffer.length() && Character.isDigit(buffer.charAt(i))) { i++; } String numbers = buffer.substring(0, i); try { id = Integer.parseInt(numbers); } catch (NumberFormatException e) { } // Consume the token. buffer.delete(0, i); } return id; } /** * Assuming that the usual leading comma was consumed. * Extract the MI Result comma seperated responses. */ private MIResult[] processMIResults(FSB buffer) { List<MIResult> aList = new ArrayList<MIResult>(); MIResult result = processMIResult(buffer); if (result != null) { aList.add(result); } while (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); result = processMIResult(buffer); if (result != null) { aList.add(result); } } return aList.toArray(new MIResult[aList.size()]); } /** * Construct the DsfMIResult. Characters will be consume/delete * moving forward constructing the AST. */ private MIResult processMIResult(FSB buffer) { MIResult result = new MIResult(); int equal; if (buffer.length() > 0 && Character.isLetter(buffer.charAt(0)) && (equal = buffer.indexOf('=')) != -1) { String variable = buffer.substring(0, equal); result.setVariable(variable); buffer.delete(0, equal + 1); MIValue value = processMIValue(buffer); result.setMIValue(value); } else if(buffer.length()>0 && buffer.charAt(0)=='"') { // This an error but we just swallow it and move on. MIValue value = processMIValue(buffer); result.setMIValue(value); } else { result.setVariable(buffer.toString()); result.setMIValue(new MIConst()); // Empty string:??? buffer.setLength(0); } return result; } /** * Find a DsfMIValue implementation or return null. */ private MIValue processMIValue(FSB buffer) { MIValue value = null; if (buffer.length() > 0) { if (buffer.charAt(0) == '{') { buffer.deleteCharAt(0); value = processMITuple(buffer); } else if (buffer.charAt(0) == '[') { buffer.deleteCharAt(0); value = processMIList(buffer); } else if (buffer.charAt(0) == '"') { buffer.deleteCharAt(0); MIConst cnst = new MIConst(); cnst.setCString(translateCString(buffer)); value = cnst; } } return value; } /** * Assuming the starting '{' was deleted form the StringBuffer, * go to the closing '}' consuming/deleting all the characters. * This is usually call by processMIvalue(); */ private MIValue processMITuple(FSB buffer) { MITuple tuple = new MITuple(); List<MIValue> valueList = new ArrayList<MIValue>(); List<MIResult> resultList = new ArrayList<MIResult>(); // Catch closing '}' while (buffer.length() > 0 && buffer.charAt(0) != '}') { // Try for the DsfMIValue first MIValue value = processMIValue(buffer); if (value != null) { valueList.add(value); } else { MIResult result = processMIResult(buffer); if (result != null) { resultList.add(result); } } if (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); } } if (buffer.length() > 0 && buffer.charAt(0) == '}') { buffer.deleteCharAt(0); } MIValue[] values = valueList.toArray(new MIValue[valueList.size()]); MIResult[] res = resultList.toArray(new MIResult[resultList.size()]); tuple.setMIValues(values); tuple.setMIResults(res); return tuple; } /** * Assuming the leading '[' was deleted, find the closing * ']' consuming/delete chars from the StringBuffer. */ private MIValue processMIList(FSB buffer) { MIList list = new MIList(); List<MIValue> valueList = new ArrayList<MIValue>(); List<MIResult> resultList = new ArrayList<MIResult>(); // catch closing ']' while (buffer.length() > 0 && buffer.charAt(0) != ']') { // Try for the DsfMIValue first MIValue value = processMIValue(buffer); if (value != null) { valueList.add(value); } else { MIResult result = processMIResult(buffer); if (result != null) { resultList.add(result); } } if (buffer.length() > 0 && buffer.charAt(0) == ',') { buffer.deleteCharAt(0); } } if (buffer.length() > 0 && buffer.charAt(0) == ']') { buffer.deleteCharAt(0); } MIValue[] values = valueList.toArray(new MIValue[valueList.size()]); MIResult[] res = resultList.toArray(new MIResult[resultList.size()]); list.setMIValues(values); list.setMIResults(res); return list; } /* * MI C-String rather MICOnst values are enclose in double quotes * and any double quotes or backslash in the string are escaped. * Assuming the starting double quote was removed. * This method will stop at the closing double quote remove the extra * backslach escaping and return the string __without__ the enclosing double quotes * The orignal StringBuffer will move forward. */ private String translateCString(FSB buffer) { boolean escape = false; boolean closingQuotes = false; StringBuffer sb = new StringBuffer(); int index = 0; for (; index < buffer.length() && !closingQuotes; index++) { char c = buffer.charAt(index); if (c == '\\') { if (escape) { sb.append(c); sb.append(c); escape = false; } else { escape = true; } } else if (c == '"') { if (escape) { sb.append(c); escape = false; } else { // Bail out. closingQuotes = true; } } else { if (escape) { sb.append('\\'); } sb.append(c); escape = false; } } buffer.delete(0, index); return sb.toString(); } /** * Tests if this string starts with the specified prefix beginning * a specified index. * * @param value the string. * @param prefix the prefix. * @return <code>true</code> if prefix starts value. */ public boolean startsWith(StringBuffer value, String prefix) { int vlen = value.length(); int plen = prefix.length(); if (vlen < plen) { return false; } for (int i = 0; i < plen; i++) { if (value.charAt(i) != prefix.charAt(i)) { return false; } } return true; } /** * Fast String Buffer class. MIParser does a lot * of deleting off the front of a string, that's clearly * an order N operation for StringBuffer which makes * the MIParser an order N^2 operation. There are "issues" * with this for large arrays. Use of FSB rather than String * Buffer makes MIParser N rather than N^2 because FSB can * delete from the front in constant time. */ public class FSB { StringBuffer buf; int pos; boolean shared; public FSB(StringBuffer buf) { this.buf = buf; pos = 0; shared = false; } public FSB(FSB fbuf) { pos = fbuf.pos; buf = fbuf.buf; shared = true; } public int length() { int res = buf.length() - pos; if (res < 0) return 0; return res; } public char charAt(int index) { return buf.charAt(index + pos); } private void resolveCopy() { if (shared) { buf = new StringBuffer(buf.toString()); shared = false; } } public FSB deleteCharAt(int index) { if (index == 0) { pos++; } else { resolveCopy(); buf = buf.deleteCharAt(pos + index); } return this; } public FSB delete(int start, int end) { if (start == 0) { pos = pos + end - start; } else { resolveCopy(); buf.delete(start + pos, end + pos); } return this; } public void setLength(int a) { if (a == 0) pos = buf.length(); else { // panic! fortunately we don't do this. } } public String substring(int start, int end) { return buf.substring(start + pos, end + pos); } @Override public String toString() { return buf.substring(pos, buf.length()); } int indexOf(char c) { int len = buf.length(); for (int i = pos; i < len; i++) { if (buf.charAt(i) == c) return i - pos; } return -1; } boolean startsWith(String s) { int len = Math.min(s.length(), length()); if (len < s.length()) return false; for (int i = 0; i < len; i++) { if (s.charAt(i) != buf.charAt(pos + i)) return false; } return true; } } }