Refactor tabular output for eventual re-use printing table metadata.
[squelch.git] / src / main / java / net / jaekl / squelch / stmt / Tabular.java
diff --git a/src/main/java/net/jaekl/squelch/stmt/Tabular.java b/src/main/java/net/jaekl/squelch/stmt/Tabular.java
new file mode 100644 (file)
index 0000000..03c403e
--- /dev/null
@@ -0,0 +1,246 @@
+package net.jaekl.squelch.stmt;
+
+import java.io.PrintWriter;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import net.jaekl.squelch.sql.Column;
+import net.jaekl.squelch.sql.Row;
+
+// Copyright (C) 2016 by Chris Jaekl
+//
+// Tabular:  wrapper around a set of tabular data (such as a ResultSet or table's MetaData).
+// Includes routines to output the data as a human-friendly table, or as CSV.
+
+abstract public class Tabular {
+       private static class RowBuffer {
+               private final int ROW_BUF_SIZE = 50;
+               
+               private Row[] m_rowBuf;
+               private int m_pending;
+               
+               public RowBuffer() {
+                       m_rowBuf = new Row[ROW_BUF_SIZE];
+                       m_pending = 0;
+               }
+
+               public void addRow(Row row) {
+                       assert (m_pending < m_rowBuf.length);
+                       m_rowBuf[m_pending] = row;
+                       m_pending++;
+               }
+               public int getPending() { return m_pending; }
+               public Row getRow(int idx) {
+                       assert (idx >= 0 && idx < m_pending);
+                       return m_rowBuf[idx];
+               }
+               public boolean isFull() { return m_pending >= m_rowBuf.length; }
+       }
+       
+       abstract Column[] getCols() throws SQLException;
+       abstract Row getNext();
+       
+       // Returns the number of (data) rows that were output
+       public int printTable(PrintWriter pw) throws SQLException {
+               int rowCount = 0;
+               Column[] cols = getCols();
+               RowBuffer rowBuf;
+
+               // Initialize colWidths to the widths of the column names
+               int[] colWidths = initColWidthsFromColNames(cols);
+               
+               // Examine and buffer up to ROW_BUF_SIZE rows, adjusting (widening) colWidths where needed
+               rowBuf = bufferRows(colWidths);
+               
+               int pending = rowBuf.getPending();
+               if (pending > 0) {
+                       writeHeader(pw, cols, colWidths);
+                       writeRowBuffer(pw, rowBuf, colWidths);
+                       rowCount = rowBuf.getPending();
+               }
+               
+               while (pending > 0) {
+                       rowBuf = bufferRows(colWidths);
+                       writeRowBuffer(pw, rowBuf, colWidths);
+                       pending = rowBuf.getPending();
+                       rowCount += pending;
+               }
+               
+               // TODO:  Implement a String table for i18n
+               pw.println("" + rowCount + " row(s) returned.");
+               pw.flush();
+               
+               return rowCount;
+       }
+       
+       // Returns the number of (data) rows that were output
+       public int printCsv(PrintWriter pw) throws SQLException {
+               int rowCount = 0;
+               Column[] cols = getCols();
+               Row row = null;
+               
+               StringBuilder sb = new StringBuilder();
+               boolean firstCol = true;
+               for (int idx = 1; idx <= cols.length; ++idx) {
+                       if (firstCol) {
+                               firstCol = false;
+                       }
+                       else {
+                               sb.append(",");
+                       }
+                       sb.append("" + cols[idx - 1].getLabel());
+               }
+               sb.append("\n");
+               String header = sb.toString();
+               
+               while (null != (row = getNext())) {
+                       if (null != header) {
+                               pw.print(header);
+                               header = null;
+                       }
+                       
+                       firstCol = true;
+                       for (int idx = 1; idx <= cols.length; ++idx) {
+                               if (firstCol) {
+                                       firstCol = false;
+                               }
+                               else {
+                                       pw.print(",");
+                               }
+                               pw.print("" + row.getValue(idx));
+                       }
+                       pw.println("");
+                       rowCount++;
+               }
+               pw.flush();
+               
+               return rowCount;                
+       }
+       
+       // 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 rowBuf = new RowBuffer();
+               
+               while (!rowBuf.isFull()) {
+                       Row row = getNext();
+                       if (null == row) {
+                               // No more rows available
+                               return rowBuf;
+                       }
+                       rowBuf.addRow(row);
+
+                       // Check whether all values in this row will fit in the current column widths
+                       for (int colIdx = 0; colIdx < colWidths.length; ++colIdx) {
+                               int width = ("" + row.getValue(colIdx + 1)).length();
+                               if (width > colWidths[colIdx]) {
+                                       // Widen the column to fit this value
+                                       colWidths[colIdx] = width;
+                               }
+                       }
+               }
+               
+               return rowBuf;
+       }
+       
+       String centrePad(String value, int desiredWidth)
+       {
+               if (null == value) {
+                       return centrePad("NULL", desiredWidth);
+               }
+               if (value.length() >= desiredWidth) {
+                       return value;
+               }
+               int left = (desiredWidth - value.length()) / 2;
+               int right = desiredWidth - value.length() - left;
+               return repChar(' ', left) + value + repChar(' ', right);
+       }
+       
+       int[] initColWidthsFromColNames(Column[] cols) 
+       {
+               int[] widths = new int[cols.length];
+               
+               for (int idx = 0; idx < cols.length; ++idx) {
+                       widths[idx] = cols[idx].getLabel().length();
+               }
+               
+               return widths;
+       }
+       
+       Class<?> classForSqlType(int sqlType)
+       {
+               switch(sqlType)
+               {
+               case Types.BOOLEAN:
+                       return Boolean.class;
+               case Types.CHAR:
+                       return Character.class;
+               case Types.DATE:
+               case Types.TIME:
+               case Types.TIMESTAMP:
+                       return java.util.Date.class;
+               case Types.DECIMAL:
+               case Types.DOUBLE:
+               case Types.FLOAT:
+               case Types.NUMERIC:
+               case Types.REAL:
+                       return Double.class;
+               case Types.INTEGER:
+                       return Long.class;
+               case Types.ROWID:
+               case Types.SMALLINT:
+                       return Integer.class;
+               case Types.TINYINT:
+                       return Short.class;
+               case Types.LONGNVARCHAR:
+               case Types.LONGVARCHAR:
+               case Types.NCHAR:
+               case Types.NVARCHAR:
+               case Types.VARCHAR:
+                       return String.class;
+               }
+               return Object.class;
+       }
+
+       String repChar(char chr, int times)
+       {
+               StringBuffer sb = new StringBuffer();
+               for (int idx = 0; idx < times; ++idx) {
+                       sb.append(chr);
+               }
+               return sb.toString();
+       }
+       
+       void writeDivider(PrintWriter pw, int[] colWidths) {
+               for (int idx = 0; idx < colWidths.length; ++idx) {
+                       pw.print("+" + repChar('-', colWidths[idx]));
+               }
+               pw.println("+");
+       }
+       
+       void writeHeader(PrintWriter pw, Column[] cols, int[] colWidths) {
+               writeDivider(pw, colWidths);
+
+               for (int idx = 1; idx <= cols.length; ++idx) {
+                       Column col = cols[idx];
+                       pw.print("|" + centrePad(col.getLabel(), colWidths[idx]));
+               }
+               pw.println("|");
+               
+               writeDivider(pw, colWidths);
+       }
+       
+       void writeRowBuffer(PrintWriter pw, RowBuffer rowBuf, int[] colWidths) {
+               
+               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);
+                       }
+                       pw.println("|");
+               }
+       }
+}