franta-hg@32: /** franta-hg@32: * SQL-DK franta-hg@32: * Copyright © 2013 František Kučera (frantovo.cz) franta-hg@32: * franta-hg@32: * This program is free software: you can redistribute it and/or modify franta-hg@32: * it under the terms of the GNU General Public License as published by franta-hg@250: * the Free Software Foundation, version 3 of the License. franta-hg@32: * franta-hg@32: * This program is distributed in the hope that it will be useful, franta-hg@32: * but WITHOUT ANY WARRANTY; without even the implied warranty of franta-hg@32: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the franta-hg@32: * GNU General Public License for more details. franta-hg@32: * franta-hg@32: * You should have received a copy of the GNU General Public License franta-hg@32: * along with this program. If not, see . franta-hg@32: */ franta-hg@32: package info.globalcode.sql.dk.formatting; franta-hg@32: franta-hg@34: import info.globalcode.sql.dk.ColorfulPrintWriter; franta-hg@37: import static info.globalcode.sql.dk.ColorfulPrintWriter.*; franta-hg@218: import info.globalcode.sql.dk.Functions; franta-hg@39: import static info.globalcode.sql.dk.Functions.lpad; franta-hg@39: import static info.globalcode.sql.dk.Functions.rpad; franta-hg@40: import static info.globalcode.sql.dk.Functions.repeat; franta-hg@206: import info.globalcode.sql.dk.configuration.PropertyDeclaration; franta-hg@206: import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL; franta-hg@206: import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION; franta-hg@234: import java.sql.SQLException; franta-hg@234: import java.sql.SQLXML; franta-hg@88: import java.util.List; franta-hg@234: import java.util.logging.Level; franta-hg@234: import java.util.logging.Logger; franta-hg@34: franta-hg@32: /** franta-hg@185: *

franta-hg@185: * Prints human-readable output – tables of result sets and text messages with update counts. franta-hg@185: *

franta-hg@155: * franta-hg@185: *

franta-hg@234: * Longer values might break the table – overflow the cells – see alternative tabular formatters and franta-hg@234: * the {@linkplain #PROPERTY_TRIM} property. franta-hg@185: *

franta-hg@32: * franta-hg@32: * @author Ing. František Kučera (frantovo.cz) franta-hg@155: * @see TabularPrefetchingFormatter franta-hg@155: * @see TabularWrappingFormatter franta-hg@32: */ franta-hg@207: @PropertyDeclaration(name = COLORFUL, defaultValue = "true", type = Boolean.class, description = COLORFUL_DESCRIPTION) franta-hg@207: @PropertyDeclaration(name = TabularFormatter.PROPERTY_ASCII, defaultValue = "false", type = Boolean.class, description = "whether to use ASCII table borders instead of unicode ones") franta-hg@207: @PropertyDeclaration(name = TabularFormatter.PROPERTY_TRIM, defaultValue = "false", type = Boolean.class, description = "whether to trim the values to fit the column width") franta-hg@227: @PropertyDeclaration(name = TabularFormatter.PROPERTY_HEADER_TYPE, defaultValue = "true", type = Boolean.class, description = "whether to print data types in column headers") franta-hg@32: public class TabularFormatter extends AbstractFormatter { franta-hg@32: franta-hg@234: private static final Logger log = Logger.getLogger(TabularFormatter.class.getName()); franta-hg@79: public static final String NAME = "tabular"; // bash-completion:formatter franta-hg@39: private static final String HEADER_TYPE_PREFIX = " ("; franta-hg@39: private static final String HEADER_TYPE_SUFFIX = ")"; franta-hg@104: public static final String PROPERTY_ASCII = "ascii"; franta-hg@104: public static final String PROPERTY_TRIM = "trim"; franta-hg@227: public static final String PROPERTY_HEADER_TYPE = "headerTypes"; franta-hg@123: protected ColorfulPrintWriter out; franta-hg@37: private boolean firstResult = true; franta-hg@39: private int[] columnWidth; franta-hg@87: /** franta-hg@87: * use ASCII borders instead of unicode ones franta-hg@87: */ franta-hg@104: private final boolean asciiNostalgia; franta-hg@87: /** franta-hg@87: * Trim values if they are longer than cell size franta-hg@87: */ franta-hg@104: private final boolean trimValues; franta-hg@227: /** franta-hg@227: * Print data type of each column in the header franta-hg@227: */ franta-hg@227: private final boolean printHeaderTypes; franta-hg@32: franta-hg@32: public TabularFormatter(FormatterContext formatterContext) { franta-hg@32: super(formatterContext); franta-hg@34: out = new ColorfulPrintWriter(formatterContext.getOutputStream()); franta-hg@104: asciiNostalgia = formatterContext.getProperties().getBoolean(PROPERTY_ASCII, false); franta-hg@104: trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false); franta-hg@227: printHeaderTypes = formatterContext.getProperties().getBoolean(PROPERTY_HEADER_TYPE, true); franta-hg@206: out.setColorful(formatterContext.getProperties().getBoolean(COLORFUL, true)); franta-hg@34: } franta-hg@34: franta-hg@34: @Override franta-hg@142: public void writeStartResultSet(ColumnsHeader header) { franta-hg@142: super.writeStartResultSet(header); franta-hg@37: printResultSeparator(); franta-hg@37: franta-hg@88: initColumnWidths(header.getColumnCount()); franta-hg@39: franta-hg@40: printTableIndent(); franta-hg@40: printTableBorder("╭"); franta-hg@88: franta-hg@88: List columnDescriptors = header.getColumnDescriptors(); franta-hg@88: franta-hg@88: for (ColumnDescriptor cd : columnDescriptors) { franta-hg@88: // padding: make header cell at least same width as data cells in this column franta-hg@227: int typeWidth = printHeaderTypes ? cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length() : 0; franta-hg@88: cd.setLabel(rpad(cd.getLabel(), getColumnWidth(cd.getColumnNumber()) - typeWidth)); franta-hg@88: updateColumnWidth(cd.getColumnNumber(), cd.getLabel().length() + typeWidth); franta-hg@88: franta-hg@40: if (!cd.isFirstColumn()) { franta-hg@40: printTableBorder("┬"); franta-hg@40: } franta-hg@40: printTableBorder(repeat('─', getColumnWidth(cd.getColumnNumber()) + 2)); franta-hg@40: } franta-hg@40: printTableBorder("╮"); franta-hg@40: out.println(); franta-hg@40: franta-hg@88: for (ColumnDescriptor cd : columnDescriptors) { franta-hg@40: if (cd.isFirstColumn()) { franta-hg@40: printTableIndent(); franta-hg@40: printTableBorder("│ "); franta-hg@40: } else { franta-hg@40: printTableBorder(" │ "); franta-hg@40: } franta-hg@37: out.print(TerminalStyle.Bright, cd.getLabel()); franta-hg@227: if (printHeaderTypes) { franta-hg@227: out.print(HEADER_TYPE_PREFIX); franta-hg@227: out.print(cd.getTypeName()); franta-hg@227: out.print(HEADER_TYPE_SUFFIX); franta-hg@227: } franta-hg@40: if (cd.isLastColumn()) { franta-hg@40: printTableBorder(" │"); franta-hg@37: } franta-hg@37: } franta-hg@37: out.println(); franta-hg@40: franta-hg@40: printTableIndent(); franta-hg@40: printTableBorder("├"); franta-hg@40: for (int i = 1; i <= header.getColumnCount(); i++) { franta-hg@40: if (i > 1) { franta-hg@40: printTableBorder("┼"); franta-hg@40: } franta-hg@40: printTableBorder(repeat('─', getColumnWidth(i) + 2)); franta-hg@40: } franta-hg@40: printTableBorder("┤"); franta-hg@40: out.println(); franta-hg@40: franta-hg@37: out.flush(); franta-hg@37: } franta-hg@37: franta-hg@88: /** franta-hg@234: * Must be called before {@linkplain #updateColumnWidth(int, int)} and franta-hg@234: * {@linkplain #getColumnWidth(int)} for each result set. franta-hg@88: * franta-hg@88: * @param columnCount number of columns in current result set franta-hg@88: */ franta-hg@88: protected void initColumnWidths(int columnCount) { franta-hg@88: if (columnWidth == null) { franta-hg@88: columnWidth = new int[columnCount]; franta-hg@88: } franta-hg@88: } franta-hg@88: franta-hg@88: protected void cleanColumnWidths() { franta-hg@88: columnWidth = null; franta-hg@88: } franta-hg@88: franta-hg@37: @Override franta-hg@34: public void writeColumnValue(Object value) { franta-hg@34: super.writeColumnValue(value); franta-hg@123: writeColumnValueInternal(value); franta-hg@123: } franta-hg@123: franta-hg@123: protected void writeColumnValueInternal(Object value) { franta-hg@34: franta-hg@40: if (isCurrentColumnFirst()) { franta-hg@40: printTableIndent(); franta-hg@40: printTableBorder("│ "); franta-hg@40: } else { franta-hg@90: printTableBorder(" │ "); franta-hg@34: } franta-hg@37: franta-hg@219: printValueWithWhitespaceReplaced(toString(value)); franta-hg@40: franta-hg@40: if (isCurrentColumnLast()) { franta-hg@90: printTableBorder(" │"); franta-hg@40: } franta-hg@40: franta-hg@39: } franta-hg@39: franta-hg@219: protected void printValueWithWhitespaceReplaced(String text) { franta-hg@219: Functions.printValueWithWhitespaceReplaced(out, text, TerminalColor.Cyan, TerminalColor.Red); franta-hg@219: } franta-hg@219: franta-hg@123: protected int getColumnWidth(int columnNumber) { franta-hg@39: return columnWidth[columnNumber - 1]; franta-hg@39: } franta-hg@39: franta-hg@39: private void setColumnWidth(int columnNumber, int width) { franta-hg@39: columnWidth[columnNumber - 1] = width; franta-hg@39: } franta-hg@39: franta-hg@88: protected void updateColumnWidth(int columnNumber, int width) { franta-hg@39: int oldWidth = getColumnWidth(columnNumber); franta-hg@39: setColumnWidth(columnNumber, Math.max(width, oldWidth)); franta-hg@39: franta-hg@39: } franta-hg@39: franta-hg@39: protected String toString(Object value) { franta-hg@39: final int width = getColumnWidth(getCurrentColumnsCount()); franta-hg@87: String result; franta-hg@43: if (value instanceof Number || value instanceof Boolean) { franta-hg@98: result = lpad(String.valueOf(value), width); franta-hg@39: } else { franta-hg@234: if (value instanceof SQLXML) { franta-hg@234: // TODO: move to a common method, share with other formatters franta-hg@234: try { franta-hg@234: value = ((SQLXML) value).getString(); franta-hg@234: } catch (SQLException e) { franta-hg@234: log.log(Level.SEVERE, "Unable to format XML", e); franta-hg@234: } franta-hg@234: } franta-hg@234: franta-hg@98: result = rpad(String.valueOf(value), width); franta-hg@39: } franta-hg@40: // ? value = (boolean) value ? "✔" : "✗"; franta-hg@87: franta-hg@87: if (trimValues && result.length() > width) { franta-hg@87: result = result.substring(0, width - 1) + "…"; franta-hg@87: } franta-hg@87: franta-hg@87: return result; franta-hg@34: } franta-hg@34: franta-hg@34: @Override franta-hg@34: public void writeEndRow() { franta-hg@34: super.writeEndRow(); franta-hg@123: writeEndRowInternal(); franta-hg@123: } franta-hg@123: franta-hg@123: public void writeEndRowInternal() { franta-hg@34: out.println(); franta-hg@34: out.flush(); franta-hg@32: } franta-hg@37: franta-hg@37: @Override franta-hg@37: public void writeEndResultSet() { franta-hg@40: int columnCount = getCurrentColumnsHeader().getColumnCount(); franta-hg@37: super.writeEndResultSet(); franta-hg@40: franta-hg@40: printTableIndent(); franta-hg@40: printTableBorder("╰"); franta-hg@40: for (int i = 1; i <= columnCount; i++) { franta-hg@40: if (i > 1) { franta-hg@40: printTableBorder("┴"); franta-hg@40: } franta-hg@40: printTableBorder(repeat('─', getColumnWidth(i) + 2)); franta-hg@40: } franta-hg@40: printTableBorder("╯"); franta-hg@40: out.println(); franta-hg@40: franta-hg@88: cleanColumnWidths(); franta-hg@40: franta-hg@37: out.print(TerminalColor.Yellow, "Record count: "); franta-hg@37: out.println(getCurrentRowCount()); franta-hg@40: out.bell(); franta-hg@37: out.flush(); franta-hg@37: } franta-hg@37: franta-hg@37: @Override franta-hg@142: public void writeUpdatesResult(int updatedRowsCount) { franta-hg@142: super.writeUpdatesResult(updatedRowsCount); franta-hg@37: printResultSeparator(); franta-hg@37: out.print(TerminalColor.Red, "Updated records: "); franta-hg@37: out.println(updatedRowsCount); franta-hg@40: out.bell(); franta-hg@37: out.flush(); franta-hg@37: } franta-hg@37: franta-hg@37: @Override franta-hg@37: public void writeEndDatabase() { franta-hg@37: super.writeEndDatabase(); franta-hg@37: out.flush(); franta-hg@37: } franta-hg@37: franta-hg@37: private void printResultSeparator() { franta-hg@37: if (firstResult) { franta-hg@37: firstResult = false; franta-hg@37: } else { franta-hg@37: out.println(); franta-hg@37: } franta-hg@37: } franta-hg@40: franta-hg@123: protected void printTableBorder(String border) { franta-hg@76: if (asciiNostalgia) { franta-hg@76: border = border.replaceAll("─", "-"); franta-hg@76: border = border.replaceAll("│", "|"); franta-hg@76: border = border.replaceAll("[╭┬╮├┼┤╰┴╯]", "+"); franta-hg@76: } franta-hg@87: franta-hg@40: out.print(TerminalColor.Green, border); franta-hg@40: } franta-hg@40: franta-hg@123: protected void printTableIndent() { franta-hg@40: out.print(" "); franta-hg@40: } franta-hg@224: franta-hg@224: /** franta-hg@224: * @return whether should print only ASCII characters instead of unlimited Unicode. franta-hg@224: */ franta-hg@224: protected boolean isAsciiNostalgia() { franta-hg@224: return asciiNostalgia; franta-hg@224: } franta-hg@32: }