evildecay 4 lat temu
commit
39a16f0ab3

+ 19 - 0
.gitignore

@@ -0,0 +1,19 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.iml
+
+# Folders
+_obj
+_test
+.idea
+out
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.exe
+*.class
+
+# Package Files
+*.war
+*.ear

BIN
lib/gson-2.8.5.jar


+ 107 - 0
src/com/wecise/odb/Connector.java

@@ -0,0 +1,107 @@
+package com.wecise.odb;
+
+import com.google.gson.Gson;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.HashMap;
+
+public class Connector implements Runnable {
+    private Socket socket;
+    private boolean alive;
+    private boolean closed;
+
+    public Connector(Socket socket) {
+        this.socket = socket;
+        if (socket != null && !socket.isClosed()) {
+            this.alive = true;
+            this.closed = false;
+        }
+    }
+
+//    public Socket getSocket() {
+//        return this.socket;
+//    }
+
+    public boolean isAlive() {
+        return this.alive;
+    }
+
+    public void close() {
+        try {
+            if (!this.socket.isClosed()) {
+                if (!this.socket.isInputShutdown()) {
+                    this.socket.shutdownInput();
+                }
+                if (!this.socket.isOutputShutdown()) {
+                    this.socket.shutdownOutput();
+                }
+                this.socket.close();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        this.closed = true;
+    }
+
+    public boolean isClosed() {
+        return this.closed;
+    }
+
+    public synchronized Message request(String uri, Object body) {
+        HashMap<String, String> meta = new HashMap<>();
+        meta.put("X-Accept-Body-Codec", "106");
+        Message sendmsg = new Message("", 1, uri, meta, 'j', new Gson().toJson(body));
+        Message recvmsg = null;
+        try {
+            OutputStream outputStream = socket.getOutputStream(); // 获取一个输出流,向服务端发送信息
+            outputStream.write(sendmsg.encode());
+            outputStream.flush();
+//            PrintWriter printWriter = new PrintWriter(outputStream); // 将输出流包装成打印流
+//            printWriter.print(s);
+//            printWriter.flush();
+
+            InputStream inputStream = socket.getInputStream(); // 获取一个输入流,接收服务端的信息
+            recvmsg = new Message();
+            recvmsg.decode(inputStream);
+//            System.out.println(recvmsg.getBody());
+        } catch (Exception e) {
+            // Output or receive message error do nothing.
+        }
+        // 关闭相对应的资源,我们这里不需要
+
+        return recvmsg;
+    }
+
+    @Override
+    public void run() { // For heartbeat
+        int heartbeatSecond = 3;
+        while (!closed) {
+            try {
+                Message m = request("/heartbeat?hb_" + heartbeatSecond, "");
+                if (m == null) {
+                    reconnect();
+                }
+                Thread.sleep(heartbeatSecond * 1000);
+            } catch (ConnectException e) {
+                // Reconnect exception do nothing.
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private synchronized void reconnect() throws IOException {
+        this.alive = false;
+        this.socket.close();
+        SocketAddress address = this.socket.getRemoteSocketAddress();
+        Socket newSocket = new Socket();
+        newSocket.connect(address);
+        this.socket = newSocket;
+        this.alive = true;
+    }
+}

+ 70 - 0
src/com/wecise/odb/Error.java

@@ -0,0 +1,70 @@
+package com.wecise.odb;
+
+public class Error {
+    public static String cannotConvertToBigDecimal = "Cannot convert value to java.math.BigDecimal";
+    public static String cannotInferColumns = "Cannot infer column types until first row is fetched";
+    public static String caseNotLogical = "CASE condition must result in true or false";
+    public static String closedConnection = "Connection is already closed";
+    public static String closedResultSet = "ResultSet is already closed";
+    public static String columnNotInGroupBy = "Column not included in GROUP BY";
+    public static String columnsRead = "Columns read";
+    public static String columnsExpected = "Columns expected";
+    public static String columnsWithAggregateFunctions = "Query columns cannot be combined with aggregate functions";
+    public static String dansDbfError = "DANS DBF Library error";
+    public static String dataReaderError = "Error initializing DataReader";
+    public static String dbfTypeNotSupported = "DBF Data Type not supported";
+    public static String dirNotFound = "Directory not found";
+    public static String duplicateColumns = "Table contains duplicate column names";
+    public static String eofInQuotes = "Reached end of file inside quotes starting at line";
+    public static String expectedSeparator = "Expected separator at line and position";
+    public static String fileNotFound = "File not found";
+    public static String fileNotReadable = "File not readable";
+    public static String fileReadError = "Error reading file";
+    public static String functionArgCount = "Wrong number of arguments to SQL function";
+    public static String functionArgClass = "Java class of SQL function argument not supported";
+    public static String havingNotLogical = "HAVING clause must result in true or false";
+    public static String initFailed = "Failed to initialize CsvJdbc driver";
+    public static String interfaceNotImplemented = "Class does not implement interface";
+    public static String invalid = "Invalid";
+    public static String invalidColumnIndex = "Invalid column index";
+    public static String invalidColumnName = "Invalid column name";
+    public static String invalidColumnType = "Invalid column type";
+    public static String invalidQueryExpression = "Invalid expression in query";
+    public static String invalidFunction = "Invalid SQL function name";
+    public static String invalidGroupBy = "Invalid GROUP BY column";
+    public static String invalidHaving = "Invalid HAVING column";
+    public static String invalidOrderBy = "Invalid ORDER BY column";
+    public static String invalidProperty = "Invalid Property";
+    public static String invalidResultSetType = "ResultSet type invalid";
+    public static String joinNotSupported = "JOIN not supported";
+    public static String methodNotSupported = "Method not supported";
+    public static String noAggregateFunctions = "Aggregate functions not allowed in WHERE clause";
+    public static String noCodecClass = "Codec class not found";
+    public static String noColumnsSelected = "Malformed SQL. No columns selected";
+    public static String noCurrentRow = "No current row, perhaps you need to call next";
+    public static String noCryptoFilter = "Could not initialize CryptoFilter";
+    public static String noDansDbf = "DANS DBF Library classes not found";
+    public static String noFunction = "SQL function not found";
+    public static String noFunctionClass = "Java class for SQL function not found";
+    public static String noFunctionMethod = "Static Java method for SQL function not found";
+    public static String noGetMethod = "No previous getter method called";
+    public static String noLocale = "Locale not available";
+    public static String noPath = "Path not provided";
+    public static String orderByNotInGroupBy = "ORDER BY column not included in GROUP BY";
+    public static String parameterIndex = "Parameter index out of range";
+    public static String statementClosed = "Statement is already closed";
+    public static String streamClosed = "Stream is already closed";
+    public static String subqueryNotSupported = "Subquery not supported";
+    public static String subqueryOneColumn = "Subquery must return one column";
+    public static String subqueryOneRow = "Subquery must return one row";
+    public static String syntaxError = "Syntax error";
+    public static String tableNotFound = "Table not found";
+    public static String unknownCommandLine = "Unknown command line option";
+    public static String unsupportedDirection = "Direction not supported";
+    public static String unsupportedHoldability = "Holdability not supported";
+    public static String whereNotLogical = "WHERE clause must result in true or false";
+    public static String wrongColumnCount = "Wrong number of columns in line";
+    public static String wrongResultSetType = "Method not allowed for result set type TYPE_FORWARD_ONLY";
+    public static String zipOpenError = "Error opening ZIP file";
+    public static String unableConnectServer = "Unable to connect to server";
+}

+ 244 - 0
src/com/wecise/odb/Message.java

@@ -0,0 +1,244 @@
+package com.wecise.odb;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.stream.JsonReader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.HashMap;
+
+public class Message {
+    private String seq;
+    private int mtype;
+    private String uri;
+    private HashMap<String, String> meta;
+    private int body_codec;
+    private String body;
+
+    public Message(String seq, int mtype, String uri, HashMap<String, String> meta, int bodyCodec, String body) {
+        this.seq = seq;
+        this.mtype = mtype;
+        this.uri = uri;
+        this.meta = meta;
+        this.body_codec = bodyCodec;
+        this.body = body;
+    }
+
+    public Message() {
+
+    }
+
+    /*
+	  {length bytes} {xfer_pipe length byte} {xfer_pipe bytes} {JSON bytes}
+
+	  {length bytes}: uint32, 4 bytes, big endian
+	  {xfer_pipe length byte}: 1 byte
+	  {xfer_pipe bytes}: one byte one xfer
+	  {JSON bytes}: {"seq":%d,"mtype":%d,"uri":%q,"meta":%q,"body_codec":%d,"body":"%s"}
+	 */
+    public byte[] encode() {
+        StringBuilder meta = new StringBuilder();
+        for (String key : this.meta.keySet()) {
+            if (!meta.toString().equals("")) {
+                meta.append("&");
+            }
+            meta.append(String.format("%s=%s", key, this.meta.get(key)));
+        }
+
+        HashMap msg = new <String, Object>HashMap();
+        msg.put("seq", this.seq);
+        msg.put("mtype", this.mtype);
+        msg.put("uri", this.uri);
+        msg.put("body_codec", this.body_codec);
+        msg.put("body", this.body);
+        msg.put("meta", meta.toString());
+
+        byte[] content = new Gson().toJson(msg).getBytes();
+        byte[] xferBytes = new byte[0]; // no filter now
+
+        int size = 1 + xferBytes.length + content.length;
+        byte[] b =  new byte[4 + size];
+        // lenght bytes copy into total bytes. golang: binary.BigEndian.PutUint32(b, size)
+        byte[] lengthBytes = Util.intToBytesBig(size);
+        System.arraycopy(lengthBytes, 0, b, 0, 4);
+        b[4] = (byte)xferBytes.length; // golang: b[4] = byte(len(xferBytes))
+        System.arraycopy(xferBytes, 0, b, 4 + 1, xferBytes.length); // golang: copy(b[4+1:], xferBytes)
+        System.arraycopy(content, 0, b, 4 + 1 + xferBytes.length, content.length); // golang: copy(b[4+1+len(xferBytes):], content)
+
+        return b;
+    }
+
+    /* golang implements:
+    b := make([]byte, 2048)
+
+	message := new(bytes.Buffer)
+	var isMessage bool
+	var size uint32
+	var xferLen uint32
+	var remaining int
+	for {
+		if !isMessage { // read data length
+			n, err := conn.Read(b)
+			if err != nil {
+				log.Fatal(err)
+			}
+			buf := bytes.NewBuffer(b[:4])
+			//var size uint32
+			err = binary.Read(buf, binary.BigEndian, &size)
+			if err != nil {
+				log.Fatal(err)
+			}
+			//log.Println("total:", size, "read:", n)
+
+			buf.Reset()
+			buf.Write(b[4:5]) // xfer length
+			xferLen = uint32(buf.Bytes()[0])
+
+			var xfers []rune // xfer does not use
+			buf.Reset()
+			buf.Write(b[5:5+xferLen]) // xfer
+			for _, bt := range buf.Bytes() {
+				xfers = append(xfers, rune(bt))
+			}
+
+			message.Write(b[5+xferLen : n]) // data
+			isMessage = true // next read message
+		} else {
+			if int(remaining) < len(b) {
+				b = make([]byte, remaining)
+			}
+			n, err := conn.Read(b)
+			if err != nil {
+				log.Fatal(err)
+			}
+			message.Write(b[:n])
+		}
+
+		remaining = int(size) - 1 - int(xferLen) - message.Len()
+		log.Println("length:", size, "read:", message.Len(), "remaining:", remaining)
+		if remaining <= 0 {
+			break
+		}
+	}
+	log.Println("Recv:", message.String())
+    */
+    public void decode(InputStream inputStream) {
+        byte[] b = new byte[2048];
+
+        byte[] message = new byte[0];
+        boolean isMessage = false;
+        int size = 0;
+        int xferLen = 0;
+        int remaining = 0;
+        while (true) {
+            try {
+                if (!isMessage) {
+                    int n = inputStream.read(b);
+                    size = Util.bytesToIntBig(Util.subBytes(b, 0, 4), 0);
+
+                    xferLen = (int)Util.subBytes(b, 4, 1)[0]; // Java default is big endian
+                    char[] xfers = new char[xferLen];
+                    byte[] xferBytes = Util.subBytes(b, 5, xferLen);
+                    for (int i = 0; i < xferBytes.length; i++) {
+                        xfers[i] = (char) xferBytes[i];
+                    }
+
+                    message = Util.bytesMerger(message, Util.subBytes(b, 5 + xferLen, n - 5 - xferLen)); // data
+
+                    isMessage = true;
+                } else {
+                    if (remaining < b.length) {
+                        b = new byte[remaining];
+                    }
+                    int n = inputStream.read(b);
+                    if (n < b.length) {
+                        message = Util.bytesMerger(message, Util.subBytes(b, 0, n)); // data
+                    } else {
+                        message = Util.bytesMerger(message, b);
+                    }
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+                break;
+            }
+
+            remaining = size -1 -xferLen - message.length;
+            if (remaining <= 0) {
+                break;
+            }
+        }
+        StringReader reader = new StringReader(new String(message));
+        JsonReader jr = new JsonReader(reader);
+        jr.setLenient(true);
+        JsonElement js = new Gson().fromJson(jr, JsonElement.class);
+        this.seq = js.getAsJsonObject().get("seq").getAsString();
+        this.mtype = js.getAsJsonObject().get("mtype").getAsInt();
+        this.uri = js.getAsJsonObject().get("uri").getAsString();
+        String metaStr = js.getAsJsonObject().get("meta").getAsString();
+        HashMap<String, String> meta = new HashMap<>();
+        if (metaStr != null) {
+            String[] a = metaStr.split("&");
+            if (a.length > 0) {
+                for (String s: a) {
+                    if (!s.equals("")) {
+                        String[] kv = s.split("=");
+                        meta.put(kv[0], kv[1]);
+                    }
+                }
+            }
+        }
+        this.meta = meta;
+        this.body_codec = js.getAsJsonObject().get("body_codec").getAsInt();
+        this.body = js.getAsJsonObject().get("body").getAsString();
+    }
+
+    public String getSeq() {
+        return seq;
+    }
+
+    public void setSeq(String seq) {
+        this.seq = seq;
+    }
+
+    public int getMtype() {
+        return mtype;
+    }
+
+    public void setMtype(int mtype) {
+        this.mtype = mtype;
+    }
+
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+
+    public HashMap<String, String> getMeta() {
+        return meta;
+    }
+
+    public void setMeta(HashMap<String, String> meta) {
+        this.meta = meta;
+    }
+
+    public int getBody_codec() {
+        return body_codec;
+    }
+
+    public void setBody_codec(int body_codec) {
+        this.body_codec = body_codec;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public void setBody(String body) {
+        this.body = body;
+    }
+}

+ 184 - 0
src/com/wecise/odb/ObjectPool.java

@@ -0,0 +1,184 @@
+package com.wecise.odb;
+
+import java.util.Enumeration;
+import java.util.Vector;
+
+/*
+    从网上找的一个很不负责任的代码,错误太多,改了好多。。
+ */
+public class ObjectPool {
+//    private int numObjects = 10; // 对象池的大小
+//    private int maxObjects = 50; // 对象池最大的大小
+    private Vector<PooledObject> objects = null; //存放对象池中对象的向量( PooledObject类型)
+
+    public ObjectPool(Object... objs) {
+        createPool(objs);
+    }
+
+    /*** 创建一个对象池***/
+    private synchronized void createPool(Object... objs) {
+        // 确保对象池没有创建。如果创建了,保存对象的向量 objects 不会为空
+        if (objects != null) {
+            return; // 如果己经创建,则返回
+        }
+
+        // 创建保存对象的向量 , 初始时有 0 个元素
+        objects = new Vector<>();
+        for (Object o: objs) {
+            objects.addElement(new PooledObject(o));
+        }
+    }
+
+    public synchronized Object get() {
+        // 确保对象池己被创建
+        if (objects == null) {
+            return null; // 对象池还没创建,则返回 null
+        }
+
+        Object o = getFreeObject(); // 获得一个可用的对象
+
+        // 如果目前没有可以使用的对象,即所有的对象都在使用中
+        while (o == null) {
+            wait(250);
+            o = getFreeObject(); // 重新再试,直到获得可用的对象,如果
+        }
+
+        return o;// 返回获得的可用的对象
+    }
+
+    /**
+     * 本函数从对象池对象 objects 中返回一个可用的的对象,如果
+     * 当前没有可用的对象,则创建几个对象,并放入对象池中。
+     * 如果创建后,所有的对象都在使用中,则返回 null
+     */
+    private Object getFreeObject() {
+        return findFreeObject(); // 从对象池中获得一个可用的对象
+    }
+
+    /**
+     * 查找对象池中所有的对象,查找一个可用的对象,
+     * 如果没有可用的对象,返回 null
+     */
+    private Object findFreeObject() {
+
+        Object obj = null;
+        PooledObject pObj;
+
+        // 获得对象池向量中所有的对象
+        Enumeration enumerate = objects.elements();
+
+        // 遍历所有的对象,看是否有可用的对象
+        while (enumerate.hasMoreElements()) {
+            pObj = (PooledObject) enumerate.nextElement();
+
+            // 如果此对象不忙,则获得它的对象并把它设为忙
+            if (!pObj.isBusy()) {
+                obj = pObj.getObject();
+                pObj.setBusy(true);
+                break;
+            }
+        }
+        return obj; // 返回找到到的可用对象
+    }
+
+    /**
+     * 此函数返回一个对象到对象池中,并把此对象置为空闲。
+     * 所有使用对象池获得的对象均应在不使用此对象时返回它。
+     */
+
+    public void release(Object obj) {
+        // 确保对象池存在,如果对象没有创建(不存在),直接返回
+        if (objects == null) {
+            return;
+        }
+
+        PooledObject pObj;
+        Enumeration enumerate = objects.elements();
+
+        // 遍历对象池中的所有对象,找到这个要返回的对象对象
+        while (enumerate.hasMoreElements()) {
+            pObj = (PooledObject) enumerate.nextElement();
+
+            // 先找到对象池中的要返回的对象对象
+            if (obj == pObj.getObject()) {
+                // 找到了, 设置此对象为空闲状态
+                objects.removeElement(pObj);
+                objects.addElement(pObj);
+                pObj.setBusy(false);
+                break;
+            }
+        }
+    }
+
+    /**
+     * 关闭对象池中所有的对象,并清空对象池。
+     */
+    public synchronized void closeObjectPool () {
+        // 确保对象池存在,如果不存在,返回
+        if (objects == null) {
+            return;
+        }
+
+        PooledObject pObj;
+        Enumeration enumerate = objects.elements();
+
+        while (enumerate.hasMoreElements()) {
+            pObj = (PooledObject) enumerate.nextElement();
+            // 如果忙,等 5 秒
+            if (pObj.isBusy()) {
+                wait(5000); // 等 5 秒
+            }
+            // 从对象池向量中删除它
+            objects.removeElement(pObj);
+        }
+
+        // 置对象池为空
+        objects = null;
+    }
+
+    /**
+     * 使程序等待给定的毫秒数
+     */
+    private void wait (int mSeconds){
+        try {
+            Thread.sleep(mSeconds);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 内部使用的用于保存对象池中对象的类。
+     * 此类中有两个成员,一个是对象,另一个是指示此对象是否正在使用的标志 。
+     */
+    class PooledObject {
+        Object objection;// 对象
+        boolean busy = false; // 此对象是否正在使用的标志,默认没有正在使用
+
+        // 构造函数,根据一个 Object 构告一个 PooledObject 对象
+        private PooledObject(Object obj) {
+            this.objection = obj;
+        }
+
+        // 返回此对象中的对象
+        public Object getObject() {
+            return objection;
+        }
+
+        // 设置此对象的,对象
+        public void setObject(Object objection) {
+            this.objection = objection;
+
+        }
+
+        // 获得对象对象是否忙
+        private boolean isBusy() {
+            return busy;
+        }
+
+        // 设置对象的对象正在忙
+        private void setBusy(boolean busy) {
+            this.busy = busy;
+        }
+    }
+}

+ 79 - 0
src/com/wecise/odb/Util.java

@@ -0,0 +1,79 @@
+package com.wecise.odb;
+
+public class Util {
+
+    /*
+     * 截取byte[]数组
+     */
+    public static byte[] subBytes(byte[] src, int begin, int count) {
+        byte[] bs = new byte[count];
+        System.arraycopy(src, begin, bs, 0, count);
+        return bs;
+    }
+
+    /**
+     * 以大端模式将int转成byte[]
+     */
+    public static byte[] intToBytesBig(int value) {
+        byte[] src = new byte[4];
+        src[0] = (byte) ((value >> 24) & 0xFF);
+        src[1] = (byte) ((value >> 16) & 0xFF);
+        src[2] = (byte) ((value >> 8) & 0xFF);
+        src[3] = (byte) (value & 0xFF);
+        return src;
+    }
+
+    /**
+     * 以小端模式将int转成byte[]
+     *
+     * @param value
+     * @return
+     */
+    public static byte[] intToBytesLittle(int value) {
+        byte[] src = new byte[4];
+        src[3] = (byte) ((value >> 24) & 0xFF);
+        src[2] = (byte) ((value >> 16) & 0xFF);
+        src[1] = (byte) ((value >> 8) & 0xFF);
+        src[0] = (byte) (value & 0xFF);
+        return src;
+    }
+
+    /**
+     * 以大端模式将byte[]转成int
+     */
+    public static int bytesToIntBig(byte[] src, int offset) {
+        int value;
+        value = (int) (((src[offset] & 0xFF) << 24)
+                | ((src[offset + 1] & 0xFF) << 16)
+                | ((src[offset + 2] & 0xFF) << 8)
+                | (src[offset + 3] & 0xFF));
+        return value;
+    }
+
+    /**
+     * 以小端模式将byte[]转成int
+     */
+    public static int bytesToIntLittle(byte[] src, int offset) {
+        int value;
+        value = (int) ((src[offset] & 0xFF)
+                | ((src[offset + 1] & 0xFF) << 8)
+                | ((src[offset + 2] & 0xFF) << 16)
+                | ((src[offset + 3] & 0xFF) << 24));
+        return value;
+    }
+
+
+    public static byte[] bytesMerger(byte[]... values) {
+        int length_byte = 0;
+        for (byte[] value : values) {
+            length_byte += value.length;
+        }
+        byte[] all_byte = new byte[length_byte];
+        int countLength = 0;
+        for (byte[] b : values) {
+            System.arraycopy(b, 0, all_byte, countLength, b.length);
+            countLength += b.length;
+        }
+        return all_byte;
+    }
+}

+ 366 - 0
src/com/wecise/odb/jdbc/ODBConnection.java

@@ -0,0 +1,366 @@
+package com.wecise.odb.jdbc;
+
+import com.wecise.odb.Connector;
+import com.wecise.odb.Error;
+import com.wecise.odb.ObjectPool;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+
+public class ODBConnection implements Connection {
+    private String address;
+    private String keyspace;
+    private Connector[] connectors;
+    private ObjectPool pool;
+    private boolean autoCommit = false;
+    private boolean closed = false;
+    private int numConns = 2; // number of connections per host (default: 2)
+
+    ODBConnection(String address, String keyspace) throws SQLException {
+        this.address = address;
+        this.keyspace = keyspace;
+        String[] cluster = address.split(",");
+        connectors = new Connector[cluster.length * numConns];
+        int num = 0;
+        for (String c: cluster) {
+            String[] a = c.split(":");
+            String host = a[0];
+            int port = 9062;
+            if (a.length > 1) {
+                port = Integer.parseInt(a[1]);
+            }
+            try {
+                for (int k = 0; k < numConns; k++) {
+                    Connector ctr = new Connector(new Socket(host, port));
+                    new Thread(ctr).start();
+                    this.connectors[num] = ctr;
+                    num++;
+                }
+            } catch (IOException e) {
+                throw new SQLException(e.toString());
+            }
+        }
+        this.pool = new ObjectPool(connectors);
+    }
+
+    public Connector getConnector() {
+        Connector ctr;
+        ArrayList<Connector> offlineSockets = new ArrayList<>();
+        while (true) {
+            ctr = (Connector) this.pool.get();
+            if (ctr.isAlive()) {
+                break;
+            } else {
+                offlineSockets.add(ctr);
+            }
+        }
+        for (Connector offctr: offlineSockets) {
+            releaseConnector(offctr);
+        }
+
+        return ctr;
+    }
+
+    public void releaseConnector(Connector socket) {
+        this.pool.release(socket);
+    }
+
+    @Override
+    public Statement createStatement() throws SQLException {
+        return new ODBStatement(this);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String s) throws SQLException {
+        return new ODBPreparedStatement(this, s);
+    }
+
+    @Override
+    public CallableStatement prepareCall(String s) throws SQLException {
+        throw new SQLFeatureNotSupportedException(Error.methodNotSupported + ": Connection.prepareCall(String)");
+    }
+
+    @Override
+    public String nativeSQL(String s) throws SQLException {
+        throw new SQLFeatureNotSupportedException(Error.methodNotSupported + ": Connection.nativeSQL(String)");
+    }
+
+    @Override
+    public void setAutoCommit(boolean b) throws SQLException {
+        this.autoCommit = b;
+    }
+
+    @Override
+    public boolean getAutoCommit() throws SQLException {
+        return this.autoCommit;
+    }
+
+    @Override
+    public void commit() throws SQLException {
+
+    }
+
+    @Override
+    public void rollback() throws SQLException {
+
+    }
+
+    @Override
+    public void close() throws SQLException {
+        if (!this.closed) {
+            this.pool.closeObjectPool();
+            for (Connector c: this.connectors) {
+                if (!c.isClosed()) {
+                    c.close();
+                }
+            }
+        }
+        this.closed = true;
+    }
+
+    @Override
+    public boolean isClosed() throws SQLException {
+        return this.closed;
+    }
+
+    @Override
+    public DatabaseMetaData getMetaData() throws SQLException {
+        return new ODBDatabaseMetaData(this);
+    }
+
+    @Override
+    public void setReadOnly(boolean b) throws SQLException {
+
+    }
+
+    @Override
+    public boolean isReadOnly() throws SQLException {
+        return false;
+    }
+
+    @Override
+    public void setCatalog(String s) throws SQLException {
+
+    }
+
+    @Override
+    public String getCatalog() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void setTransactionIsolation(int i) throws SQLException {
+
+    }
+
+    @Override
+    public int getTransactionIsolation() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public SQLWarning getWarnings() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void clearWarnings() throws SQLException {
+
+    }
+
+    @Override
+    public Statement createStatement(int i, int i1) throws SQLException {
+        return new ODBStatement(this);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String s, int i, int i1) throws SQLException {
+        return new ODBPreparedStatement(this, s);
+    }
+
+    @Override
+    public CallableStatement prepareCall(String s, int i, int i1) throws SQLException {
+        throw new SQLFeatureNotSupportedException(Error.methodNotSupported + ": Connection.prepareCall(String, int, int)");
+    }
+
+    @Override
+    public Map<String, Class<?>> getTypeMap() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+
+    }
+
+    @Override
+    public void setHoldability(int i) throws SQLException {
+
+    }
+
+    @Override
+    public int getHoldability() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public Savepoint setSavepoint() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public Savepoint setSavepoint(String s) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void rollback(Savepoint savepoint) throws SQLException {
+
+    }
+
+    @Override
+    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
+
+    }
+
+    @Override
+    public Statement createStatement(int i, int i1, int i2) throws SQLException {
+        return new ODBStatement(this);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String s, int i, int i1, int i2) throws SQLException {
+        return new ODBPreparedStatement(this, s);
+    }
+
+    @Override
+    public CallableStatement prepareCall(String s, int i, int i1, int i2) throws SQLException {
+        throw new SQLFeatureNotSupportedException(Error.methodNotSupported + ": Connection.prepareCall(String, int, int, int)");
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String s, int i) throws SQLException {
+        return new ODBPreparedStatement(this, s);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String s, int[] ints) throws SQLException {
+        return new ODBPreparedStatement(this, s);
+    }
+
+    @Override
+    public PreparedStatement prepareStatement(String s, String[] strings) throws SQLException {
+        return new ODBPreparedStatement(this, s);
+    }
+
+    @Override
+    public Clob createClob() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public Blob createBlob() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public NClob createNClob() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public SQLXML createSQLXML() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public boolean isValid(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public void setClientInfo(String s, String s1) throws SQLClientInfoException {
+
+    }
+
+    @Override
+    public void setClientInfo(Properties properties) throws SQLClientInfoException {
+
+    }
+
+    @Override
+    public String getClientInfo(String s) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public Properties getClientInfo() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public Array createArrayOf(String s, Object[] objects) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public Struct createStruct(String s, Object[] objects) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void setSchema(String s) throws SQLException {
+
+    }
+
+    @Override
+    public String getSchema() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void abort(Executor executor) throws SQLException {
+
+    }
+
+    @Override
+    public void setNetworkTimeout(Executor executor, int i) throws SQLException {
+
+    }
+
+    @Override
+    public int getNetworkTimeout() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public <T> T unwrap(Class<T> aClass) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public boolean isWrapperFor(Class<?> aClass) throws SQLException {
+        return false;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public void setAddress(String address) {
+        this.address = address;
+    }
+
+    public String getKeyspace() {
+        return keyspace;
+    }
+
+    public void setKeyspace(String keyspace) {
+        this.keyspace = keyspace;
+    }
+}

Plik diff jest za duży
+ 1024 - 0
src/com/wecise/odb/jdbc/ODBDatabaseMetaData.java


+ 69 - 0
src/com/wecise/odb/jdbc/ODBDriver.java

@@ -0,0 +1,69 @@
+package com.wecise.odb.jdbc;
+
+import com.wecise.odb.Error;
+
+import java.sql.*;
+import java.util.Properties;
+import java.util.logging.Logger;
+
+public class ODBDriver implements Driver {
+
+    private final static String URL_PREFIX = "jdbc:wecise:omdb:";
+    private final static String JDBC_DRIVER = "com.wecise.odb.jdbc.ODBDriver";
+
+    @Override
+    public Connection connect(String s, Properties properties) throws SQLException {
+        String dbstr = s.substring(s.indexOf(URL_PREFIX) + URL_PREFIX.length());
+        dbstr = dbstr.replaceAll("//", "");
+        String[] a = dbstr.split("/");
+        String address = "";
+        String keyspace = "";
+        if (a.length > 1) {
+            address = a[0];
+            keyspace = a[1];
+        } else {
+            throw new SQLException("Connect error, format: jdbc:wecise:omdb://<host:port>/<keyspace>");
+        }
+        return new ODBConnection(address, keyspace);
+    }
+
+    @Override
+    public boolean acceptsURL(String s) throws SQLException {
+        return s.startsWith(URL_PREFIX);
+    }
+
+    @Override
+    public DriverPropertyInfo[] getPropertyInfo(String s, Properties properties) throws SQLException {
+        return new DriverPropertyInfo[0];
+    }
+
+    @Override
+    public int getMajorVersion() {
+        return 1;
+    }
+
+    @Override
+    public int getMinorVersion() {
+        return 0;
+    }
+
+    @Override
+    public boolean jdbcCompliant() {
+        return false;
+    }
+
+    @Override
+    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+        throw new SQLFeatureNotSupportedException(Error.methodNotSupported + ": Driver.getParentLogger()");
+//        return null;
+    }
+
+    // This static block inits the driver when the class is loaded by the JVM.
+    static {
+        try {
+            DriverManager.registerDriver(new ODBDriver());
+        } catch (SQLException e) {
+            throw new RuntimeException("Failed to initialize ODBJdbc driver: " + e.getMessage());
+        }
+    }
+}

+ 334 - 0
src/com/wecise/odb/jdbc/ODBPreparedStatement.java

@@ -0,0 +1,334 @@
+package com.wecise.odb.jdbc;
+
+import com.wecise.odb.Error;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.Socket;
+import java.net.URL;
+import java.sql.*;
+import java.util.Calendar;
+
+public class ODBPreparedStatement extends ODBStatement implements PreparedStatement {
+
+    private Object[] parameters;
+    private String sql;
+
+    public ODBPreparedStatement(Connection conn, String sql) {
+        super(conn);
+        this.sql = sql;
+        this.parseSql();
+    }
+
+    private void parseSql() {
+        int num = 0;
+        char[] a = this.sql.toCharArray();
+        boolean ok = true;
+        boolean escape = false;
+        for (char c: a) {
+            if (!escape) {
+                if (c == '\\') {
+                    escape = true;
+                    continue;
+                }
+            } else {
+                escape = false;
+                continue;
+            }
+            if (c == '\'' || c == '"') {
+                ok = !ok;
+            }
+            if (ok && c == '?') {
+                num += 1;
+            }
+        }
+        this.parameters = new Object[num];
+    }
+
+    private void setParam(int i, Object o) throws SQLException {
+        if (i < 1 || i > parameters.length) {
+            throw new SQLException(Error.parameterIndex + ": " + i);
+        }
+        int index = i - 1; // Sql start with 1, java start with 0
+        this.parameters[index] = o;
+    }
+
+    @Override
+    public ResultSet executeQuery() throws SQLException {
+        exec(this.sql, this.parameters);
+        return this.getResultSet();
+    }
+
+    @Override
+    public int executeUpdate() throws SQLException {
+        exec(this.sql, this.parameters);
+        return 0;
+    }
+
+    @Override
+    public void setNull(int i, int i1) throws SQLException {
+
+    }
+
+    @Override
+    public void setBoolean(int i, boolean b) throws SQLException {
+        setParam(i, b);
+    }
+
+    @Override
+    public void setByte(int i, byte b) throws SQLException {
+        setParam(i, b);
+    }
+
+    @Override
+    public void setShort(int i, short i1) throws SQLException {
+        setParam(i, i1);
+    }
+
+    @Override
+    public void setInt(int i, int i1) throws SQLException {
+        setParam(i, i1);
+    }
+
+    @Override
+    public void setLong(int i, long l) throws SQLException {
+        setParam(i, l);
+    }
+
+    @Override
+    public void setFloat(int i, float v) throws SQLException {
+        setParam(i, v);
+    }
+
+    @Override
+    public void setDouble(int i, double v) throws SQLException {
+        setParam(i, v);
+    }
+
+    @Override
+    public void setBigDecimal(int i, BigDecimal bigDecimal) throws SQLException {
+        setParam(i, bigDecimal);
+    }
+
+    @Override
+    public void setString(int i, String s) throws SQLException {
+        setParam(i, s);
+    }
+
+    @Override
+    public void setBytes(int i, byte[] bytes) throws SQLException {
+        setParam(i, bytes);
+    }
+
+    @Override
+    public void setDate(int i, Date date) throws SQLException {
+        setParam(i, date);
+    }
+
+    @Override
+    public void setTime(int i, Time time) throws SQLException {
+        setParam(i, time);
+    }
+
+    @Override
+    public void setTimestamp(int i, Timestamp timestamp) throws SQLException {
+        setParam(i, timestamp);
+    }
+
+    @Override
+    public void setAsciiStream(int i, InputStream inputStream, int i1) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setUnicodeStream(int i, InputStream inputStream, int i1) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setBinaryStream(int i, InputStream inputStream, int i1) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void clearParameters() throws SQLException {
+        this.parameters = new Object[0];
+    }
+
+    @Override
+    public void setObject(int i, Object o, int i1) throws SQLException {
+        setParam(i, o);
+    }
+
+    @Override
+    public void setObject(int i, Object o) throws SQLException {
+        setParam(i, o);
+    }
+
+    @Override
+    public boolean execute() throws SQLException {
+        exec(this.sql, this.parameters);
+        return this.getResultSet() != null;
+    }
+
+    @Override
+    public void addBatch() throws SQLException {
+        throw new SQLFeatureNotSupportedException(Error.methodNotSupported + ": PreparedStatement.addBatch()");
+    }
+
+    @Override
+    public void setCharacterStream(int i, Reader reader, int i1) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setRef(int i, Ref ref) throws SQLException {
+        setParam(i, ref);
+    }
+
+    @Override
+    public void setBlob(int i, Blob blob) throws SQLException {
+        setParam(i, blob);
+    }
+
+    @Override
+    public void setClob(int i, Clob clob) throws SQLException {
+        setParam(i, clob);
+    }
+
+    @Override
+    public void setArray(int i, Array array) throws SQLException {
+        setParam(i, array);
+    }
+
+    @Override
+    public ResultSetMetaData getMetaData() throws SQLException {
+        return this.getResultSet().getMetaData();
+    }
+
+    @Override
+    public void setDate(int i, Date date, Calendar calendar) throws SQLException {
+        setParam(i, date);
+    }
+
+    @Override
+    public void setTime(int i, Time time, Calendar calendar) throws SQLException {
+        setParam(i, time);
+    }
+
+    @Override
+    public void setTimestamp(int i, Timestamp timestamp, Calendar calendar) throws SQLException {
+        setParam(i, timestamp);
+    }
+
+    @Override
+    public void setNull(int i, int i1, String s) throws SQLException {
+
+    }
+
+    @Override
+    public void setURL(int i, URL url) throws SQLException {
+        setParam(i, url);
+    }
+
+    @Override
+    public ParameterMetaData getParameterMetaData() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void setRowId(int i, RowId rowId) throws SQLException {
+        setParam(i, rowId);
+    }
+
+    @Override
+    public void setNString(int i, String s) throws SQLException {
+        setParam(i, s);
+    }
+
+    @Override
+    public void setNCharacterStream(int i, Reader reader, long l) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setNClob(int i, NClob nClob) throws SQLException {
+        setParam(i, nClob);
+    }
+
+    @Override
+    public void setClob(int i, Reader reader, long l) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setBlob(int i, InputStream inputStream, long l) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setNClob(int i, Reader reader, long l) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setSQLXML(int i, SQLXML sqlxml) throws SQLException {
+        setParam(i, sqlxml);
+    }
+
+    @Override
+    public void setObject(int i, Object o, int i1, int i2) throws SQLException {
+        setParam(i, o);
+    }
+
+    @Override
+    public void setAsciiStream(int i, InputStream inputStream, long l) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setBinaryStream(int i, InputStream inputStream, long l) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setCharacterStream(int i, Reader reader, long l) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setAsciiStream(int i, InputStream inputStream) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setBinaryStream(int i, InputStream inputStream) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setCharacterStream(int i, Reader reader) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setNCharacterStream(int i, Reader reader) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setClob(int i, Reader reader) throws SQLException {
+        setParam(i, reader);
+    }
+
+    @Override
+    public void setBlob(int i, InputStream inputStream) throws SQLException {
+        setParam(i, inputStream);
+    }
+
+    @Override
+    public void setNClob(int i, Reader reader) throws SQLException {
+        setParam(i, reader);
+    }
+}

Plik diff jest za duży
+ 1320 - 0
src/com/wecise/odb/jdbc/ODBResultSet.java


+ 181 - 0
src/com/wecise/odb/jdbc/ODBResultSetMetaData.java

@@ -0,0 +1,181 @@
+package com.wecise.odb.jdbc;
+
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ODBResultSetMetaData implements ResultSetMetaData {
+
+    /**
+     * Names of columns
+     */
+    private String[] columnNames;
+    private String[] columnLabels;
+    private String[] columnTypes;
+    private int[] columnDisplaySizes;
+    /**
+     * Name of table
+     */
+    private String tableName;
+
+    ODBResultSetMetaData(String tableName, String[] columnNames, String[] columnLabels, String[] columnTypes, int[] columnDisplaySizes) {
+        this.tableName = tableName;
+        this.columnNames = columnNames;
+        this.columnLabels = columnLabels;
+        this.columnTypes = columnTypes;
+        this.columnDisplaySizes = columnDisplaySizes;
+    }
+
+    @Override
+    public int getColumnCount() throws SQLException {
+        return this.columnTypes == null ? 0:this.columnTypes.length;
+    }
+
+    @Override
+    public boolean isAutoIncrement(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public boolean isCaseSensitive(int i) throws SQLException {
+        // all columns are lowercase
+        return false;
+    }
+
+    @Override
+    public boolean isSearchable(int i) throws SQLException {
+        // the implementation doesn't support the where clause
+        return false;
+    }
+
+    @Override
+    public boolean isCurrency(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public int isNullable(int i) throws SQLException {
+        return ResultSetMetaData.columnNullableUnknown;
+    }
+
+    @Override
+    public boolean isSigned(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public int getColumnDisplaySize(int i) throws SQLException {
+        return this.columnDisplaySizes[i - 1];
+    }
+
+    @Override
+    public String getColumnLabel(int i) throws SQLException {
+        // SQL column numbers start at 1
+        return this.columnLabels[i - 1];
+    }
+
+    @Override
+    public String getColumnName(int i) throws SQLException {
+        // SQL column numbers start at 1
+        return this.columnNames[i - 1];
+    }
+
+    @Override
+    public String getSchemaName(int i) throws SQLException {
+        return "";
+    }
+
+    @Override
+    public int getPrecision(int i) throws SQLException {
+        // What does it mean?
+        return 0;
+    }
+
+    @Override
+    public int getScale(int i) throws SQLException {
+        // What does it mean?
+        return 0;
+    }
+
+    @Override
+    public String getTableName(int i) throws SQLException {
+        return this.tableName;
+    }
+
+    @Override
+    public String getCatalogName(int i) throws SQLException {
+        return "";
+    }
+
+    private Map<String, Integer> typeNameToTypeCode = new HashMap<String, Integer>() {
+        private static final long serialVersionUID = -8819579540085202365L;
+        {
+            put("varchar", Types.VARCHAR);
+            put("text", Types.LONGVARCHAR);
+            put("int", Types.INTEGER);
+            put("smallint", Types.SMALLINT);
+            put("bigint", Types.BIGINT);
+            put("float", Types.FLOAT);
+            put("double", Types.DOUBLE);
+            put("date", Types.DATE);
+            put("timestamp", Types.TIMESTAMP);
+            put("blob", Types.BLOB);
+            // collection
+            put("map", Types.JAVA_OBJECT);
+            put("list", Types.JAVA_OBJECT);
+            put("set", Types.JAVA_OBJECT);
+            put("bucket", Types.JAVA_OBJECT);
+//            put("Boolean", Types.BOOLEAN);
+//            put("Byte", Types.TINYINT);
+//            put("Short", Types.SMALLINT);
+//            put("Integer", Types.INTEGER);
+//            put("BigDecimal", Types.DECIMAL);
+//            put("Time", Types.TIME);
+//            put("Clob", Types.CLOB);
+//            put("expression", Types.BLOB);
+        }
+    };
+
+    @Override
+    public int getColumnType(int i) throws SQLException {
+        String columnTypeName = getColumnTypeName(i);
+        return typeNameToTypeCode.get(columnTypeName);
+    }
+
+    @Override
+    public String getColumnTypeName(int i) throws SQLException {
+        return this.columnTypes[i - 1];
+    }
+
+    @Override
+    public boolean isReadOnly(int i) throws SQLException {
+        return true;
+    }
+
+    @Override
+    public boolean isWritable(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public boolean isDefinitelyWritable(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public String getColumnClassName(int i) throws SQLException {
+        return this.columnTypes[i - 1];
+    }
+
+    @Override
+    public <T> T unwrap(Class<T> aClass) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public boolean isWrapperFor(Class<?> aClass) throws SQLException {
+        return false;
+    }
+}

+ 289 - 0
src/com/wecise/odb/jdbc/ODBStatement.java

@@ -0,0 +1,289 @@
+package com.wecise.odb.jdbc;
+
+import com.google.gson.Gson;
+import com.wecise.odb.Connector;
+import com.wecise.odb.Error;
+import com.wecise.odb.Message;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ODBStatement implements Statement {
+
+    private Connection conn;
+    private boolean closed;
+    private ResultSet rs;
+    private ArrayList<String> batch;
+
+    ODBStatement(Connection conn) {
+        this.conn = conn;
+        this.closed = false;
+        this.batch = new ArrayList<>();
+    }
+
+    protected void exec(String s, Object[] values) throws SQLException {
+        Connector ctr = ((ODBConnection)this.conn).getConnector();
+//        System.out.println(ctr.getSocket().getRemoteSocketAddress());
+        try {
+            HashMap<String, Object> body = new HashMap<>();
+            body.put("keyspace", ((ODBConnection)this.conn).getKeyspace());
+            body.put("stmt", s);
+            if (values != null && values.length > 0) {
+                body.put("values", values);
+            }
+            Message recvmsg = ctr.request("/data/mql/execute", body);
+            if (recvmsg == null) {
+                throw new SQLException(Error.unableConnectServer);
+            }
+            Gson gs = new Gson();
+            Map recvBody = gs.fromJson(recvmsg.getBody(), Map.class); // {message:{data:[...],meta:{...}}, status:""}
+            String status = recvBody.get("status").toString();
+            if (status.equals("success")) {
+                Map msg = (Map) recvBody.get("message");
+                ArrayList data = (ArrayList) msg.get("data");
+                Map odbMeta = (Map) msg.get("meta");
+                this.rs = new ODBResultSet(data, odbMeta, this);
+            } else {
+                throw new SQLDataException(recvBody.get("message").toString());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new SQLDataException(e.toString());
+        } finally {
+            ((ODBConnection) this.conn).releaseConnector(ctr);
+        }
+    }
+
+    @Override
+    public ResultSet executeQuery(String s) throws SQLException {
+        exec(s, null);
+        return rs;
+    }
+
+    @Override
+    public int executeUpdate(String s) throws SQLException {
+        executeQuery(s);
+        return 0;
+    }
+
+    @Override
+    public void close() throws SQLException {
+        closed = true;
+    }
+
+    @Override
+    public int getMaxFieldSize() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public void setMaxFieldSize(int i) throws SQLException {
+
+    }
+
+    @Override
+    public int getMaxRows() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public void setMaxRows(int i) throws SQLException {
+
+    }
+
+    @Override
+    public void setEscapeProcessing(boolean b) throws SQLException {
+
+    }
+
+    @Override
+    public int getQueryTimeout() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public void setQueryTimeout(int i) throws SQLException {
+
+    }
+
+    @Override
+    public void cancel() throws SQLException {
+
+    }
+
+    @Override
+    public SQLWarning getWarnings() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public void clearWarnings() throws SQLException {
+
+    }
+
+    @Override
+    public void setCursorName(String s) throws SQLException {
+
+    }
+
+    @Override
+    public boolean execute(String s) throws SQLException {
+        executeQuery(s);
+        return this.rs != null;
+    }
+
+    @Override
+    public ResultSet getResultSet() throws SQLException {
+        return this.rs;
+    }
+
+    @Override
+    public int getUpdateCount() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public boolean getMoreResults() throws SQLException {
+        return false;
+    }
+
+    @Override
+    public void setFetchDirection(int i) throws SQLException {
+
+    }
+
+    @Override
+    public int getFetchDirection() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public void setFetchSize(int i) throws SQLException {
+
+    }
+
+    @Override
+    public int getFetchSize() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public int getResultSetConcurrency() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public int getResultSetType() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public void addBatch(String s) throws SQLException {
+        this.batch.add(s);
+    }
+
+    @Override
+    public void clearBatch() throws SQLException {
+        this.batch.clear();
+    }
+
+    @Override
+    public int[] executeBatch() throws SQLException {
+        String s = String.join(";", this.batch);
+        executeQuery(s);
+        return new int[0];
+    }
+
+    @Override
+    public Connection getConnection() throws SQLException {
+        return this.conn;
+    }
+
+    @Override
+    public boolean getMoreResults(int i) throws SQLException {
+        return false;
+    }
+
+    @Override
+    public ResultSet getGeneratedKeys() throws SQLException {
+        return null;
+    }
+
+    @Override
+    public int executeUpdate(String s, int i) throws SQLException {
+        executeQuery(s);
+        return 0;
+    }
+
+    @Override
+    public int executeUpdate(String s, int[] ints) throws SQLException {
+        executeQuery(s);
+        return 0;
+    }
+
+    @Override
+    public int executeUpdate(String s, String[] strings) throws SQLException {
+        executeQuery(s);
+        return 0;
+    }
+
+    @Override
+    public boolean execute(String s, int i) throws SQLException {
+        executeQuery(s);
+        return this.rs != null;
+    }
+
+    @Override
+    public boolean execute(String s, int[] ints) throws SQLException {
+        executeQuery(s);
+        return this.rs != null;
+    }
+
+    @Override
+    public boolean execute(String s, String[] strings) throws SQLException {
+        executeQuery(s);
+        return this.rs != null;
+    }
+
+    @Override
+    public int getResultSetHoldability() throws SQLException {
+        return 0;
+    }
+
+    @Override
+    public boolean isClosed() throws SQLException {
+        return closed;
+    }
+
+    @Override
+    public void setPoolable(boolean b) throws SQLException {
+
+    }
+
+    @Override
+    public boolean isPoolable() throws SQLException {
+        return false;
+    }
+
+    @Override
+    public void closeOnCompletion() throws SQLException {
+
+    }
+
+    @Override
+    public boolean isCloseOnCompletion() throws SQLException {
+        return false;
+    }
+
+    @Override
+    public <T> T unwrap(Class<T> aClass) throws SQLException {
+        return null;
+    }
+
+    @Override
+    public boolean isWrapperFor(Class<?> aClass) throws SQLException {
+        return false;
+    }
+}

+ 10 - 0
src/com/wecise/odb/table/Align.java

@@ -0,0 +1,10 @@
+package com.wecise.odb.table;
+
+/**
+ * Created by fabmars on 17/09/2016.
+ */
+public enum Align {
+  LEFT,
+  CENTER,
+  RIGHT;
+}

+ 15 - 0
src/com/wecise/odb/table/ConsoleCellRenderer.java

@@ -0,0 +1,15 @@
+package com.wecise.odb.table;
+
+/**
+ * Created by mars on 27/09/2016.
+ */
+public interface ConsoleCellRenderer<T> {
+
+  /**
+   * @param value
+   * @param rowNum
+   * @param colNum
+   * @return textual representation of the value
+   */
+  String render(T value, int rowNum, int colNum);
+}

+ 11 - 0
src/com/wecise/odb/table/ConsoleHeaderRenderer.java

@@ -0,0 +1,11 @@
+package com.wecise.odb.table;
+
+public interface ConsoleHeaderRenderer<T> {
+
+  /**
+   * @param value
+   * @param colNum
+   * @return textual representation of the value
+   */
+  String render(T value, int colNum);
+}

+ 310 - 0
src/com/wecise/odb/table/ConsoleTable.java

@@ -0,0 +1,310 @@
+package com.wecise.odb.table;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import static com.wecise.odb.table.Utils.*;
+
+
+/**
+ * Created by fabmars on 17/09/16.
+ */
+public abstract class ConsoleTable<R> {
+
+  public static final String BORDER_LEFT = "|";
+  public static final String COLUMN_SEPARATOR = BORDER_LEFT;
+  public static final String BORDER_RIGHT = BORDER_LEFT;
+  public static final char BORDER_PADDING = ' ';
+  public static final char BORDER_TOP_BOTTOM = '=';
+  public static final char BORDER_LINE = '-';
+
+  private boolean headers;
+
+  Boolean preProcessed = false;
+  private int[] widthsCache;
+  private String[] headerRenderCache;
+  private List<String[]> cellRenderCache;
+
+
+  public ConsoleTable(boolean headers) {
+    this.headers = headers;
+  }
+
+  public abstract int getColumnCount();
+  public abstract int getRowCount();
+
+  public boolean isHeaders() {
+    return headers;
+  }
+  public abstract Object getHeader(int column);
+
+  public abstract R getRow(int rowNum);
+
+  /**
+   * Tells whether a given row should be rendered.
+   * For instance you might not want to render null rows
+   * @param rowObject
+   * @param rowNum
+   * @return
+   */
+  public boolean isRenderable(R rowObject, int rowNum) {
+    return true;
+  }
+
+  /**
+   * gets a property of an object aimed at one column of the table
+   * @param row a non-null row object
+   * @param colNum the target table column
+   * @return the cell value
+   */
+  public abstract Object getCell(R row, int colNum);
+
+
+  public final int getRenderedWidth(int colNum) {
+    ensurePreProcessed(false); // Not necessary per se, just in case someone calls this method independently of stream()
+    return widthsCache[colNum];
+  }
+
+  public final String getRenderedHeader(int colNum) {
+    ensurePreProcessed(false); // Not necessary per se, just in case someone calls this method independently of stream()
+    String headerString = headerRenderCache[colNum];
+    return pad(headerString, getRenderedWidth(colNum), getAlignment(colNum));
+  }
+
+  private String getRenderedCell(int rowNum, int colNum) {
+    ensurePreProcessed(false); // Not necessary per se, just in case someone calls this method independently of stream()
+    String cellString = cellRenderCache.get(rowNum)[colNum];
+    return pad(cellString, getRenderedWidth(colNum), getAlignment(colNum));
+  }
+
+  protected synchronized final void ensurePreProcessed(boolean force) {
+    if(!preProcessed || force) {
+      int columnCount = getColumnCount();
+      widthsCache = new int[columnCount];
+      headerRenderCache = new String[columnCount];
+
+      if (isHeaders()) {
+        for (int colNum = 0; colNum < columnCount; colNum++) {
+          String renderedHeader = safeRenderHeader(getHeader(colNum), colNum);
+          headerRenderCache[colNum] = renderedHeader;
+          widthsCache[colNum] = renderedHeader.length();
+        }
+      }
+
+      int rowCount = getRowCount();
+      cellRenderCache = new LinkedList<>();
+      for (int rowNum = 0; rowNum < rowCount; rowNum++) {
+        R row = getRow(rowNum);
+        if (isRenderable(row, rowNum)) {
+          String[] rowCache = new String[columnCount];
+          for (int colNum = 0; colNum < columnCount; colNum++) {
+            Object cellValue = row != null ? getCell(row, colNum) : NonExistent.instance;
+            String renderedCell = safeRenderCell(cellValue, rowNum, colNum);
+            rowCache[colNum] = renderedCell;
+
+            int len = renderedCell.length();
+            if (widthsCache[colNum] < len) {
+              widthsCache[colNum] = len;
+            }
+          }
+
+          cellRenderCache.add(rowCache);
+        }
+      }
+      preProcessed = true;
+    }
+  }
+
+
+  public Align getAlignment(int colNum) {
+    return Align.RIGHT;
+  }
+
+  public ConsoleHeaderRenderer getHeaderRenderer(int colNum) {
+    return null;
+  }
+
+  public ConsoleCellRenderer getCellRenderer(int rowNum, int colNum) {
+    return null;
+  }
+
+  public Class<?> getColumnClass(int column) {
+    return Object.class;
+  }
+
+  public ConsoleHeaderRenderer getDefaultHeaderRenderer(Class<?> clazz) {
+    return null;
+  }
+
+  public ConsoleCellRenderer getDefaultCellRenderer(Class<?> clazz) {
+    return null;
+  }
+
+
+  @Override
+  public String toString() {
+    try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+      stream(baos);
+      return baos.toString();
+    }
+    catch(IOException e) {
+      throw new RuntimeException(e); // Not likely to happen
+    }
+  }
+
+  public String toString(String charsetName) {
+    try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+      stream(baos);
+      return baos.toString(charsetName);
+    }
+    catch(IOException e) {
+      throw new RuntimeException(e); // Not likely to happen
+    }
+  }
+
+  public void stream(OutputStream os) {
+    ensurePreProcessed(true); //forcing at each rendering in case the underlying datas have changed
+
+    PrintStream ps = new PrintStream(os);
+    int columnCount = getColumnCount();
+
+    int borderlessWidth = 3 * (columnCount-1);
+    for(int colNum = 0; colNum < columnCount; colNum++) {
+      borderlessWidth += getRenderedWidth(colNum); // calls preProcess
+    }
+
+    // Top line
+    separator(ps, BORDER_TOP_BOTTOM, borderlessWidth);
+
+    // Headers
+    if(isHeaders()) {
+      List<String> paddedHeaderValues = new ArrayList<>(columnCount);
+
+      ps.append(BORDER_LEFT).append(BORDER_PADDING);
+      for (int colNum = 0; colNum < columnCount; colNum++) {
+        String renderedHeader = getRenderedHeader(colNum);
+        paddedHeaderValues.add(renderedHeader);
+      }
+      ps.append(String.join(BORDER_PADDING + COLUMN_SEPARATOR + BORDER_PADDING, paddedHeaderValues));
+      ps.append(BORDER_PADDING).println(BORDER_RIGHT);
+
+      // Separator
+      separator(ps, BORDER_TOP_BOTTOM, borderlessWidth);
+    }
+
+    boolean canDrawLine = false;
+
+    // Table data
+    int rowCount = cellRenderCache.size();
+    for(int rowNum = 0; rowNum < rowCount; rowNum++) {
+      // Separator. It's easier to test like this than after an actual line.
+      if(canDrawLine) {
+        separator(ps, BORDER_LINE, borderlessWidth);
+      }
+      canDrawLine = true;
+
+      List<String> paddedRowValues = new ArrayList<>(columnCount);
+      ps.append(BORDER_LEFT).append(BORDER_PADDING);
+      for (int colNum = 0; colNum < columnCount; colNum++) {
+        String renderedCell = getRenderedCell(rowNum, colNum);
+        paddedRowValues.add(renderedCell);
+      }
+      ps.append(String.join(BORDER_PADDING + COLUMN_SEPARATOR + BORDER_PADDING, paddedRowValues));
+      ps.append(BORDER_PADDING).println(BORDER_RIGHT);
+    }
+
+    // Bottom
+    separator(ps, BORDER_TOP_BOTTOM, borderlessWidth);
+
+    ps.flush();
+  }
+
+
+  protected String pad(String txt, int length, Align align) {
+    char compChar = BORDER_PADDING;
+    switch(align) {
+      case RIGHT:
+        return padLeft(txt, compChar, length);
+
+      case CENTER:
+        return padCenter(txt, compChar, length);
+
+      case LEFT:
+        return padRight(txt, compChar, length);
+
+      default:
+        throw new UnsupportedOperationException(align.name());
+    }
+  }
+
+
+  private void separator(PrintStream ps, char c, int borderlessWidth) {
+    ps.append(BORDER_LEFT).append(c);
+    for(int t = 0; t < borderlessWidth; t++) {
+      ps.append(c);
+    }
+    ps.append(c).println(BORDER_RIGHT);
+  }
+
+
+  /**
+   * Renderes a textual representation of the header cell
+   * @param value
+   * @param colNum
+   * @return textual representation of the header cell. MUST NOT return null
+   */
+  private final String safeRenderHeader(Object value, int colNum) {
+    String result = renderHeader(value, colNum);
+    if(result == null) {
+      result = ToStringConsoleRenderer.instance.render(value, colNum);
+    }
+    return result;
+  }
+
+  protected String renderHeader(Object value, int column) {
+    ConsoleHeaderRenderer headerRenderer = getHeaderRenderer(column);
+    if(headerRenderer == null) {
+      Class<?> headerClass = value != null ? value.getClass() : null;
+      headerRenderer = getDefaultHeaderRenderer(headerClass);
+    }
+
+    String result = null;
+    if(headerRenderer != null) {
+      result = headerRenderer.render(value, column);
+    }
+    return result;
+  }
+
+  private final String safeRenderCell(Object value, int rowNum, int colNum) {
+    String result = renderCell(value, rowNum, colNum);
+    if(result == null) {
+      result = ToStringConsoleRenderer.instance.render(value, rowNum, colNum);
+    }
+    return result;
+  }
+
+  protected String renderCell(Object value, int rowNum, int colNum) {
+    ConsoleCellRenderer cellRenderer;
+    if(NonExistent.isIt(value)) {
+      cellRenderer = getDefaultCellRenderer(NonExistent.class);
+    }
+    else {
+      cellRenderer = getCellRenderer(rowNum, colNum);
+      if(cellRenderer == null) {
+        Class<?> columnClass = getColumnClass(colNum);
+        cellRenderer = getDefaultCellRenderer(columnClass);
+      }
+    }
+
+    String result = null;
+    if(cellRenderer != null) {
+      result = cellRenderer.render(value, rowNum, colNum);
+    }
+    return result;
+  }
+}

+ 50 - 0
src/com/wecise/odb/table/DefaultConsoleRenderer.java

@@ -0,0 +1,50 @@
+package com.wecise.odb.table;
+
+/**
+ * Created by mars on 27/09/2016.
+ */
+public class DefaultConsoleRenderer implements ConsoleHeaderRenderer<Object>, ConsoleCellRenderer<Object> {
+
+  public static final String EMPTY = "";
+  public final static DefaultConsoleRenderer instance = new DefaultConsoleRenderer();
+
+
+  private final String ifNull;        // string to apply in the cell if the value is null
+  private final String ifInexistent;  // string to apply in the cell if the value doesn't exist (eg: out of bounds)
+
+  public DefaultConsoleRenderer() {
+    this(EMPTY, EMPTY);
+  }
+
+  public DefaultConsoleRenderer(String ifNullOrInexistent) {
+    this(ifNullOrInexistent, ifNullOrInexistent);
+  }
+
+  public DefaultConsoleRenderer(String ifNull, String ifInexistent) {
+    this.ifNull = ifNull;
+    this.ifInexistent = ifInexistent;
+  }
+
+  public String getIfNull() {
+    return ifNull;
+  }
+
+  public String getIfInexistent() {
+    return ifInexistent;
+  }
+
+  @Override
+  public String render(Object value, int colNum) {
+    return render(value, -1, colNum);
+  }
+
+  @Override
+  public String render(Object value, int rowNum, int colNum) {
+    if(NonExistent.isIt(value)) {
+      return ifInexistent;
+    }
+    else {
+      return value != null ? value.toString() : ifNull;
+    }
+  }
+}

+ 36 - 0
src/com/wecise/odb/table/GenericConsoleTable.java

@@ -0,0 +1,36 @@
+package com.wecise.odb.table;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public abstract class GenericConsoleTable<R> extends ConsoleTable<R> {
+
+  private final List<R> list;
+
+
+  public GenericConsoleTable(Collection<R> collection, boolean headers) {
+    super(headers);
+    this.list = new ArrayList<>(collection);
+  }
+
+  @Override
+  public final int getRowCount() {
+    return list.size();
+  }
+  @Override
+  public final R getRow(int rowNum) {
+    return list.get(rowNum);
+  }
+
+
+  @Override
+  public ConsoleHeaderRenderer getDefaultHeaderRenderer(Class<?> clazz) {
+    return DefaultConsoleRenderer.instance;
+  }
+
+  @Override
+  public ConsoleCellRenderer getDefaultCellRenderer(Class<?> clazz) {
+    return DefaultConsoleRenderer.instance;
+  }
+}

+ 20 - 0
src/com/wecise/odb/table/NonExistent.java

@@ -0,0 +1,20 @@
+package com.wecise.odb.table;
+
+/**
+ * Symbolizes a value that doesn't exist (eg: out of bounds)
+ * as opposed to a null value
+ */
+public class NonExistent {
+  public final static NonExistent instance = new NonExistent();
+
+  private NonExistent() {/*nothing*/}
+
+  public static boolean isIt(Object value) {
+    return instance.equals(value); // it is null-safe
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}

+ 80 - 0
src/com/wecise/odb/table/ResultSetConsoleTable.java

@@ -0,0 +1,80 @@
+package com.wecise.odb.table;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+
+public class ResultSetConsoleTable extends ConsoleTable<ResultSet> {
+
+  private final ResultSet rs;
+  private int initialResultSetRow; // initial 0-based row
+  private final int rowCount, columnCount;
+  private final String[] headers;
+
+  public ResultSetConsoleTable(ResultSet rs) throws SQLException {
+    super(true);
+    this.rs = rs;
+    this.initialResultSetRow = rs.getRow(); //1-based
+
+    ResultSetMetaData rsMetaData = rs.getMetaData();
+    this.columnCount = rsMetaData.getColumnCount();
+    this.rowCount = rs.last() ? rs.getRow() : 0;
+    rs.beforeFirst(); // In all likelihood we'll scroll the table top to bottom.
+
+    this.headers = new String[columnCount];
+    for(int colNum = 0; colNum < columnCount; colNum++) {
+      this.headers[colNum] = rsMetaData.getColumnLabel(colNum+1);
+    }
+  }
+
+
+  @Override
+  public int getColumnCount() {
+    return columnCount;
+  }
+
+  @Override
+  public int getRowCount() {
+    return rowCount;
+  }
+
+  @Override
+  public String getHeader(int colNum) {
+    return headers[colNum];
+  }
+
+  /**
+   * @param rowNum 0-based row number (whereas ResultSet is 1-based)
+   * @return the ResultSet positioned on the requested row
+   */
+  @Override
+  public ResultSet getRow(int rowNum) {
+    try {
+      int current = rs.getRow() - 1; //to 0-based, will be -1 if we're positioned beforeFirst
+      for(; rowNum < current; current--) {
+        rs.previous();
+      }
+      for(; rowNum > current; current++) {
+        rs.next();
+      }
+    }
+    catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+    return rs;
+  }
+
+  @Override
+  public Object getCell(ResultSet rs, int colNum) {
+    try {
+      return rs.getObject(colNum+1);
+    }
+    catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public ResultSet restoreInitialRow() {
+    return getRow(initialResultSetRow -1); // -1: to 0-based
+  }
+}

+ 179 - 0
src/com/wecise/odb/table/SimpleConsoleTable.java

@@ -0,0 +1,179 @@
+package com.wecise.odb.table;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Created by fabmars on 17/09/16.
+ *
+ * Introspected ConsoleTable
+ * @param <R>
+ */
+public class SimpleConsoleTable<R> extends GenericConsoleTable<R> {
+
+  private final int columns;
+  private final List<String> headers;
+  private final List<Method> getters;
+  private final DefaultConsoleRenderer cellRenderer;
+
+  public SimpleConsoleTable(Collection<R> collection) {
+    this(collection, true);
+  }
+
+  public SimpleConsoleTable(Collection<R> collection, boolean headers) {
+    this(collection, headers, DefaultConsoleRenderer.EMPTY);
+  }
+
+  public SimpleConsoleTable(Collection<R> collection, String ifNullOrInexistent) {
+    this(collection, true, ifNullOrInexistent);
+  }
+
+  public SimpleConsoleTable(Collection<R> collection, boolean headers, String ifNullOrInexistent) {
+    super(collection, headers);
+    this.cellRenderer = new DefaultConsoleRenderer(ifNullOrInexistent);
+
+    boolean isArrayElements = collection.isEmpty() || collection.stream().filter(Objects::nonNull).anyMatch(o -> o.getClass().isArray());
+    if (isArrayElements) {
+      this.getters = Collections.emptyList();
+      this.headers = Collections.emptyList();
+      this.columns = collection.stream().filter(Objects::nonNull).mapToInt(o -> ((Object[]) o).length).max().orElse(0);
+    }
+    else {
+      Optional<R> opt = collection.stream().filter(Objects::nonNull).findAny();
+      if (opt.isPresent()) {
+        try {
+          Map<String, Method> propertyMap = inspect(opt.get().getClass());
+          this.getters = new ArrayList<>(propertyMap.values());
+          this.headers = propertyMap.keySet().stream().map(h -> Utils.toUpperCaseFirstChar(h)).collect(Collectors.toList());
+          this.columns = this.headers.size();
+        }
+        catch (IntrospectionException | NoSuchMethodException e) {
+          throw new RuntimeException(e);
+        }
+      } else { // no non-null object in the collection
+        this.getters = Collections.emptyList();
+        this.headers = Collections.emptyList();
+        this.columns = 0;
+      }
+    }
+  }
+
+  public SimpleConsoleTable(R... array) {
+    this(Arrays.asList(array));
+  }
+
+  public SimpleConsoleTable(R[] array, boolean headers) {
+    this(Arrays.asList(array), headers);
+  }
+
+  public SimpleConsoleTable(R[] array, String ifNullOrInexistent) {
+    this(Arrays.asList(array), ifNullOrInexistent);
+  }
+
+  public SimpleConsoleTable(R[] array, boolean headers, String ifNullOrInexistent) {
+    this(Arrays.asList(array), headers, ifNullOrInexistent);
+  }
+
+  public SimpleConsoleTable(Map map) {
+    this(map.entrySet());
+  }
+
+  public SimpleConsoleTable(Map map, boolean headers) {
+    this(map.entrySet(), headers);
+  }
+
+  public SimpleConsoleTable(Map map, String ifNullOrInexistent) {
+    this(map.entrySet(), ifNullOrInexistent);
+  }
+
+  public SimpleConsoleTable(Map map, boolean headers, String ifNullOrInexistent) {
+    this(map.entrySet(), headers, ifNullOrInexistent);
+  }
+
+  public SimpleConsoleTable(Stream<R> stream) {
+    this(stream.collect(Collectors.toSet()));
+  }
+
+  public SimpleConsoleTable(Stream<R> stream, boolean headers) {
+    this(stream.collect(Collectors.toSet()), headers);
+  }
+
+  public SimpleConsoleTable(Stream<R> stream, String ifNullOrInexistent) {
+    this(stream.collect(Collectors.toSet()), ifNullOrInexistent);
+  }
+
+  public SimpleConsoleTable(Stream<R> stream, boolean headers, String ifNullOrInexistent) {
+    this(stream.collect(Collectors.toSet()), headers, ifNullOrInexistent);
+  }
+
+
+  protected static Map<String, Method> inspect(Class<?> clazz) throws IntrospectionException, NoSuchMethodException {
+    Map<String, Method> map = new LinkedHashMap<>(); //Linked to keep the order
+
+    if(clazz.isArray()) {
+      // Can't call any method on that; useless test in this class' case (see ctor)
+    }
+    else if(String.class.getPackage().equals(clazz.getPackage())) {
+      map.put(clazz.getSimpleName(), clazz.getMethod("toString")); // so only #toString is called on java.lang classes (like String, Integer...)
+    }
+    else {
+      BeanInfo beanInfo = Introspector.getBeanInfo(clazz, Object.class);
+      PropertyDescriptor pds[] = beanInfo.getPropertyDescriptors();
+      for (PropertyDescriptor pd : pds) {
+        Method meth = pd.getReadMethod();
+        if (meth != null) { //there is a getter
+          String name = pd.getName();
+          map.put(name, meth);
+        }
+      }
+    }
+    return map;
+  }
+
+  @Override
+  public int getColumnCount() {
+    return columns;
+  }
+
+  @Override
+  public boolean isHeaders() {
+    return super.isHeaders() && !headers.isEmpty();
+  }
+
+  @Override
+  public String getHeader(int colNum) {
+    return headers.get(colNum);
+  }
+
+  @Override
+  public Object getCell(R row, int colNum) {
+    if(row.getClass().isArray()) {
+      Object[] array = (Object[])row;
+      // Testing length because different rows (which are arrays) may have different widths
+      return colNum < array.length ? array[colNum] : NonExistent.instance;
+    }
+    else try {
+      return getters.get(colNum).invoke(row);
+    }
+    catch (IllegalAccessException | InvocationTargetException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public ConsoleHeaderRenderer getDefaultHeaderRenderer(Class<?> clazz) {
+    return cellRenderer;
+  }
+
+  @Override
+  public ConsoleCellRenderer getDefaultCellRenderer(Class<?> clazz) {
+    return cellRenderer;
+  }
+}

+ 19 - 0
src/com/wecise/odb/table/ToStringConsoleRenderer.java

@@ -0,0 +1,19 @@
+package com.wecise.odb.table;
+
+public class ToStringConsoleRenderer implements ConsoleHeaderRenderer<Object>, ConsoleCellRenderer<Object> {
+  public static ToStringConsoleRenderer instance = new ToStringConsoleRenderer();
+
+  private ToStringConsoleRenderer() {
+    // nothing
+  }
+
+  @Override
+  public String render(Object value, int colNum) {
+    return render(value, -1, colNum);
+  }
+
+  @Override
+  public String render(Object value, int rowNum, int colNum) {
+    return String.valueOf(value);
+  }
+}

+ 64 - 0
src/com/wecise/odb/table/Utils.java

@@ -0,0 +1,64 @@
+package com.wecise.odb.table;
+
+/**
+ * Created by fabmars on 17/09/2016.
+ */
+public class Utils {
+
+  public static String toStringOrNull(Object o) {
+    return o != null ? o.toString() : null;
+  }
+
+  public static int safeLength(String s) {
+    return s != null ? s.length() : 0;
+  }
+
+  public static String padLeft(String txt, char compchar, int length) {
+    String out = null;
+    if (txt != null) {
+      out = txt;
+      while (out.length() < length) {
+        out = compchar + out;
+      }
+    }
+    return out;
+  }
+
+  public static String padRight(String txt, char compchar, int length) {
+    String out = null;
+    if (txt != null) {
+      out = txt;
+      while (out.length() < length) {
+        out = out + compchar;
+      }
+    }
+    return out;
+  }
+
+  public static String padCenter(String txt, char compchar, int length) {
+    String out = null;
+    if (txt != null) {
+      out = txt;
+      boolean flip = true;
+      while (out.length() < length) {
+        if (flip) {
+          out = out + compchar;
+        }
+        else {
+          out = compchar + out;
+        }
+        flip = !flip;
+      }
+    }
+    return out;
+  }
+
+  public static String toUpperCaseFirstChar(String s) {
+    if(s != null && s.length() > 0) {
+      return Character.toUpperCase(s.charAt(0)) + s.substring(1);
+    }
+    else {
+      return s;
+    }
+  }
+}

+ 225 - 0
src/com/wecise/odb/test/Test.java

@@ -0,0 +1,225 @@
+package com.wecise.odb.test;
+
+import com.wecise.odb.table.SimpleConsoleTable;
+
+import java.sql.*;
+import java.util.ArrayList;
+
+public class Test {
+    public static void main(String[] args) throws Exception {
+        test(false);
+        System.out.println("JDBC Test");
+
+        Class.forName("com.wecise.odb.jdbc.ODBDriver");
+//        Connection conn = DriverManager.getConnection("jdbc:wecise:omdb://192.168.31.221,127.0.0.1:9062/matrix");
+        Connection conn = DriverManager.getConnection("jdbc:wecise:omdb://47.92.151.165:9062/matrix");
+
+        System.out.println("Meta info:");
+        printMetaData(conn.getMetaData());
+
+        System.out.println("Wait...");
+        Thread.sleep(2000);
+        System.out.println("Continue....");
+
+        Statement stmt = conn.createStatement();
+        ResultSet rs = stmt.executeQuery("select id, name, host as h, vtime, day, severity, tags from /matrix/devops/alert limit 100");
+        printResult(rs);
+        rs.close();
+        stmt.close();
+
+        Statement stmt2 = conn.createStatement();
+        ResultSet rs2 = stmt2.executeQuery("select id, name, host as h, vtime, day, severity, tags from /matrix/devops/alert limit 2");
+        printResult(rs2);
+        rs2.close();
+        stmt2.close();
+
+        // Need odb server support
+        PreparedStatement pstmt = conn.prepareStatement("select id, host from /matrix/devops/alert where host = ? limit 1");
+        pstmt.setString(1, "wecise");
+        ResultSet rs3 = pstmt.executeQuery();
+        printResult(rs3);
+        rs3.close();
+        pstmt.close();
+
+        Statement stmt4 = conn.createStatement();
+        ResultSet rs4 = stmt4.executeQuery("match ('biz:查账系统')-[*]->() until 'router:router1','router:router2'");
+        printResult(rs4);
+        rs4.close();
+        stmt4.close();
+
+        conn.close();
+    }
+
+    private static void printMetaData(DatabaseMetaData meta) throws Exception {
+        // https://blog.csdn.net/russle/article/details/53182848
+        // https://aisia.moe/java6api-cn/java/sql/DatabaseMetaData.html
+        ResultSet schemas = meta.getSchemas();
+        String schem = null;
+        String catalog = null;
+        while (schemas.next()) {
+            schem = schemas.getString(1);
+            catalog = schemas.getString(2);
+            System.out.println("    TABLE_SCHEM: " + schemas.getString(1));
+            System.out.println("    TABLE_CATALOG: " + schemas.getString(2));
+        }
+
+        String[] type = new String[2];
+        type[0] = "TABLE";
+        type[1] = "VIEW";
+        // _ = all table, ST_ = prefix ST table
+        // % = all table, ST% = prefix ST table
+        System.out.println("    Tables:");
+        ResultSet tables = meta.getTables(catalog, schem, "%", type);
+        String tname = null;
+        while (tables.next()) {
+            //TABLE_CAT
+            String tableCatalog = tables.getString(1);
+            //TABLE_SCHEM
+            String tableSchema = tables.getString(2);
+            //TABLE_NAME
+            String tableName = tables.getString(3);
+            //TABLE_TYPE
+            String tableType = tables.getString(4);
+            //REMARKS
+            String remarks = tables.getString(5);
+            //TYPE_CAT
+            String typeCatalog = tables.getString(6);
+
+            System.out.println("        tableCatalog:" + tableCatalog + ", tableSchema:" + tableSchema +", tableName:" + tableName
+                    + ", tableType:" + tableType
+                    + ", typeCatalog:" + typeCatalog
+                    + ", remarks:" + remarks);
+
+            if ("/matrix/devops/alert".equals(tableName)) {
+                tname = tableName;
+            }
+        }
+
+        System.out.println("    Table " + tname + " columns:");
+        ResultSet columns = meta.getColumns(catalog, schem, tname, "%");
+        while (columns.next()) {
+            String columnName = columns.getString(4);
+            int dataType = columns.getInt(5);
+            String typeName = columns.getString(6);
+            int columnSize = columns.getInt(7);
+            System.out.println("        columnName:" + columnName + ", typeName:" + typeName +", columnSize:" + columnSize + ", dataType:" + dataType);
+        }
+    }
+
+    private static void printResult(ResultSet rs) throws Exception {
+        ResultSetMetaData meta = rs.getMetaData();
+        ArrayList<Object> data = new ArrayList<>();
+        Object[] header = new Object[meta.getColumnCount() + 1];
+        header[0] = "No";
+        for (int i = 1; i < header.length; i ++) {
+            header[i] = meta.getColumnLabel(i);
+        }
+        data.add(header);
+        while (rs.next()) {
+            Object[] row = new Object[header.length];
+            row[0] = rs.getRow();
+            for (int i = 1; i <= meta.getColumnCount(); i++) {
+                Object val;
+                switch (meta.getColumnType(i)) {
+                    case Types.TIMESTAMP:
+                        val = rs.getTimestamp(i).getTime();
+                        break;
+                    case Types.DATE:
+                        val = rs.getDate(i);
+                        break;
+                    case Types.JAVA_OBJECT:
+                        val = rs.getObject(i);
+                        break;
+                    default: // default varchar
+                        val = rs.getString(i);
+                        break;
+                }
+                row[i] = val;
+            }
+            data.add(row);
+        }
+
+        SimpleConsoleTable<Object> consoleTable = new SimpleConsoleTable<>(data.toArray());
+        System.out.println(consoleTable.toString());
+    }
+
+    private static void test(boolean exit) {
+//        String s = "select * from test where age = ? and id = '\\'a?' and name = ? limit 1";
+//        parseSql(s);
+
+//        String[] a = new String[]{"a", "b", "c"};
+//        ObjectPool pool = new ObjectPool(a);
+//
+//        Object v = pool.get();
+//        System.out.println(v);
+//        Thread.sleep(200);
+//
+//        Object v1 = pool.get();
+//        System.out.println(v1);
+//        Thread.sleep(200);
+//
+//        pool.release(v);
+//        pool.release(v1);
+//
+//        Object v2 = pool.get();
+//        System.out.println(v2);
+//        Thread.sleep(500);
+//        pool.release(v2);
+
+        // 原数组, 原数组的开始位置, 目标数组, 目标数组的开始位置, 拷贝个数
+//        byte[] b1 = new byte[3];
+//        b1[0] = 'a';
+//        b1[1] = 'b';
+//        b1[2] = 'c';
+//        byte[] b2 = new byte[3];
+//        b2[0] = 'x';
+//        b2[1] = 'y';
+//        b2[2] = 'z';
+//        byte[] b3 = Util.bytesMerger(b1, b2);
+//        for (byte bt: b3) {
+//            System.out.print(String.format("%c", bt));
+//        }
+//        System.out.println();
+//
+//        System.out.println(String.format("%c %c %c", b1[0], b1[1], b1[2]));
+//        System.out.println(String.format("%c %c %c", b2[0], b2[1], b2[2]));
+//        System.arraycopy(b2, 0, b1, 0, 2);
+//        System.out.println(String.format("%c %c %c", b1[0], b1[1], b1[2]));
+//
+//        HashMap<String, String> meta = new HashMap<>();
+//        meta.put("X-Accept-Body-Codec", "106");
+//        HashMap<String, String> body = new HashMap<>();
+//        body.put("keyspace", "matrix");
+//        Message m = new Message("", 1, "/data/mql/execute", meta, 'j', new Gson().toJson(body));
+//        System.out.println(new String(m.encode()));
+
+        if (exit) {
+            System.exit(0);
+        }
+    }
+
+//    private static void parseSql(String sql) {
+//        int num = 0;
+//        char[] a = sql.toCharArray();
+//        boolean ok = true;
+//        boolean escape = false;
+//        for (char c: a) {
+//            if (!escape) {
+//                if (c == '\\') {
+//                    escape = true;
+//                    continue;
+//                }
+//            } else {
+//                escape = false;
+//                continue;
+//            }
+//            if (c == '\'' || c == '"') {
+//                ok = !ok;
+//            }
+//            if (ok && c == '?') {
+//                num += 1;
+//            }
+//        }
+//        System.out.println(num);
+//    }
+}