java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java Mon Mar 04 20:15:24 2019 +0100
1.3 @@ -0,0 +1,308 @@
1.4 +/**
1.5 + * SQL-DK
1.6 + * Copyright © 2013 František Kučera (frantovo.cz)
1.7 + *
1.8 + * This program is free software: you can redistribute it and/or modify
1.9 + * it under the terms of the GNU General Public License as published by
1.10 + * the Free Software Foundation, either version 3 of the License, or
1.11 + * (at your option) any later version.
1.12 + *
1.13 + * This program is distributed in the hope that it will be useful,
1.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
1.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1.16 + * GNU General Public License for more details.
1.17 + *
1.18 + * You should have received a copy of the GNU General Public License
1.19 + * along with this program. If not, see <http://www.gnu.org/licenses/>.
1.20 + */
1.21 +package info.globalcode.sql.dk.formatting;
1.22 +
1.23 +import info.globalcode.sql.dk.ColorfulPrintWriter;
1.24 +import static info.globalcode.sql.dk.ColorfulPrintWriter.*;
1.25 +import info.globalcode.sql.dk.Functions;
1.26 +import static info.globalcode.sql.dk.Functions.lpad;
1.27 +import static info.globalcode.sql.dk.Functions.rpad;
1.28 +import static info.globalcode.sql.dk.Functions.repeat;
1.29 +import info.globalcode.sql.dk.configuration.PropertyDeclaration;
1.30 +import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL;
1.31 +import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION;
1.32 +import java.sql.SQLException;
1.33 +import java.sql.SQLXML;
1.34 +import java.util.List;
1.35 +import java.util.logging.Level;
1.36 +import java.util.logging.Logger;
1.37 +
1.38 +/**
1.39 + * <p>
1.40 + * Prints human-readable output – tables of result sets and text messages with update counts.
1.41 + * </p>
1.42 + *
1.43 + * <p>
1.44 + * Longer values might break the table – overflow the cells – see alternative tabular formatters and
1.45 + * the {@linkplain #PROPERTY_TRIM} property.
1.46 + * </p>
1.47 + *
1.48 + * @author Ing. František Kučera (frantovo.cz)
1.49 + * @see TabularPrefetchingFormatter
1.50 + * @see TabularWrappingFormatter
1.51 + */
1.52 +@PropertyDeclaration(name = COLORFUL, defaultValue = "true", type = Boolean.class, description = COLORFUL_DESCRIPTION)
1.53 +@PropertyDeclaration(name = TabularFormatter.PROPERTY_ASCII, defaultValue = "false", type = Boolean.class, description = "whether to use ASCII table borders instead of unicode ones")
1.54 +@PropertyDeclaration(name = TabularFormatter.PROPERTY_TRIM, defaultValue = "false", type = Boolean.class, description = "whether to trim the values to fit the column width")
1.55 +@PropertyDeclaration(name = TabularFormatter.PROPERTY_HEADER_TYPE, defaultValue = "true", type = Boolean.class, description = "whether to print data types in column headers")
1.56 +public class TabularFormatter extends AbstractFormatter {
1.57 +
1.58 + private static final Logger log = Logger.getLogger(TabularFormatter.class.getName());
1.59 + public static final String NAME = "tabular"; // bash-completion:formatter
1.60 + private static final String HEADER_TYPE_PREFIX = " (";
1.61 + private static final String HEADER_TYPE_SUFFIX = ")";
1.62 + public static final String PROPERTY_ASCII = "ascii";
1.63 + public static final String PROPERTY_TRIM = "trim";
1.64 + public static final String PROPERTY_HEADER_TYPE = "headerTypes";
1.65 + protected ColorfulPrintWriter out;
1.66 + private boolean firstResult = true;
1.67 + private int[] columnWidth;
1.68 + /**
1.69 + * use ASCII borders instead of unicode ones
1.70 + */
1.71 + private final boolean asciiNostalgia;
1.72 + /**
1.73 + * Trim values if they are longer than cell size
1.74 + */
1.75 + private final boolean trimValues;
1.76 + /**
1.77 + * Print data type of each column in the header
1.78 + */
1.79 + private final boolean printHeaderTypes;
1.80 +
1.81 + public TabularFormatter(FormatterContext formatterContext) {
1.82 + super(formatterContext);
1.83 + out = new ColorfulPrintWriter(formatterContext.getOutputStream());
1.84 + asciiNostalgia = formatterContext.getProperties().getBoolean(PROPERTY_ASCII, false);
1.85 + trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false);
1.86 + printHeaderTypes = formatterContext.getProperties().getBoolean(PROPERTY_HEADER_TYPE, true);
1.87 + out.setColorful(formatterContext.getProperties().getBoolean(COLORFUL, true));
1.88 + }
1.89 +
1.90 + @Override
1.91 + public void writeStartResultSet(ColumnsHeader header) {
1.92 + super.writeStartResultSet(header);
1.93 + printResultSeparator();
1.94 +
1.95 + initColumnWidths(header.getColumnCount());
1.96 +
1.97 + printTableIndent();
1.98 + printTableBorder("╭");
1.99 +
1.100 + List<ColumnDescriptor> columnDescriptors = header.getColumnDescriptors();
1.101 +
1.102 + for (ColumnDescriptor cd : columnDescriptors) {
1.103 + // padding: make header cell at least same width as data cells in this column
1.104 + int typeWidth = printHeaderTypes ? cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length() : 0;
1.105 + cd.setLabel(rpad(cd.getLabel(), getColumnWidth(cd.getColumnNumber()) - typeWidth));
1.106 + updateColumnWidth(cd.getColumnNumber(), cd.getLabel().length() + typeWidth);
1.107 +
1.108 + if (!cd.isFirstColumn()) {
1.109 + printTableBorder("┬");
1.110 + }
1.111 + printTableBorder(repeat('─', getColumnWidth(cd.getColumnNumber()) + 2));
1.112 + }
1.113 + printTableBorder("╮");
1.114 + out.println();
1.115 +
1.116 + for (ColumnDescriptor cd : columnDescriptors) {
1.117 + if (cd.isFirstColumn()) {
1.118 + printTableIndent();
1.119 + printTableBorder("│ ");
1.120 + } else {
1.121 + printTableBorder(" │ ");
1.122 + }
1.123 + out.print(TerminalStyle.Bright, cd.getLabel());
1.124 + if (printHeaderTypes) {
1.125 + out.print(HEADER_TYPE_PREFIX);
1.126 + out.print(cd.getTypeName());
1.127 + out.print(HEADER_TYPE_SUFFIX);
1.128 + }
1.129 + if (cd.isLastColumn()) {
1.130 + printTableBorder(" │");
1.131 + }
1.132 + }
1.133 + out.println();
1.134 +
1.135 + printTableIndent();
1.136 + printTableBorder("├");
1.137 + for (int i = 1; i <= header.getColumnCount(); i++) {
1.138 + if (i > 1) {
1.139 + printTableBorder("┼");
1.140 + }
1.141 + printTableBorder(repeat('─', getColumnWidth(i) + 2));
1.142 + }
1.143 + printTableBorder("┤");
1.144 + out.println();
1.145 +
1.146 + out.flush();
1.147 + }
1.148 +
1.149 + /**
1.150 + * Must be called before {@linkplain #updateColumnWidth(int, int)} and
1.151 + * {@linkplain #getColumnWidth(int)} for each result set.
1.152 + *
1.153 + * @param columnCount number of columns in current result set
1.154 + */
1.155 + protected void initColumnWidths(int columnCount) {
1.156 + if (columnWidth == null) {
1.157 + columnWidth = new int[columnCount];
1.158 + }
1.159 + }
1.160 +
1.161 + protected void cleanColumnWidths() {
1.162 + columnWidth = null;
1.163 + }
1.164 +
1.165 + @Override
1.166 + public void writeColumnValue(Object value) {
1.167 + super.writeColumnValue(value);
1.168 + writeColumnValueInternal(value);
1.169 + }
1.170 +
1.171 + protected void writeColumnValueInternal(Object value) {
1.172 +
1.173 + if (isCurrentColumnFirst()) {
1.174 + printTableIndent();
1.175 + printTableBorder("│ ");
1.176 + } else {
1.177 + printTableBorder(" │ ");
1.178 + }
1.179 +
1.180 + printValueWithWhitespaceReplaced(toString(value));
1.181 +
1.182 + if (isCurrentColumnLast()) {
1.183 + printTableBorder(" │");
1.184 + }
1.185 +
1.186 + }
1.187 +
1.188 + protected void printValueWithWhitespaceReplaced(String text) {
1.189 + Functions.printValueWithWhitespaceReplaced(out, text, TerminalColor.Cyan, TerminalColor.Red);
1.190 + }
1.191 +
1.192 + protected int getColumnWidth(int columnNumber) {
1.193 + return columnWidth[columnNumber - 1];
1.194 + }
1.195 +
1.196 + private void setColumnWidth(int columnNumber, int width) {
1.197 + columnWidth[columnNumber - 1] = width;
1.198 + }
1.199 +
1.200 + protected void updateColumnWidth(int columnNumber, int width) {
1.201 + int oldWidth = getColumnWidth(columnNumber);
1.202 + setColumnWidth(columnNumber, Math.max(width, oldWidth));
1.203 +
1.204 + }
1.205 +
1.206 + protected String toString(Object value) {
1.207 + final int width = getColumnWidth(getCurrentColumnsCount());
1.208 + String result;
1.209 + if (value instanceof Number || value instanceof Boolean) {
1.210 + result = lpad(String.valueOf(value), width);
1.211 + } else {
1.212 + if (value instanceof SQLXML) {
1.213 + // TODO: move to a common method, share with other formatters
1.214 + try {
1.215 + value = ((SQLXML) value).getString();
1.216 + } catch (SQLException e) {
1.217 + log.log(Level.SEVERE, "Unable to format XML", e);
1.218 + }
1.219 + }
1.220 +
1.221 + result = rpad(String.valueOf(value), width);
1.222 + }
1.223 + // ? value = (boolean) value ? "✔" : "✗";
1.224 +
1.225 + if (trimValues && result.length() > width) {
1.226 + result = result.substring(0, width - 1) + "…";
1.227 + }
1.228 +
1.229 + return result;
1.230 + }
1.231 +
1.232 + @Override
1.233 + public void writeEndRow() {
1.234 + super.writeEndRow();
1.235 + writeEndRowInternal();
1.236 + }
1.237 +
1.238 + public void writeEndRowInternal() {
1.239 + out.println();
1.240 + out.flush();
1.241 + }
1.242 +
1.243 + @Override
1.244 + public void writeEndResultSet() {
1.245 + int columnCount = getCurrentColumnsHeader().getColumnCount();
1.246 + super.writeEndResultSet();
1.247 +
1.248 + printTableIndent();
1.249 + printTableBorder("╰");
1.250 + for (int i = 1; i <= columnCount; i++) {
1.251 + if (i > 1) {
1.252 + printTableBorder("┴");
1.253 + }
1.254 + printTableBorder(repeat('─', getColumnWidth(i) + 2));
1.255 + }
1.256 + printTableBorder("╯");
1.257 + out.println();
1.258 +
1.259 + cleanColumnWidths();
1.260 +
1.261 + out.print(TerminalColor.Yellow, "Record count: ");
1.262 + out.println(getCurrentRowCount());
1.263 + out.bell();
1.264 + out.flush();
1.265 + }
1.266 +
1.267 + @Override
1.268 + public void writeUpdatesResult(int updatedRowsCount) {
1.269 + super.writeUpdatesResult(updatedRowsCount);
1.270 + printResultSeparator();
1.271 + out.print(TerminalColor.Red, "Updated records: ");
1.272 + out.println(updatedRowsCount);
1.273 + out.bell();
1.274 + out.flush();
1.275 + }
1.276 +
1.277 + @Override
1.278 + public void writeEndDatabase() {
1.279 + super.writeEndDatabase();
1.280 + out.flush();
1.281 + }
1.282 +
1.283 + private void printResultSeparator() {
1.284 + if (firstResult) {
1.285 + firstResult = false;
1.286 + } else {
1.287 + out.println();
1.288 + }
1.289 + }
1.290 +
1.291 + protected void printTableBorder(String border) {
1.292 + if (asciiNostalgia) {
1.293 + border = border.replaceAll("─", "-");
1.294 + border = border.replaceAll("│", "|");
1.295 + border = border.replaceAll("[╭┬╮├┼┤╰┴╯]", "+");
1.296 + }
1.297 +
1.298 + out.print(TerminalColor.Green, border);
1.299 + }
1.300 +
1.301 + protected void printTableIndent() {
1.302 + out.print(" ");
1.303 + }
1.304 +
1.305 + /**
1.306 + * @return whether should print only ASCII characters instead of unlimited Unicode.
1.307 + */
1.308 + protected boolean isAsciiNostalgia() {
1.309 + return asciiNostalgia;
1.310 + }
1.311 +}