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.db.DbDriver;
15 import net.jaekl.squelch.sql.Column;
16 import net.jaekl.squelch.sql.Row;
18 // Copyright (C) 2016 by Chris Jaekl
20 // Tabular: wrapper around a set of tabular data (such as a ResultSet or table's MetaData).
21 // Includes routines to output the data as a human-friendly table, or as CSV.
23 abstract public class Tabular {
24 private static final long BLOB_MAX = 65536;
26 private static class RowBuffer {
27 private final int ROW_BUF_SIZE = 50;
29 private Row[] m_rowBuf;
30 private int m_pending;
33 m_rowBuf = new Row[ROW_BUF_SIZE];
37 public void addRow(Row row) {
38 assert (m_pending < m_rowBuf.length);
39 m_rowBuf[m_pending] = row;
42 public int getPending() { return m_pending; }
43 public Row getRow(int idx) {
44 assert (idx >= 0 && idx < m_pending);
47 public boolean isFull() { return m_pending >= m_rowBuf.length; }
50 abstract Column[] getCols() throws SQLException;
51 abstract Row getNext() throws SQLException;
53 // Returns the number of (data) rows that were output
54 public int printTable(DbDriver driver, PrintWriter pw, String noRowsMessage)
58 Column[] cols = getCols();
61 // Initialize colWidths to the widths of the column names
62 int[] colWidths = initColWidthsFromColNames(cols);
64 // Examine and buffer up to ROW_BUF_SIZE rows, adjusting (widening) colWidths where needed
65 rowBuf = bufferRows(driver, colWidths);
66 boolean[] suppressed = suppressNulls(driver, rowBuf, cols);
68 int pending = rowBuf.getPending();
70 writeHeader(pw, cols, colWidths, suppressed);
71 writeRowBuffer(pw, rowBuf, colWidths, suppressed);
75 rowBuf = bufferRows(driver, colWidths);
76 pending = rowBuf.getPending();
78 if (driver.isSet(DbDriver.SUPPRESS_NULLS)) {
79 writeDivider(pw, colWidths, suppressed);
80 pw.println("Row limit for suppress_nulls has been reached; output may have been truncated.");
83 writeRowBuffer(pw, rowBuf, colWidths, suppressed);
85 rowBuf = bufferRows(driver, colWidths);
86 pending = rowBuf.getPending();
90 writeDivider(pw, colWidths, suppressed);
91 // TODO: Implement a StringTable for i18n
92 pw.println("" + rowCount + " row(s) returned.");
95 pw.println(noRowsMessage);
103 // Returns the number of (data) rows that were output
104 public int printCsv(PrintWriter pw) throws SQLException {
106 Column[] cols = getCols();
109 StringBuilder sb = new StringBuilder();
110 boolean firstCol = true;
111 for (int idx = 1; idx <= cols.length; ++idx) {
118 sb.append("" + cols[idx - 1].getLabel());
121 String header = sb.toString();
123 while (null != (row = getNext())) {
124 if (null != header) {
130 for (int idx = 1; idx <= cols.length; ++idx) {
137 pw.print("" + row.getValue(idx));
147 // Examine and buffer up to rowBuf.length rows.
148 // Returns the number of actual rows that were buffered (zero if no more rows are available).
149 RowBuffer bufferRows(DbDriver driver, int[] colWidths) throws SQLException
151 RowBuffer rowBuf = new RowBuffer();
153 while (!rowBuf.isFull()) {
156 // No more rows available
160 boolean allColsNull = true;
161 for (int idx = 1; idx <= colWidths.length; ++idx) {
162 if (null != row.getValue(idx)) {
167 if ((!allColsNull) || (!driver.isSet(DbDriver.SUPPRESS_NULLS)))
171 // Check whether all values in this row will fit in the current column widths
172 for (int colIdx = 0; colIdx < colWidths.length; ++colIdx) {
173 int width = stringify(row.getValue(colIdx + 1)).length();
174 if (width > colWidths[colIdx]) {
175 // Widen the column to fit this value
176 colWidths[colIdx] = width;
185 String centrePad(String value, int desiredWidth)
188 return centrePad("NULL", desiredWidth);
190 if (value.length() >= desiredWidth) {
193 int left = (desiredWidth - value.length()) / 2;
194 int right = desiredWidth - value.length() - left;
195 return repChar(' ', left) + value + repChar(' ', right);
198 int[] initColWidthsFromColNames(Column[] cols)
200 int[] widths = new int[cols.length];
202 for (int idx = 0; idx < cols.length; ++idx) {
203 widths[idx] = cols[idx].getLabel().length();
209 Class<?> classForSqlType(int sqlType)
214 return Boolean.class;
216 return Character.class;
219 case Types.TIMESTAMP:
220 return java.util.Date.class;
231 return Integer.class;
234 case Types.LONGNVARCHAR:
235 case Types.LONGVARCHAR:
244 String repChar(char chr, int times)
246 StringBuffer sb = new StringBuffer();
247 for (int idx = 0; idx < times; ++idx) {
250 return sb.toString();
253 String stringify(Object obj) throws SQLException
255 if (obj instanceof java.sql.Blob) {
256 java.sql.Blob blob = null;
259 blob = (java.sql.Blob)obj;
260 long length = blob.length();
261 if (length > BLOB_MAX) {
264 byte[] content = blob.getBytes(1, (int)length);
267 // Assume that the BLOB is actually UTF-8 text, and attempt to return it that way.
268 // This happens to work well for the databases that I deal with regularly.
269 // Other users may want to change this assumption here...
270 return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)
271 .onUnmappableCharacter(CodingErrorAction.REPORT)
272 .decode(ByteBuffer.wrap(content))
275 catch (CharacterCodingException exc) {
276 // If we get here, then the BLOB's content is not valid UTF-8.
277 // This may be because it's in a different character set, or because it's non-text
278 // (e.g., a bitmap image).
279 // We'll hex-dump it instead.
280 return DatatypeConverter.printHexBinary(content);
288 catch (UnsupportedOperationException | SQLFeatureNotSupportedException exc) {
289 // free() was only a hint; if the hint is not welcome, then that's OK.
298 int stringWidth(Object obj)
300 if (obj instanceof java.sql.Blob) {
303 return ("" + obj).length();
306 boolean[] suppressNulls(DbDriver driver, RowBuffer rowBuf, Column[] cols)
308 boolean[] result = new boolean[cols.length];
310 if ( !(driver.isSet(DbDriver.SUPPRESS_NULLS))
311 || (rowBuf.getPending() < 1) )
313 // Null-suppression is turned off, or
314 // there are no data rows,
315 // so do not suppress any columns.
316 for (int colIdx = 0; colIdx < cols.length; ++colIdx) {
317 result[colIdx] = false;
322 // Start with the assumption that we'll suppress all columns
323 for (int colIdx = 0; colIdx < cols.length; ++colIdx) {
324 result[colIdx] = true;
327 for (int rowIdx = 0; rowIdx < rowBuf.getPending(); ++rowIdx) {
328 Row row = rowBuf.getRow(rowIdx);
329 for (int colIdx = 1; colIdx <= cols.length; ++colIdx) {
330 if (null != row.getValue(colIdx)) {
331 // This column has data, so do not suppress it
332 result[colIdx - 1] = false;
340 void writeDivider(PrintWriter pw, int[] colWidths, boolean[] suppressed) {
341 for (int idx = 0; idx < colWidths.length; ++idx) {
342 if (!suppressed[idx]) {
343 pw.print("+" + repChar('-', colWidths[idx] + 2));
349 void writeHeader(PrintWriter pw, Column[] cols, int[] colWidths, boolean[] suppressed) {
350 writeDivider(pw, colWidths, suppressed);
352 for (int idx = 0; idx < cols.length; ++idx) {
353 if (!suppressed[idx]) {
354 Column col = cols[idx];
355 pw.print("| " + centrePad(col.getLabel(), colWidths[idx]) + " ");
360 writeDivider(pw, colWidths, suppressed);
363 void writeRowBuffer(PrintWriter pw, RowBuffer rowBuf, int[] colWidths, boolean[] suppressed) throws SQLException {
365 for (int rowIdx = 0; rowIdx < rowBuf.getPending(); ++rowIdx) {
366 Row row = rowBuf.getRow(rowIdx);
367 for (int colIdx = 0; colIdx < colWidths.length; ++colIdx) {
368 if (!suppressed[colIdx]) {
369 Object obj = row.getValue(colIdx + 1);
370 String value = stringify(obj);
371 int width = stringWidth(value);
372 String padding = repChar(' ', colWidths[colIdx] - width);
373 pw.print("| " + value + padding + " ");