Merge pull request #6 from GideonLeGrange/parser-fix

Fixed parser problems described in issues #3, #4 and #5
This commit is contained in:
Gideon le Grange 2014-04-14 20:17:57 +02:00
commit a115355d22
7 changed files with 242 additions and 70 deletions

View File

@ -8,9 +8,10 @@ This project provides a Java client to manipulate Mikrotik routers using the rem
Versions Versions
-------- --------
The current stable version 1.1.2, which adds support for handling multi-line results, like for example /file print. The current stable version 1.1.3, which fixes two severe command line parsing bugs, #3 and #4
Version 1.1 added TLS (SSL) support to encrypt API traffic. * 1.1.2 added support for handling multi-line results, like for example /file print.
* 1.1 added TLS (SSL) support to encrypt API traffic.
Examples Examples

View File

@ -7,7 +7,7 @@ package examples;
public class Config { public class Config {
public static final String HOST = "10.0.1.3"; public static final String HOST = "10.0.1.134";
public static final String USERNAME = "admin"; public static final String USERNAME = "admin";
public static final String PASSWORD = ""; public static final String PASSWORD = "";

View File

@ -17,8 +17,13 @@ public class Example6 extends Example {
} }
private void test() throws MikrotikApiException, InterruptedException { private void test() throws MikrotikApiException, InterruptedException {
con.execute("/interface/gre/add remote-address=10.0.1.1 name=gre1 keepalive=10"); System.out.println("Creating interface gre1");
con.execute("/interface/gre/add remote-address=1.2.3.4 name=gre1 keepalive=10 comment='test comment'");
System.out.println("Adding firewall rule for interface gre1");
con.execute("/ip/firewall/filter/add action=drop chain=forward in-interface=gre1 protocol=udp dst-port=78,80");//,80,32");
System.out.println("Waiting 10 seconds");
Thread.sleep(10000); // 10 seconds for the user to look on the router to see the interface with /interface gre print Thread.sleep(10000); // 10 seconds for the user to look on the router to see the interface with /interface gre print
System.out.println("Changing IP for interface gre1");
con.execute("/interface/gre/set remote-address=172.16.1.1 .id=gre1"); con.execute("/interface/gre/set remote-address=172.16.1.1 .id=gre1");
// now look again and the IP has changed // now look again and the IP has changed
} }

View File

@ -486,7 +486,7 @@ public final class ApiConnectionImpl extends ApiConnection {
throw new ApiConnectionException(ex.getMessage(), ex); throw new ApiConnectionException(ex.getMessage(), ex);
} }
if (err != null) { if (err != null) {
throw err; throw new MikrotikApiException(err.getMessage(), err);
} }
return results; return results;
} }

View File

