dumps the content of blobs, and reworks table formatting to add spaces around lines
[squelch.git] / src / main / java / net / jaekl / squelch / stmt / Tabular.java
index 03c403ef3f037684977376d14e3f3dd3d8b8472b..582b26ad1df1a6eee18dd4a2bcc5bf1ff449470e 100644 (file)
@@ -1,9 +1,16 @@
 package net.jaekl.squelch.stmt;
 
 import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
 import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
 import java.sql.Types;
 
+import javax.xml.bind.DatatypeConverter;
+
 import net.jaekl.squelch.sql.Column;
 import net.jaekl.squelch.sql.Row;
 
@@ -13,6 +20,8 @@ import net.jaekl.squelch.sql.Row;
 // Includes routines to output the data as a human-friendly table, or as CSV.
 
 abstract public class Tabular {
+       private static final long BLOB_MAX = 65536;
+       
        private static class RowBuffer {
                private final int ROW_BUF_SIZE = 50;
                
@@ -38,10 +47,12 @@ abstract public class Tabular {
        }
        
        abstract Column[] getCols() throws SQLException;
-       abstract Row getNext();
+       abstract Row getNext() throws SQLException;
        
        // Returns the number of (data) rows that were output
-       public int printTable(PrintWriter pw) throws SQLException {
+       public int printTable(PrintWriter pw, String noRowsMessage) 
+               throws SQLException 
+       {
                int rowCount = 0;
                Column[] cols = getCols();
                RowBuffer rowBuf;
@@ -66,8 +77,15 @@ abstract public class Tabular {
                        rowCount += pending;
                }
                
-               // TODO:  Implement a String table for i18n
-               pw.println("" + rowCount + " row(s) returned.");
+               if (rowCount > 0) {
+                       writeDivider(pw, colWidths);
+                       // TODO:  Implement a String table for i18n
+                       pw.println("" + rowCount + " row(s) returned.");
+               }
+               else {
+                       pw.println(noRowsMessage);
+               }
+               
                pw.flush();
                
                return rowCount;
@@ -119,7 +137,7 @@ abstract public class Tabular {
        
        // Examine and buffer up to rowBuf.length rows.
        // Returns the number of actual rows that were buffered (zero if no more rows are available).
-       RowBuffer bufferRows(int[] colWidths)
+       RowBuffer bufferRows(int[] colWidths) throws SQLException
        {
                RowBuffer rowBuf = new RowBuffer();
                
@@ -202,7 +220,7 @@ abstract public class Tabular {
                }
                return Object.class;
        }
-
+       
        String repChar(char chr, int times)
        {
                StringBuffer sb = new StringBuffer();
@@ -212,9 +230,62 @@ abstract public class Tabular {
                return sb.toString();
        }
        
+       String stringify(Object obj) throws SQLException
+       {
+               if (obj instanceof java.sql.Blob) {
+                       java.sql.Blob blob = null;
+                       
+                       try {
+                               blob = (java.sql.Blob)obj;
+                               long length = blob.length();
+                               if (length > BLOB_MAX) {
+                                       length = BLOB_MAX;
+                               }
+                               byte[] content = blob.getBytes(1, (int)length);
+                               
+                               try {
+                                       // Assume that the BLOB is actually UTF-8 text, and attempt to return it that way.
+                                       // This happens to work well for the databases that I deal with regularly.
+                                       // Other users may want to change this assumption here...
+                                       return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)
+                                                                                                                         .onUnmappableCharacter(CodingErrorAction.REPORT)
+                                                                                                                         .decode(ByteBuffer.wrap(content))
+                                                                                                                         .toString();
+                               }
+                               catch (CharacterCodingException exc) {
+                                       // If we get here, then the BLOB's content is not valid UTF-8.
+                                       // This may be because it's in a different character set, or because it's non-text
+                                       // (e.g., a bitmap image).
+                                       // We'll hex-dump it instead.
+                                       return DatatypeConverter.printHexBinary(content);
+                               }
+                       }
+                       finally {
+                               if (null != blob) {
+                                       try {
+                                               blob.free();
+                                       }
+                                       catch (UnsupportedOperationException | SQLFeatureNotSupportedException exc) {
+                                               // free() was only a hint; if the hint is not welcome, then that's OK.
+                                       }
+                               }
+                       }
+               }
+               
+               return "" + obj;
+       }
+       
+       int stringWidth(Object obj) 
+       {
+               if (obj instanceof java.sql.Blob) {
+                       return 1;
+               }
+               return ("" + obj).length();
+       }
+       
        void writeDivider(PrintWriter pw, int[] colWidths) {
                for (int idx = 0; idx < colWidths.length; ++idx) {
-                       pw.print("+" + repChar('-', colWidths[idx]));
+                       pw.print("+" + repChar('-', colWidths[idx] + 2));
                }
                pw.println("+");
        }
@@ -222,23 +293,25 @@ abstract public class Tabular {
        void writeHeader(PrintWriter pw, Column[] cols, int[] colWidths) {
                writeDivider(pw, colWidths);
 
-               for (int idx = 1; idx <= cols.length; ++idx) {
+               for (int idx = 0; idx < cols.length; ++idx) {
                        Column col = cols[idx];
-                       pw.print("|" + centrePad(col.getLabel(), colWidths[idx]));
+                       pw.print("| " + centrePad(col.getLabel(), colWidths[idx]) + " ");
                }
                pw.println("|");
                
                writeDivider(pw, colWidths);
        }
        
-       void writeRowBuffer(PrintWriter pw, RowBuffer rowBuf, int[] colWidths) {
+       void writeRowBuffer(PrintWriter pw, RowBuffer rowBuf, int[] colWidths) throws SQLException {
                
                for (int rowIdx = 0; rowIdx < rowBuf.getPending(); ++rowIdx) {
                        Row row = rowBuf.getRow(rowIdx);
                        for (int colIdx = 0; colIdx < colWidths.length; ++colIdx) {
-                               String value = "" + row.getValue(colIdx);
-                               String padding = repChar(' ', colWidths[colIdx] - value.length());
-                               pw.print("|" + value + padding);
+                               Object obj = row.getValue(colIdx + 1);
+                               String value = stringify(obj);
+                               int width = stringWidth(obj);
+                               String padding = repChar(' ', colWidths[colIdx] - width);
+                               pw.print("| " + value + padding + " ");
                        }
                        pw.println("|");
                }