--- /dev/null
+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("|");
+ }
+ }
+}