@ -1,11 +1,9 @@
package me.legrange.mikrotik.impl; package me.legrange.mikrotik.impl;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import me.legrange.mikrotik.impl.Scanner.Token;
import java.util.StringTokenizer;
/** /**
* Parse the pseudo-command line into command objects. * Parse the pseudo-command line into command objects.
@ -21,11 +19,8 @@ class Parser {
/** run parse on the internal data and return the command object */ /** run parse on the internal data and return the command object */
private Command parse() throws ParseException { private Command parse() throws ParseException {
next(); command();
expect(Token.COMMAND); while (!is(Token.WHERE, Token.RETURN, Token.EOL)) {
cmd = new Command(text);
next();
while (!((token == Token.WHERE) || (token == Token.RETURN) || (token == Token.EOL))) {
param(); param();
} }
if (token == Token.WHERE) { if (token == Token.WHERE) {
@ -38,14 +33,35 @@ class Parser {
return cmd; return cmd;
} }
private void command() throws ParseException {
StringBuilder path = new StringBuilder();
do {
expect(Token.SLASH);
path.append("/");
next();
expect(Token.TEXT);
path.append(text);
next();
} while (token == Token.SLASH);
cmd = new Command(path.toString());
}
private void param() throws ParseException { private void param() throws ParseException {
String name = text; String name = text;
next(); next();
if (token == Token.EQUALS) { if (token == Token.EQUALS) {
next(); next();
expect(Token.NAME); expect(Token.TEXT);
cmd.addParameter(new Parameter(name, text)); StringBuilder val = new StringBuilder(text);
next(); next();
while (is(Token.COMMA, Token.SLASH)) {
val.append(token);
next();
expect(Token.TEXT);
val.append(text);
next();
}
cmd.addParameter(new Parameter(name, val.toString()));
} }
else { else {
cmd.addParameter(new Parameter(name)); cmd.addParameter(new Parameter(name));
@ -58,12 +74,12 @@ class Parser {
} }
private void expr() throws ParseException { private void expr() throws ParseException {
expect(Token.NOT, Token.NAME); expect(Token.NOT, Token.TEXT);
switch (token) { switch (token) {
case NOT: case NOT:
notExpr(); notExpr();
break; break;
case NAME: { case TEXT: {
String name = text; String name = text;
next(); next();
expect(Token.EQUALS, Token.LESS, Token.MORE); expect(Token.EQUALS, Token.LESS, Token.MORE);
@ -110,19 +126,20 @@ class Parser {
cmd.addQuery("?#!"); cmd.addQuery("?#!");
} }
private void eqExpr(String name) { private void eqExpr(String name) throws ParseException {
next(); // eat = next(); // eat =
expect(Token.TEXT);
cmd.addQuery(String.format("?%s=%s", name, text)); cmd.addQuery(String.format("?%s=%s", name, text));
next(); next();
} }
private void lessExpr(String name) { private void lessExpr(String name) throws ScanException {
next(); // eat < next(); // eat <
cmd.addQuery(String.format("?<%s=%s", name, text)); cmd.addQuery(String.format("?<%s=%s", name, text));
next(); next();
} }
private void moreExpr(String name) { private void moreExpr(String name) throws ScanException {
next(); // eat > next(); // eat >
cmd.addQuery(String.format("?>%s=%s", name, text)); cmd.addQuery(String.format("?>%s=%s", name, text));
next(); next();
@ -134,8 +151,8 @@ class Parser {
private void returns() throws ParseException { private void returns() throws ParseException {
next(); next();
expect(Token.NAME); expect(Token.TEXT);
List<String> props = new LinkedList<String>(); List<String> props = new LinkedList<>();
while (!(token == Token.EOL)) { while (!(token == Token.EOL)) {
if (token != Token.COMMA) { if (token != Token.COMMA) {
props.add(text); props.add(text);
@ -146,63 +163,35 @@ class Parser {
} }
private void expect(Token...tokens) throws ParseException { private void expect(Token...tokens) throws ParseException {
if (!is(tokens))
throw new ParseException(String.format("Expected %s but found %s at position %d", Arrays.asList(tokens), this.token, scanner.pos()));
}
private boolean is(Token...tokens) {
for (Token want : tokens) { for (Token want : tokens) {
if (this.token == want) return; if (this.token == want) return true;
} }
throw new ParseException(String.format("Expected %s but found %s", Arrays.asList(tokens), this.token)); return false;
} }
private void next() { /** move to the next token returned by the scanner */
if (!words.isEmpty()) { private void next() throws ScanException {
text = words.remove(0); token = scanner.next();
if (text.startsWith("/")) { while (token == Token.WS) {
token = Token.COMMAND; token = scanner.next();
} else {
Token t = lookup.get(text);
if (t != null) {
token = t;
text = "";
} else {
token = Token.NAME;
} }
} text = scanner.text();
} else {
token = Token.EOL;
text = "";
}
// System.out.printf("%s: %s\n", token, text);
} }
private Parser(String text) { private Parser(String line) throws ScanException {
text = text.trim(); line = line.trim();
StringTokenizer st = new StringTokenizer(text, " \t,=", true); scanner = new Scanner(line);
while (st.hasMoreElements()) { next();
String t = st.nextToken().trim();
if (!t.equals("")) {
words.add(t);
} }
}
} private final Scanner scanner;
private final List<String> words = new LinkedList<String>();
private String text;
private Token token; private Token token;
private String text;
private Command cmd; private Command cmd;
private static final Map<String, Token> lookup = new HashMap<String, Token>();
private enum Token {
COMMAND, WHERE, RETURN, EOL, NOT, AND, OR, NAME, EQUALS, MORE, LESS, COMMA;
}
static {
lookup.put("where", Token.WHERE);
lookup.put("return", Token.RETURN);
lookup.put("not", Token.NOT);
lookup.put("and", Token.AND);
lookup.put("or", Token.OR);
lookup.put("=", Token.EQUALS);
lookup.put(">", Token.MORE);
lookup.put("<", Token.LESS);
lookup.put(",", Token.COMMA);
}
} }

View File

@ -0,0 +1,21 @@
package me.legrange.mikrotik.impl;
import me.legrange.mikrotik.MikrotikApiException;
/**
* Exception thrown if the scanner encounters an error while scanning a command line.
* @author GideonLeGrange
*/
public class ScanException extends ParseException {
ScanException(String msg) {
super(msg);
}
ScanException(String msg, Throwable err) {
super(msg, err);
}
}

View File

@ -0,0 +1,156 @@
/*
* Copyright 2014 GideonLeGrange.
*
* 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 me.legrange.mikrotik.impl;
/**
* A simple scanner.
* @author gideon
*/
class Scanner {
enum Token {
SLASH("/"), COMMA(","), EOL(), WS, TEXT,
LESS("<"), MORE(">"), EQUALS("="),
WHERE, NOT, AND, OR, RETURN;
@Override
public String toString() {
return (symb == null) ? name() : symb;
}
private Token(String symb) {
this.symb = symb;
}
private Token() {
symb = null;
}
private final String symb;
}
/** create a scanner for the given line of text */
Scanner(String line) {
this.line = line;
nextChar();
}
/** return the next token from the text */
Token next() throws ScanException {
text = null;
switch (c) {
case '\n' : return Token.EOL;
case ' ' :
case '\t' :
return whiteSpace();
case ',' :
nextChar();
return Token.COMMA;
case '/' :
nextChar();
return Token.SLASH;
case '<' :
nextChar();
return Token.LESS;
case '>' :
nextChar();
return Token.MORE;
case '=' :
nextChar();
return Token.EQUALS;
case '"' :
return quotedText('"');
case '\'' :
return quotedText('\'');
default :
return name();
}
}
/** return the text associated with the last token returned */
String text() {
if (text != null) return text.toString();
return "";
}
/** return the position of the scanner */
int pos() { return pos; }
/** process 'name' tokens which could be key words or text */
private Token name() throws ScanException {
text = new StringBuilder();
while (in(c,"[A-Za-z0-9-\\.]")) {
text.append(c);
nextChar();
}
String val = text.toString().toLowerCase();
switch (val) {
case "where" : return Token.WHERE;
case "not" : return Token.NOT;
case "and" : return Token.AND;
case "or" : return Token.OR;
case "return" : return Token.RETURN;
}
return Token.TEXT;
}
/** process quoted text */
private Token quotedText(char quote) throws ScanException {
nextChar(); // eat the '"'
text = new StringBuilder();
while (c != quote) {
if (c == '\n') {
throw new ScanException("Unclosed quoted text, reached end of line.");
}
text.append(c);
nextChar();
}
nextChar(); // eat the '"'
return Token.TEXT;
}
/** process white space */
private Token whiteSpace() {
while ((c == ' ') || (c == '\t')) {
nextChar();
}
return Token.WS;
}
/** return the next character from the line of text */
private void nextChar() {
if (pos < line.length()) {
c = line.charAt(pos);
pos ++;
}
else {
c = '\n';
}
}
/** check if the character matches the give expression */
private boolean in(char c, String cs) {
return ("" + c).matches(cs);
}
private final String line;
private int pos = 0;
private char c;
private StringBuilder text;
}