1 package net.jaekl.squelch.stmt;
3 import java.io.PrintWriter;
4 import java.nio.ByteBuffer;
5 import java.nio.charset.CharacterCodingException;
6 import java.nio.charset.CodingErrorAction;
7 import java.nio.charset.StandardCharsets;
8 import java.sql.SQLException;
9 import java.sql.SQLFeatureNotSupportedException;
10 import java.sql.Types;
12 import javax.xml.bind.DatatypeConverter;
14 import net.jaekl.squelch.sql.Column;
15 import net.jaekl.squelch.sql.Row;
17 // Copyright (C) 2016 by Chris Jaekl
19 // Tabular: wrapper around a set of tabular data (such as a ResultSet or table's MetaData).
20 // Includes routines to output the data as a human-friendly table, or as CSV.
22 abstract public class Tabular {
23 private static final long BLOB_MAX = 65536;
25 private static class RowBuffer {
26 private final int ROW_BUF_SIZE = 50;
28 private Row[] m_rowBuf;
29 private int m_pending;
32 m_rowBuf = new Row[ROW_BUF_SIZE];
36 public void addRow(Row row) {
37 assert (m_pending < m_rowBuf.length);
38 m_rowBuf[m_pending] = row;
41 public int getPending() { return m_pending; }
42 public Row getRow(int idx) {
43 assert (idx >= 0 && idx < m_pending);
46 public boolean isFull() { return m_pending >= m_rowBuf.length; }
49 abstract Column[] getCols() throws SQLException;
50 abstract Row getNext() throws SQLException;
52 // Returns the number of (data) rows that were output
53 public int printTable(PrintWriter pw, String noRowsMessage)
57 Column[] cols = getCols();
60 // Initialize colWidths to the widths of the column names
61 int[] colWidths = initColWidthsFromColNames(cols);
63 // Examine and buffer up to ROW_BUF_SIZE rows, adjusting (widening) colWidths where needed
64 rowBuf = bufferRows(colWidths);
66 int pending = rowBuf.getPending();
68 writeHeader(pw, cols, colWidths);
69 writeRowBuffer(pw, rowBuf, colWidths);
70 rowCount = rowBuf.getPending();
74 rowBuf = bufferRows(colWidths);
75 writeRowBuffer(pw, rowBuf, colWidths);
76 pending = rowBuf.getPending();
81 writeDivider(pw, colWidths);
82 // TODO: Implement a String table for i18n
83 pw.println("" + rowCount + " row(s) returned.");
86 pw.println(noRowsMessage);
94 // Returns the number of (data) rows that were output
95 public int printCsv(PrintWriter pw) throws SQLException {
97 Column[] cols = getCols();
100 StringBuilder sb = new StringBuilder();
101 boolean firstCol = true;
102 for (int idx = 1; idx <= cols.length; ++idx) {
109 sb.append("" + cols[idx - 1].getLabel());
112 String header = sb.toString();
114 while (null != (row = getNext())) {
115 if (null != header) {
121 for (int idx = 1; idx <= cols.length; ++idx) {
128 pw.print("" + row.getValue(idx));
138 // Examine and buffer up to rowBuf.length rows.
139 // Returns the number of actual rows that were buffered (zero if no more rows are available).
140 RowBuffer bufferRows(int[] colWidths) throws SQLException
142 RowBuffer rowBuf = new RowBuffer();
144 while (!rowBuf.isFull()) {
147 // No more rows available
152 // Check whether all values in this row will fit in the current column widths
153 for (int colIdx = 0; colIdx < colWidths.length; ++colIdx) {
154 int width = ("" + row.getValue(colIdx + 1)).length();
155 if (width > colWidths[colIdx]) {
156 // Widen the column to fit this value
157 colWidths[colIdx] = width;
165 String centrePad(String value, int desiredWidth)
168 return centrePad("NULL", desiredWidth);
170 if (value.length() >= desiredWidth) {
173 int left = (desiredWidth - value.length()) / 2;
174 int right = desiredWidth - value.length() - left;
175 return repChar(' ', left) + value + repChar(' ', right);
178 int[] initColWidthsFromColNames(Column[] cols)
180 int[] widths = new int[cols.length];
182 for (int idx = 0; idx < cols.length; ++idx) {
183 widths[idx] = cols[idx].getLabel().length();
189 Class<?> classForSqlType(int sqlType)
194 return Boolean.class;
196 return Character.class;
199 case Types.TIMESTAMP:
200 return java.util.Date.class;
211 return Integer.class;
214 case Types.LONGNVARCHAR:
215 case Types.LONGVARCHAR:
224 String repChar(char chr, int times)
226 StringBuffer sb = new StringBuffer();
227 for (int idx = 0; idx < times; ++idx) {
230 return sb.toString();
233 String stringify(Object obj) throws SQLException
235 if (obj instanceof java.sql.Blob) {
236 java.sql.Blob blob = null;
239 blob = (java.sql.Blob)obj;
240 long length = blob.length();
241 if (length > BLOB_MAX) {
244 byte[] content = blob.getBytes(1, (int)length);
247 // Assume that the BLOB is actually UTF-8 text, and attempt to return it that way.
248 // This happens to work well for the databases that I deal with regularly.
249 // Other users may want to change this assumption here...
250 return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)
251 .onUnmappableCharacter(CodingErrorAction.REPORT)
252 .decode(ByteBuffer.wrap(content))
255 catch (CharacterCodingException exc) {
256 // If we get here, then the BLOB's content is not valid UTF-8.
257 // This may be because it's in a different character set, or because it's non-text
258 // (e.g., a bitmap image).
259 // We'll hex-dump it instead.
260 return DatatypeConverter.printHexBinary(content);
268 catch (UnsupportedOperationException | SQLFeatureNotSupportedException exc) {
269 // free() was only a hint; if the hint is not welcome, then that's OK.
278 int stringWidth(Object obj)
280 if (obj instanceof java.sql.Blob) {
283 return ("" + obj).length();
286 void writeDivider(PrintWriter pw, int[] colWidths) {
287 for (int idx = 0; idx < colWidths.length; ++idx) {
288 pw.print("+" + repChar('-', colWidths[idx] + 2));
293 void writeHeader(PrintWriter pw, Column[] cols, int[] colWidths) {
294 writeDivider(pw, colWidths);
296 for (int idx = 0; idx < cols.length; ++idx) {
297 Column col = cols[idx];
298 pw.print("| " + centrePad(col.getLabel(), colWidths[idx]) + " ");
302 writeDivider(pw, colWidths);
305 void writeRowBuffer(PrintWriter pw, RowBuffer rowBuf, int[] colWidths) throws SQLException {
307 for (int rowIdx = 0; rowIdx < rowBuf.getPending(); ++rowIdx) {
308 Row row = rowBuf.getRow(rowIdx);
309 for (int colIdx = 0; colIdx < colWidths.length; ++colIdx) {
310 Object obj = row.getValue(colIdx + 1);
311 String value = stringify(obj);
312 int width = stringWidth(obj);
313 String padding = repChar(' ', colWidths[colIdx] - width);
314 pw.print("| " + value + padding + " ");