diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b8bff6a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+
+# Java
+*.class
+*.jar
+*.war
+*.ear
+
+# Scala
+*.class
+*.log
+# sbt specific
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+# Scala-IDE specific
+.scala_dependencies
+
+# Maven
+target/
+
+# Netbeans
+build/
+dist/
+#nbproject/
+
+# IntelliJ
+.idea/
+
+# Eclipse
+.settings/
+
+# Project
+Mtik.java
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..600b158
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,25 @@
+
+ 4.0.0
+
+ adept
+ mikrotik
+ 1.0-SNAPSHOT
+ jar
+
+ mikrotik
+ http://maven.apache.org
+
+
+ UTF-8
+
+
+
+
+ junit
+ junit
+ 3.8.1
+ test
+
+
+
diff --git a/src/main/java/me/legrange/mikrotik/ApiCommandException.java b/src/main/java/me/legrange/mikrotik/ApiCommandException.java
new file mode 100644
index 0000000..1c4a63d
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ApiCommandException.java
@@ -0,0 +1,31 @@
+package me.legrange.mikrotik;
+
+/**
+ * Thrown when the Mikrotik returns an error when receiving our command.
+ * @author GideonLeGrange
+ */
+public class ApiCommandException extends MikrotikApiException {
+
+
+ /** return the tag associated with this exception, if there is one */
+ public String getTag() {
+ return tag;
+ }
+
+ ApiCommandException(String msg) {
+ super(msg);
+ }
+
+ ApiCommandException(String msg, Throwable err) {
+ super(msg, err);
+ }
+
+
+ ApiCommandException(Error err) {
+ super(err.getMessage());
+ tag = err.getTag();
+ }
+
+ private String tag = null;
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/ApiConnection.java b/src/main/java/me/legrange/mikrotik/ApiConnection.java
new file mode 100644
index 0000000..08b619a
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ApiConnection.java
@@ -0,0 +1,430 @@
+package me.legrange.mikrotik;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * The Mikrotik API connection. This is the class used to connect to a remote
+ * Mikrotik and send commands to it.
+ *
+ * @author GideonLeGrange
+ */
+public class ApiConnection {
+
+ /**
+ * Create a new API connection to the give device on the supplied port
+ */
+ public static ApiConnection connect(String host, int port) throws ApiConnectionException {
+ ApiConnection con = new ApiConnection();
+ con.open(host, port);
+ return con;
+ }
+
+ /**
+ * Create a new API connection to the give device on the default API port
+ */
+ public static ApiConnection connect(String host) throws ApiConnectionException {
+ return connect(host, DEFAULT_PORT);
+ }
+
+ /**
+ * State of connection
+ *
+ * @return - if connection is established to router it returns true.
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Disconnect from the remote API
+ */
+ public void disconnect() throws ApiConnectionException {
+ if (!connected) {
+ throw new ApiConnectionException(("Not/no longer connected to remote Mikrotik"));
+ }
+ connected = false;
+ reader.interrupt();
+ try {
+ sock.close();
+ } catch (IOException ex) {
+ throw new ApiConnectionException(String.format("Error closing socket: %s", ex.getMessage()), ex);
+ }
+ }
+
+ /** cancel a command */
+ public void cancel(Command can) throws MikrotikApiException {
+ Command cmd = new Command("/cancel");
+ cmd.addParameter("tag", can.getTag());
+ execute(cmd);
+ }
+
+ /**
+ * set up method that will log you in
+ *
+ * @param name - username of the user on the router
+ * @param password - password for the user
+ * @return
+ */
+ public void login(String name, String pwd) throws MikrotikApiException, ApiCommandException, InterruptedException {
+ List list = execute("/login");
+ Result res = list.get(0);
+ String hash = res.get("ret");
+ String chal = Util.hexStrToStr("00") + new String(makePass(pwd)) + Util.hexStrToStr(hash);
+ chal = Util.hashMD5(chal);
+ execute("/login name=" + name + " response=00" + chal);
+ }
+
+ private List execute(Command cmd) throws MikrotikApiException {
+ SyncListener l = new SyncListener();
+ execute(cmd, l);
+ return l.getResults();
+ }
+
+ public List execute(String cmd) throws MikrotikApiException {
+ return execute(Parser.parse(cmd));
+ }
+
+ public Command execute(String cmd, ResultListener lis) throws MikrotikApiException {
+ return execute(Parser.parse(cmd), lis);
+ }
+
+ private Command execute(Command cmd, ResultListener lis) throws MikrotikApiException {
+ String tag = nextTag();
+ cmd.setTag(tag);
+ listeners.put(tag, lis);
+ try {
+ Util.write(cmd, out);
+ } catch (UnsupportedEncodingException ex) {
+ throw new ApiDataException(ex.getMessage(), ex);
+ } catch (IOException ex) {
+ throw new ApiConnectionException(ex.getMessage(), ex);
+ }
+ return cmd;
+ }
+
+ private ApiConnection() {
+ }
+
+ /**
+ * Start the API. Connects to the Mikrotik
+ */
+ private void open(String host, int port) throws ApiConnectionException {
+ try {
+ InetAddress ia = InetAddress.getByName(host);
+ if (ia.isReachable(1000)) {
+ sock = new Socket(ia, port);
+ in = new DataInputStream(sock.getInputStream());
+ out = new DataOutputStream(sock.getOutputStream());
+ connected = true;
+ reader = new Reader();
+ reader.setDaemon(true);
+ reader.start();
+ processor = new Processor();
+ processor.setDaemon(true);
+ processor.start();
+ } else {
+ throw new ApiConnectionException(String.format("Host '%s' port %d is uncreachable", host, port));
+ }
+ } catch (UnknownHostException ex) {
+ connected = false;
+ throw new ApiConnectionException(String.format("Unknown host '%s'", host), ex);
+ } catch (IOException ex) {
+ connected = false;
+ throw new ApiConnectionException(String.format("Error connecting to '%s': %s", host, ex.getMessage()), ex);
+ }
+ }
+
+ private char[] makePass(String pass) {
+ if (true) {
+ return pass.toCharArray();
+ }
+ char[] res = new char[pass.length() + 1];
+ System.arraycopy(pass.toCharArray(), 0, res, 0, pass.length());
+ res[pass.length()] = 0x0;
+ return res;
+ }
+
+ private synchronized String nextTag() {
+ _tag++;
+ return Integer.toHexString(_tag);
+ }
+
+ private static final int DEFAULT_PORT = 8728;
+ private Socket sock = null;
+ private DataOutputStream out = null;
+ private DataInputStream in = null;
+ private boolean connected = false;
+ private Reader reader;
+ private Processor processor;
+ private final Map listeners = new HashMap();
+ private Integer _tag = 0;
+
+ /**
+ * thread to read data from the socket and process it into Strings
+ */
+ private class Reader extends Thread {
+
+ private String take() throws ApiConnectionException, ApiDataException {
+ Object val = null;
+ try {
+ val = queue.take();
+ } catch (InterruptedException ex) {
+ throw new ApiConnectionException("Interrupted while reading data from queue.", ex);
+ }
+ if (val instanceof ApiConnectionException) {
+ throw (ApiConnectionException) val;
+ } else if (val instanceof ApiDataException) {
+ throw (ApiDataException) val;
+ }
+ return (String) val;
+ }
+
+ private boolean isEmpty() {
+ return queue.isEmpty();
+ }
+
+ @Override
+ public void run() {
+ while (connected) {
+ try {
+ String s = Util.decode(in);
+ if (s != null) {
+ queue.put(s);
+ }
+ } catch (ApiDataException ex) {
+ try {
+ queue.put(ex);
+ } catch (InterruptedException ex2) {
+ }
+ } catch (ApiConnectionException ex) {
+ } catch (InterruptedException ex1) {
+ }
+ }
+ }
+ private LinkedBlockingQueue queue = new LinkedBlockingQueue(40);
+ }
+
+ /**
+ * Thread to take the received strings and process it into Result objects
+ */
+ private class Processor extends Thread {
+
+ @Override
+ public void run() {
+ while (connected) {
+ Response res;
+ try {
+ res = unpack();
+ } catch (ApiCommandException ex) {
+ String tag = ex.getTag();
+ if (tag != null) {
+ res = new Error(tag, ex.getMessage());
+ } else {
+ continue;
+ }
+ } catch (MikrotikApiException ex) {
+ ex.printStackTrace();
+ continue;
+ }
+ ResultListener l = listeners.get(res.getTag());
+ if (l != null) {
+ if (res instanceof Result) {
+ l.receive((Result) res);
+ } else {
+ if (res instanceof Done) {
+ listeners.remove(res.getTag());
+ }
+ if (l instanceof ResponseListener) {
+ ResponseListener rl = (ResponseListener) l;
+ if (res instanceof Done) {
+ rl.completed((Done) res);
+ } else if (res instanceof Error) {
+ rl.error((Error) res);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void nextLine() throws ApiConnectionException, ApiDataException {
+ if (lines.isEmpty()) {
+ String block = reader.take();
+ String parts[] = block.split("\n");
+ lines.addAll(Arrays.asList(parts));
+ }
+ line = lines.remove(0);
+ }
+
+ private boolean hasNextLine() {
+ return !lines.isEmpty() || !reader.isEmpty();
+ }
+
+ private Response unpack() throws MikrotikApiException {
+ if (line == null) {
+ nextLine();
+ }
+ if (line.equals("!re")) {
+ return unpackRe();
+ } else if (line.equals("!done")) {
+ return unpackDone();
+ } else if (line.equals("!trap")) {
+ return unpackError();
+ } else if (line.equals("!halt")) {
+ return unpackError();
+ } else {
+ throw new ApiDataException(String.format("Unexpected line '%s'", line));
+ }
+ }
+
+ private Result unpackRe() throws ApiDataException, ApiConnectionException {
+ nextLine();
+ int l = 0;
+ Result res = new Result();
+ while (!line.startsWith(("!"))) {
+ l++;
+ if (line.startsWith(("="))) {
+ String parts[] = line.split("=", 3);
+ if (parts.length == 3) {
+ res.put(parts[1], parts[2]);
+ } else {
+ throw new ApiDataException(String.format("Malformed line '%s'", line));
+ }
+ } else if (line.startsWith(".tag=")) {
+ String parts[] = line.split("=", 2);
+ if (parts.length == 2) {
+ res.setTag(parts[1]);
+ }
+ } else {
+ throw new ApiDataException(String.format("Unexpected line '%s'", line));
+ }
+ if (hasNextLine()) {
+ nextLine();
+ } else {
+ line = null;
+ break;
+ }
+ }
+ return res;
+ }
+
+ private Done unpackDone() throws MikrotikApiException {
+ Done done = new Done(null);
+ if (hasNextLine()) {
+ nextLine();
+
+ while (!line.startsWith("!")) {
+ if (line.startsWith(".tag=")) {
+ String parts[] = line.split("=", 2);
+ if (parts.length == 2) {
+ done.setTag(parts[1]);
+ }
+ } else if (line.startsWith(("=ret"))) {
+ String parts[] = line.split("=", 3);
+ if (parts.length == 3) {
+ done.setHash(parts[2]);
+ } else {
+ throw new ApiDataException(String.format("Malformed line '%s'", line));
+ }
+ }
+ if (hasNextLine()) {
+ nextLine();
+ } else {
+ line = null;
+ break;
+ }
+ }
+ }
+ return done;
+ }
+
+ private Error unpackError() throws MikrotikApiException {
+ nextLine();
+ Error err = new Error();
+ if (hasNextLine()) {
+ while (!line.startsWith("!")) {
+ if (line.startsWith(".tag=")) {
+ String parts[] = line.split("=", 2);
+ if (parts.length == 2) {
+ err.setTag(parts[1]);
+ }
+ } else if (line.startsWith("=message=")) {
+ err.setMessage(line.split("=", 3)[2]);
+ }
+ if (hasNextLine()) {
+ nextLine();
+ } else {
+ line = null;
+ break;
+ }
+ }
+ }
+ return err;
+ }
+
+ private void queue(Response res) {
+ String tag = res.getTag();
+ if (tag != null) {
+ ResultListener rl = listeners.get(tag);
+ if (rl != null) {
+ if (res instanceof Result) {
+ rl.receive((Result) res);
+ } else {
+ // rl.error((Error)res);
+ }
+ }
+ }
+ }
+ private List lines = new LinkedList();
+ private String line;
+ }
+
+ private class SyncListener implements ResponseListener {
+
+ public synchronized void error(Error err) {
+ this.err = err;
+ notify();
+ }
+
+ public synchronized void completed(Done done) {
+ if (done.getHash() != null) {
+ Result res = new Result();
+ res.put("ret", done.getHash());
+ results.add(res);
+ }
+ notify();
+ }
+
+ public void receive(Result result) {
+ results.add(result);
+ }
+
+ private List getResults() throws ApiCommandException, ApiConnectionException {
+ try {
+ synchronized (this) {
+ wait();
+ }
+ } catch (InterruptedException ex) {
+ throw new ApiConnectionException(ex.getMessage(), ex);
+ }
+ if (err != null) {
+ throw new ApiCommandException(err);
+ }
+ return results;
+ }
+ private List results = new LinkedList();
+ private Error err;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/legrange/mikrotik/ApiConnectionException.java b/src/main/java/me/legrange/mikrotik/ApiConnectionException.java
new file mode 100644
index 0000000..10f4e35
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ApiConnectionException.java
@@ -0,0 +1,19 @@
+package me.legrange.mikrotik;
+
+/**
+ * Exception thrown if the Api experiences a connection problem
+ * @author GideonLeGrange
+ */
+public class ApiConnectionException extends MikrotikApiException {
+
+ ApiConnectionException(String msg) {
+ super(msg);
+ }
+
+ ApiConnectionException(String msg, Throwable err) {
+ super(msg, err);
+ }
+
+
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/ApiDataException.java b/src/main/java/me/legrange/mikrotik/ApiDataException.java
new file mode 100644
index 0000000..e2b9de7
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ApiDataException.java
@@ -0,0 +1,19 @@
+package me.legrange.mikrotik;
+
+/**
+ * Thrown if there is a problem unpacking data from the Api.
+ * @author GideonLeGrange
+ */
+public class ApiDataException extends MikrotikApiException {
+
+ ApiDataException(String msg) {
+ super(msg);
+ }
+
+ ApiDataException(String msg, Throwable err) {
+ super(msg, err);
+ }
+
+
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/ApiLoginException.java b/src/main/java/me/legrange/mikrotik/ApiLoginException.java
new file mode 100644
index 0000000..bc21ac2
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ApiLoginException.java
@@ -0,0 +1,18 @@
+package me.legrange.mikrotik;
+
+/**
+ * Thrown if the API cannot log in
+ * @author GideonLeGrange
+ */
+public class ApiLoginException extends MikrotikApiException {
+
+ ApiLoginException(String msg) {
+ super(msg);
+ }
+
+ ApiLoginException(String msg, Throwable err) {
+ super(msg, err);
+ }
+
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/Command.java b/src/main/java/me/legrange/mikrotik/Command.java
new file mode 100644
index 0000000..44fc173
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Command.java
@@ -0,0 +1,81 @@
+package me.legrange.mikrotik;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A command sent to a Mikrotik. This internal class is used to build complex commands
+ * with parameters, queries and property lists.
+ *
+ * @author GideonLeGrange
+ */
+public class Command {
+
+ @Override
+ public String toString() {
+ return String.format("cmd[%s] = %s, params = %s, queries = %s, props=%s ", tag, cmd, params, queries, properties);
+ }
+
+ Command(String cmd) {
+ if (!cmd.startsWith("/")) {
+ cmd = "/" + cmd;
+ }
+ this.cmd = cmd;
+ }
+
+ String getCommand() {
+ return cmd;
+ }
+
+ /**
+ * Add a parameter to a command.
+ */
+ void addParameter(String name, String value) {
+ params.add(new Parameter(name, value));
+ }
+
+ /**
+ * Add a valueless parameter to the command
+ */
+ void addParameter(Parameter param) {
+ params.add(param);
+ }
+
+ /**
+ * Add a property to include in a result
+ */
+ void addProperty(String... names) {
+ properties.addAll(Arrays.asList(names));
+ }
+
+ void addQuery(String... queries) {
+ this.queries.addAll(Arrays.asList(queries));
+ }
+
+ void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ List getQueries() {
+ return queries;
+
+ }
+
+ String getTag() {
+ return tag;
+ }
+
+ List getProperties() {
+ return properties;
+ }
+
+ List getParameters() {
+ return params;
+ }
+ private String cmd;
+ private List params = new LinkedList();
+ private List queries = new LinkedList();
+ private List properties = new LinkedList();
+ private String tag;
+}
diff --git a/src/main/java/me/legrange/mikrotik/Done.java b/src/main/java/me/legrange/mikrotik/Done.java
new file mode 100644
index 0000000..e487aa7
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Done.java
@@ -0,0 +1,23 @@
+package me.legrange.mikrotik;
+
+/**
+ * Internal representation of !done
+ * @author GideonLeGrange
+ */
+public class Done extends Response {
+
+ Done(String tag) {
+ super(tag);
+ }
+
+ void setHash(String hash) {
+ this.hash = hash;
+ }
+
+ String getHash() {
+ return hash;
+ }
+
+ private String hash;
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/Error.java b/src/main/java/me/legrange/mikrotik/Error.java
new file mode 100644
index 0000000..6fb384f
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Error.java
@@ -0,0 +1,28 @@
+package me.legrange.mikrotik;
+
+/**
+ * Used to encapsulate API error information. We need to pass both the message and the tag (if one was used).
+ * @author GideonLeGrange
+ */
+public class Error extends Response {
+
+ Error(String tag, String message) {
+ super(tag);
+ this.message = message;
+ }
+
+ Error() {
+ super(null);
+ }
+
+ String getMessage() {
+ return message;
+ }
+ void setMessage(String message) {
+ this.message = message;
+ }
+
+ private String message;
+
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/MikrotikApiException.java b/src/main/java/me/legrange/mikrotik/MikrotikApiException.java
new file mode 100644
index 0000000..cd39367
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/MikrotikApiException.java
@@ -0,0 +1,17 @@
+package me.legrange.mikrotik;
+
+/**
+ * Thrown by the Mikrotik API to indicate errors
+ *
+ * @author GideonLeGrange
+ */
+public class MikrotikApiException extends Exception {
+
+ MikrotikApiException(String msg) {
+ super(msg);
+ }
+
+ MikrotikApiException(String msg, Throwable err) {
+ super(msg, err);
+ }
+}
diff --git a/src/main/java/me/legrange/mikrotik/Parameter.java b/src/main/java/me/legrange/mikrotik/Parameter.java
new file mode 100644
index 0000000..0937e93
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Parameter.java
@@ -0,0 +1,41 @@
+package me.legrange.mikrotik;
+
+/**
+ * A command parameter
+ *
+ * @author GideonLeGrange
+ */
+class Parameter {
+
+ @Override
+ public String toString() {
+ if (hasValue()) {
+ return String.format("%s=%s", name, value);
+ } else {
+ return name;
+ }
+ }
+
+ Parameter(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ Parameter(String name) {
+ this(name, null);
+ }
+
+ boolean hasValue() {
+ return value != null;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ String getValue() {
+ return value;
+ }
+ private String name;
+ private String value;
+}
diff --git a/src/main/java/me/legrange/mikrotik/ParseException.java b/src/main/java/me/legrange/mikrotik/ParseException.java
new file mode 100644
index 0000000..5cb8185
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ParseException.java
@@ -0,0 +1,19 @@
+package me.legrange.mikrotik;
+
+/**
+ * Exception thrown if the parser encounters an error while parsing a command line.
+ * @author GideonLeGrange
+ */
+public class ParseException extends MikrotikApiException {
+
+ ParseException(String msg) {
+ super(msg);
+ }
+
+ ParseException(String msg, Throwable err) {
+ super(msg, err);
+ }
+
+
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/Parser.java b/src/main/java/me/legrange/mikrotik/Parser.java
new file mode 100644
index 0000000..fc95692
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Parser.java
@@ -0,0 +1,208 @@
+package me.legrange.mikrotik;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * Parse the pseudo-command line into command objects.
+ * @author GideonLeGrange
+ */
+class Parser {
+
+ /** parse the given bit of text into a Command object */
+ static Command parse(String text) throws ParseException {
+ Parser parser = new Parser(text);
+ return parser.parse();
+ }
+
+ /** run parse on the internal data and return the command object */
+ private Command parse() throws ParseException {
+ next();
+ expect(Token.COMMAND);
+ cmd = new Command(text);
+ next();
+ while (!((token == Token.WHERE) || (token == Token.RETURN) || (token == Token.EOL))) {
+ param();
+ }
+ if (token == Token.WHERE) {
+ where();
+ }
+ if (token == Token.RETURN) {
+ returns();
+ }
+ expect(Token.EOL);
+ return cmd;
+ }
+
+ private void param() throws ParseException {
+ String name = text;
+ next();
+ if (token == Token.EQUALS) {
+ next();
+ expect(Token.NAME);
+ cmd.addParameter(new Parameter(name, text));
+ next();
+ }
+ else {
+ cmd.addParameter(new Parameter(name));
+ }
+ }
+
+ private void where() throws ParseException {
+ next(); // swallow the word "where"
+ expr();
+ }
+
+ private void expr() throws ParseException {
+ expect(Token.NOT, Token.NAME);
+ switch (token) {
+ case NOT:
+ notExpr();
+ break;
+ case NAME: {
+ String name = text;
+ next();
+ expect(Token.EQUALS, Token.LESS, Token.MORE);
+ switch (token) {
+ case EQUALS:
+ eqExpr(name);
+ break;
+ case LESS:
+ lessExpr(name);
+ break;
+ case MORE:
+ moreExpr(name);
+ break;
+ default:
+ hasExpr(name);
+ }
+ }
+ break;
+ }
+ // if you get here, you had a expression, see if you want more.
+ switch (token) {
+ case AND : andExpr();
+ break;
+ case OR : orExpr();
+ }
+ }
+
+ private void andExpr() throws ParseException {
+ next(); // eat and
+ expr();
+ cmd.addQuery("?#&");
+ }
+
+ private void orExpr() throws ParseException {
+ next(); // eat or
+ expr();
+ cmd.addQuery("?#|");
+ }
+
+
+ private void notExpr() throws ParseException {
+ next(); // eat not
+ expr();
+ cmd.addQuery("?#!");
+ }
+
+ private void eqExpr(String name) {
+ next(); // eat =
+ cmd.addQuery(String.format("?%s=%s", name, text));
+ next();
+ }
+
+ private void lessExpr(String name) {
+ next(); // eat <
+ cmd.addQuery(String.format("?<%s=%s", name, text));
+ next();
+ }
+
+ private void moreExpr(String name) {
+ next(); // eat >
+ cmd.addQuery(String.format("?>%s=%s", name, text));
+ next();
+ }
+
+ private void hasExpr(String name) {
+ cmd.addQuery(String.format("?%s", name));
+ }
+
+ private void returns() throws ParseException {
+ next();
+ expect(Token.NAME);
+ List props = new LinkedList();
+ while (!(token == Token.EOL)) {
+ if (token != Token.COMMA) {
+ props.add(text);
+ }
+ next();
+ }
+ cmd.addProperty(props.toArray(new String[]{}));
+ }
+
+ private void expect(Token...tokens) throws ParseException {
+ for (Token want : tokens) {
+ if (this.token == want) return;
+ }
+ throw new ParseException(String.format("Expected %s but found %s", Arrays.asList(tokens), this.token));
+ }
+
+ private void next() {
+ if (!words.isEmpty()) {
+ text = words.remove(0);
+ if (text.startsWith("/")) {
+ token = Token.COMMAND;
+ } else {
+ Token t = lookup.get(text);
+ if (t != null) {
+ token = t;
+ text = "";
+ } else {
+ token = Token.NAME;
+ }
+ }
+ } else {
+ token = Token.EOL;
+ text = "";
+ }
+ // System.out.printf("%s: %s\n", token, text);
+ }
+
+ private Parser(String text) {
+ text = text.trim();
+ StringTokenizer st = new StringTokenizer(text, " \t,=", true);
+ while (st.hasMoreElements()) {
+ String t = st.nextToken().trim();
+ if (!t.equals("")) {
+ words.add(t);
+ }
+ }
+ }
+ private final List words = new LinkedList();
+ private String text;
+ private Token token;
+ private Command cmd;
+ private static final Map lookup = new HashMap();
+
+ 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);
+ }
+}
diff --git a/src/main/java/me/legrange/mikrotik/Response.java b/src/main/java/me/legrange/mikrotik/Response.java
new file mode 100644
index 0000000..5e05a02
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Response.java
@@ -0,0 +1,28 @@
+package me.legrange.mikrotik;
+
+/**
+ * Super type of possible API responses
+ *
+ * @author GideonLeGrange
+ */
+abstract class Response {
+
+ public String getTag() {
+ return tag;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s: tag=%s", getClass().getSimpleName(), tag);
+ }
+
+ void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ protected Response(String tag) {
+ this.tag = tag;
+ }
+
+ private String tag;
+}
diff --git a/src/main/java/me/legrange/mikrotik/ResponseListener.java b/src/main/java/me/legrange/mikrotik/ResponseListener.java
new file mode 100644
index 0000000..2c2c8bd
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ResponseListener.java
@@ -0,0 +1,17 @@
+package me.legrange.mikrotik;
+
+/**
+ * A listener that receives life cycle command events from the Mikrotik API,
+ * and not just results.
+ * @author GideonLeGrange
+ */
+public interface ResponseListener extends ResultListener {
+
+ /** called if the command associated with this listener experiences a trap */
+ void error(Error err);
+
+ /** called when the command associated with this listener is done */
+ void completed(Done done);
+
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/Result.java b/src/main/java/me/legrange/mikrotik/Result.java
new file mode 100644
index 0000000..1d42f17
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Result.java
@@ -0,0 +1,78 @@
+package me.legrange.mikrotik;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A result from an API command.
+ * @author GideonLeGrange
+ */
+public class Result extends Response implements Map {
+
+ public Result() {
+ super(null);
+ this.map = new HashMap();
+ }
+
+ public String get(String key) {
+ return map.get(key);
+ }
+
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tag=%s, data=%s", getTag(), map);
+ }
+
+ public int size() {
+ return map.size();
+ }
+
+ public boolean containsKey(Object o) {
+ return map.containsKey(o);
+ }
+
+ public boolean containsValue(Object o) {
+ return map.containsValue(o);
+ }
+
+ public String get(Object o) {
+ return map.get(o);
+ }
+
+ public String put(String k, String v) {
+ return map.put(k, v);
+ }
+
+ public String remove(Object o) {
+ return map.remove(o);
+ }
+
+ public void putAll(Map extends String, ? extends String> map) {
+ this.map.putAll(map);
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ public Set keySet() {
+ return map.keySet();
+ }
+
+ public Collection values() {
+ return map.values();
+ }
+
+ public Set> entrySet() {
+ return map.entrySet();
+ }
+
+ private final Map map;
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/ResultListener.java b/src/main/java/me/legrange/mikrotik/ResultListener.java
new file mode 100644
index 0000000..ab0b25a
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/ResultListener.java
@@ -0,0 +1,11 @@
+package me.legrange.mikrotik;
+
+/**
+ * Implement this interface to receive command results from the Mikrotik Api.
+ * @author GideonLeGrange
+ */
+public interface ResultListener {
+
+ void receive(Result result);
+
+}
diff --git a/src/main/java/me/legrange/mikrotik/Util.java b/src/main/java/me/legrange/mikrotik/Util.java
new file mode 100644
index 0000000..40392fe
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/Util.java
@@ -0,0 +1,177 @@
+package me.legrange.mikrotik;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+/**
+ * Utility library that handles the low level encoding required by the Mikrotik API.
+ * @author GideonLeGrange. Possibly some code by janisk left.
+ */
+class Util {
+
+ static void write(Command cmd, OutputStream out) throws UnsupportedEncodingException, IOException {
+ System.out.println("cmd = '" + cmd + "'");
+ encode(cmd.getCommand(), out);
+ for (Parameter param : cmd.getParameters()) {
+ encode(String.format("=%s=%s", param.getName(), param.hasValue() ? param.getValue() : "" ), out);
+ }
+ String tag = cmd.getTag();
+ if ((tag != null) && !tag.equals("")) {
+ encode(String.format(".tag=%s", tag), out);
+ }
+ List props = cmd.getProperties();
+ if (!props.isEmpty()) {
+ StringBuilder buf = new StringBuilder("=.proplist=");
+ for (int i = 0; i < props.size(); ++i) {
+ if (i > 0)
+ buf.append(",");
+ buf.append(props.get(i));
+ }
+ encode(buf.toString(), out);
+ }
+ for (String query : cmd.getQueries()) {
+ encode(query, out);
+ }
+ out.write(0);
+ }
+
+ /** encode text using Mikrotik's encoding scheme and write it to an output stream. */
+ private static void encode(String word, OutputStream out) throws UnsupportedEncodingException, IOException {
+ byte bytes[] = word.getBytes("US-ASCII");
+ int len = bytes.length;
+ if (len < 0x80) {
+ out.write(len);
+ } else if (len < 0x4000) {
+ len = len | 0x8000;
+ out.write(len >> 8);
+ out.write(len);
+ } else if (len < 0x20000) {
+ len = len | 0xC00000;
+ out.write(len >> 16);
+ out.write(len >> 8);
+ out.write(len);
+ } else if (len < 0x10000000) {
+ len = len | 0xE0000000;
+ out.write(len >> 24);
+ out.write(len >> 16);
+ out.write(len >> 8);
+ out.write(len);
+ } else {
+ out.write(0xF0);
+ out.write(len >> 24);
+ out.write(len >> 16);
+ out.write(len >> 8);
+ out.write(len);
+ }
+ out.write(bytes);
+ }
+
+ /** decode bytes from an input stream of Mikrotik protocol sentences into text */
+ static String decode(InputStream in) throws ApiDataException, ApiConnectionException {
+ StringBuilder res = new StringBuilder();
+ decode(in, res);
+ return res.toString();
+ }
+
+ /** decode bytes from an input stream into Mikrotik protocol sentences */
+ private static void decode(InputStream in, StringBuilder result) throws ApiDataException, ApiConnectionException {
+ try {
+ int len = readLen(in);
+ if (len > 0) {
+ byte buf[] = new byte[len];
+ for (int i = 0; i < len; ++i) {
+ int c = in.read();
+ if (c < 0) {
+ throw new ApiDataException("Truncated data. Expected to read more bytes");
+ }
+ buf[i] = (byte) (c & 0xFF);
+ }
+ String res = new String(buf);
+ if (result.length() > 0)
+ result.append("\n");
+ result.append(res);
+ decode(in, result);
+ }
+ } catch (IOException ex) {
+ throw new ApiConnectionException(ex.getMessage(), ex);
+ }
+ }
+
+ /** read length bytes from stream and return length of coming word */
+ static private int readLen(InputStream in) throws IOException {
+ int c = in.read();
+ if (c > 0) {
+ if ((c & 0x80) == 0) {
+ } else if ((c & 0xC0) == 0x80) {
+ c = c & ~0xC0;
+ c = (c << 8) | in.read();
+ } else if ((c & 0xE0) == 0xC0) {
+ c = c & ~0xE0;
+ c = (c << 8) | in.read();
+ c = (c << 8) | in.read();
+ } else if ((c & 0xF0) == 0xE0) {
+ c = c & ~0xF0;
+ c = (c << 8) | in.read();
+ c = (c << 8) | in.read();
+ c = (c << 8) | in.read();
+ } else if ((c & 0xF8) == 0xF0) {
+ c = in.read();
+ c = (c << 8) | in.read();
+ c = (c << 8) | in.read();
+ c = (c << 8) | in.read();
+ c = (c << 8) | in.read();
+ }
+ }
+ return c;
+ }
+
+ /**
+ * makes MD5 hash of string for use with RouterOS API
+ *
+ * @param s - variable to make hash from
+ * @return - the md5 hash
+ */
+ static String hashMD5(String s) throws ApiDataException {
+ MessageDigest algorithm = null;
+ try {
+ algorithm = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException nsae) {
+ throw new ApiDataException("Cannot find MD5 digest algorithm");
+ }
+ byte[] defaultBytes = new byte[s.length()];
+ for (int i = 0; i < s.length(); i++) {
+ defaultBytes[i] = (byte) (0xFF & s.charAt(i));
+ }
+ algorithm.reset();
+ algorithm.update(defaultBytes);
+ byte messageDigest[] = algorithm.digest();
+ StringBuilder hexString = new StringBuilder();
+ for (int i = 0; i < messageDigest.length; i++) {
+ String hex = Integer.toHexString(0xFF & messageDigest[i]);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ }
+
+ /**
+ * converts hex value string to normal strint for use with RouterOS API
+ *
+ * @param s - hex string to convert to
+ * @return - converted string.
+ */
+ static String hexStrToStr(String s) {
+ String ret = "";
+ for (int i = 0; i < s.length(); i += 2) {
+ ret += (char) Integer.parseInt(s.substring(i, i + 2), 16);
+ }
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/legrange/mikrotik/bnf.txt b/src/main/java/me/legrange/mikrotik/bnf.txt
new file mode 100644
index 0000000..9868188
--- /dev/null
+++ b/src/main/java/me/legrange/mikrotik/bnf.txt
@@ -0,0 +1,12 @@
+command = action [ query ] [ return ]
+action = ("/" word)+
+query = "where" expr
+expr = expr "and" expr | expr "or" expr | "not" expr | hasExpr | eqExpr | lessExpr | moreExpr
+hasExpr = name
+eqExpr = name "=" value
+lessExpr = name "<" value
+moreExpr = name ">" value
+return = "return" (name)+
+
+
+