java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java
author František Kučera <franta-hg@frantovo.cz>
Thu, 24 Oct 2019 21:43:08 +0200
branchv_0
changeset 250 aae5009bd0af
parent 238 4a1864c3e867
child 255 099bb96f8d8d
permissions -rw-r--r--
fix license version: GNU GPLv3
     1 /**
     2  * SQL-DK
     3  * Copyright © 2013 František Kučera (frantovo.cz)
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 3 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  * GNU General Public License for more details.
    13  *
    14  * You should have received a copy of the GNU General Public License
    15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
    16  */
    17 package info.globalcode.sql.dk.formatting;
    18 
    19 import info.globalcode.sql.dk.ColorfulPrintWriter;
    20 import static info.globalcode.sql.dk.ColorfulPrintWriter.*;
    21 import info.globalcode.sql.dk.Functions;
    22 import static info.globalcode.sql.dk.Functions.lpad;
    23 import static info.globalcode.sql.dk.Functions.rpad;
    24 import static info.globalcode.sql.dk.Functions.repeat;
    25 import info.globalcode.sql.dk.configuration.PropertyDeclaration;
    26 import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL;
    27 import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION;
    28 import java.sql.SQLException;
    29 import java.sql.SQLXML;
    30 import java.util.List;
    31 import java.util.logging.Level;
    32 import java.util.logging.Logger;
    33 
    34 /**
    35  * <p>
    36  * Prints human-readable output – tables of result sets and text messages with update counts.
    37  * </p>
    38  *
    39  * <p>
    40  * Longer values might break the table – overflow the cells – see alternative tabular formatters and
    41  * the {@linkplain #PROPERTY_TRIM} property.
    42  * </p>
    43  *
    44  * @author Ing. František Kučera (frantovo.cz)
    45  * @see TabularPrefetchingFormatter
    46  * @see TabularWrappingFormatter
    47  */
    48 @PropertyDeclaration(name = COLORFUL, defaultValue = "true", type = Boolean.class, description = COLORFUL_DESCRIPTION)
    49 @PropertyDeclaration(name = TabularFormatter.PROPERTY_ASCII, defaultValue = "false", type = Boolean.class, description = "whether to use ASCII table borders instead of unicode ones")
    50 @PropertyDeclaration(name = TabularFormatter.PROPERTY_TRIM, defaultValue = "false", type = Boolean.class, description = "whether to trim the values to fit the column width")
    51 @PropertyDeclaration(name = TabularFormatter.PROPERTY_HEADER_TYPE, defaultValue = "true", type = Boolean.class, description = "whether to print data types in column headers")
    52 public class TabularFormatter extends AbstractFormatter {
    53 
    54 	private static final Logger log = Logger.getLogger(TabularFormatter.class.getName());
    55 	public static final String NAME = "tabular"; // bash-completion:formatter
    56 	private static final String HEADER_TYPE_PREFIX = " (";
    57 	private static final String HEADER_TYPE_SUFFIX = ")";
    58 	public static final String PROPERTY_ASCII = "ascii";
    59 	public static final String PROPERTY_TRIM = "trim";
    60 	public static final String PROPERTY_HEADER_TYPE = "headerTypes";
    61 	protected ColorfulPrintWriter out;
    62 	private boolean firstResult = true;
    63 	private int[] columnWidth;
    64 	/**
    65 	 * use ASCII borders instead of unicode ones
    66 	 */
    67 	private final boolean asciiNostalgia;
    68 	/**
    69 	 * Trim values if they are longer than cell size
    70 	 */
    71 	private final boolean trimValues;
    72 	/**
    73 	 * Print data type of each column in the header
    74 	 */
    75 	private final boolean printHeaderTypes;
    76 
    77 	public TabularFormatter(FormatterContext formatterContext) {
    78 		super(formatterContext);
    79 		out = new ColorfulPrintWriter(formatterContext.getOutputStream());
    80 		asciiNostalgia = formatterContext.getProperties().getBoolean(PROPERTY_ASCII, false);
    81 		trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false);
    82 		printHeaderTypes = formatterContext.getProperties().getBoolean(PROPERTY_HEADER_TYPE, true);
    83 		out.setColorful(formatterContext.getProperties().getBoolean(COLORFUL, true));
    84 	}
    85 
    86 	@Override
    87 	public void writeStartResultSet(ColumnsHeader header) {
    88 		super.writeStartResultSet(header);
    89 		printResultSeparator();
    90 
    91 		initColumnWidths(header.getColumnCount());
    92 
    93 		printTableIndent();
    94 		printTableBorder("╭");
    95 
    96 		List<ColumnDescriptor> columnDescriptors = header.getColumnDescriptors();
    97 
    98 		for (ColumnDescriptor cd : columnDescriptors) {
    99 			// padding: make header cell at least same width as data cells in this column
   100 			int typeWidth = printHeaderTypes ? cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length() : 0;
   101 			cd.setLabel(rpad(cd.getLabel(), getColumnWidth(cd.getColumnNumber()) - typeWidth));
   102 			updateColumnWidth(cd.getColumnNumber(), cd.getLabel().length() + typeWidth);
   103 
   104 			if (!cd.isFirstColumn()) {
   105 				printTableBorder("┬");
   106 			}
   107 			printTableBorder(repeat('─', getColumnWidth(cd.getColumnNumber()) + 2));
   108 		}
   109 		printTableBorder("╮");
   110 		out.println();
   111 
   112 		for (ColumnDescriptor cd : columnDescriptors) {
   113 			if (cd.isFirstColumn()) {
   114 				printTableIndent();
   115 				printTableBorder("│ ");
   116 			} else {
   117 				printTableBorder(" │ ");
   118 			}
   119 			out.print(TerminalStyle.Bright, cd.getLabel());
   120 			if (printHeaderTypes) {
   121 				out.print(HEADER_TYPE_PREFIX);
   122 				out.print(cd.getTypeName());
   123 				out.print(HEADER_TYPE_SUFFIX);
   124 			}
   125 			if (cd.isLastColumn()) {
   126 				printTableBorder(" │");
   127 			}
   128 		}
   129 		out.println();
   130 
   131 		printTableIndent();
   132 		printTableBorder("├");
   133 		for (int i = 1; i <= header.getColumnCount(); i++) {
   134 			if (i > 1) {
   135 				printTableBorder("┼");
   136 			}
   137 			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   138 		}
   139 		printTableBorder("┤");
   140 		out.println();
   141 
   142 		out.flush();
   143 	}
   144 
   145 	/**
   146 	 * Must be called before {@linkplain #updateColumnWidth(int, int)} and
   147 	 * {@linkplain #getColumnWidth(int)} for each result set.
   148 	 *
   149 	 * @param columnCount number of columns in current result set
   150 	 */
   151 	protected void initColumnWidths(int columnCount) {
   152 		if (columnWidth == null) {
   153 			columnWidth = new int[columnCount];
   154 		}
   155 	}
   156 
   157 	protected void cleanColumnWidths() {
   158 		columnWidth = null;
   159 	}
   160 
   161 	@Override
   162 	public void writeColumnValue(Object value) {
   163 		super.writeColumnValue(value);
   164 		writeColumnValueInternal(value);
   165 	}
   166 
   167 	protected void writeColumnValueInternal(Object value) {
   168 
   169 		if (isCurrentColumnFirst()) {
   170 			printTableIndent();
   171 			printTableBorder("│ ");
   172 		} else {
   173 			printTableBorder(" │ ");
   174 		}
   175 
   176 		printValueWithWhitespaceReplaced(toString(value));
   177 
   178 		if (isCurrentColumnLast()) {
   179 			printTableBorder(" │");
   180 		}
   181 
   182 	}
   183 
   184 	protected void printValueWithWhitespaceReplaced(String text) {
   185 		Functions.printValueWithWhitespaceReplaced(out, text, TerminalColor.Cyan, TerminalColor.Red);
   186 	}
   187 
   188 	protected int getColumnWidth(int columnNumber) {
   189 		return columnWidth[columnNumber - 1];
   190 	}
   191 
   192 	private void setColumnWidth(int columnNumber, int width) {
   193 		columnWidth[columnNumber - 1] = width;
   194 	}
   195 
   196 	protected void updateColumnWidth(int columnNumber, int width) {
   197 		int oldWidth = getColumnWidth(columnNumber);
   198 		setColumnWidth(columnNumber, Math.max(width, oldWidth));
   199 
   200 	}
   201 
   202 	protected String toString(Object value) {
   203 		final int width = getColumnWidth(getCurrentColumnsCount());
   204 		String result;
   205 		if (value instanceof Number || value instanceof Boolean) {
   206 			result = lpad(String.valueOf(value), width);
   207 		} else {
   208 			if (value instanceof SQLXML) {
   209 				// TODO: move to a common method, share with other formatters
   210 				try {
   211 					value = ((SQLXML) value).getString();
   212 				} catch (SQLException e) {
   213 					log.log(Level.SEVERE, "Unable to format XML", e);
   214 				}
   215 			}
   216 
   217 			result = rpad(String.valueOf(value), width);
   218 		}
   219 		// ?	value = (boolean) value ? "✔" : "✗";
   220 
   221 		if (trimValues && result.length() > width) {
   222 			result = result.substring(0, width - 1) + "…";
   223 		}
   224 
   225 		return result;
   226 	}
   227 
   228 	@Override
   229 	public void writeEndRow() {
   230 		super.writeEndRow();
   231 		writeEndRowInternal();
   232 	}
   233 
   234 	public void writeEndRowInternal() {
   235 		out.println();
   236 		out.flush();
   237 	}
   238 
   239 	@Override
   240 	public void writeEndResultSet() {
   241 		int columnCount = getCurrentColumnsHeader().getColumnCount();
   242 		super.writeEndResultSet();
   243 
   244 		printTableIndent();
   245 		printTableBorder("╰");
   246 		for (int i = 1; i <= columnCount; i++) {
   247 			if (i > 1) {
   248 				printTableBorder("┴");
   249 			}
   250 			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   251 		}
   252 		printTableBorder("╯");
   253 		out.println();
   254 
   255 		cleanColumnWidths();
   256 
   257 		out.print(TerminalColor.Yellow, "Record count: ");
   258 		out.println(getCurrentRowCount());
   259 		out.bell();
   260 		out.flush();
   261 	}
   262 
   263 	@Override
   264 	public void writeUpdatesResult(int updatedRowsCount) {
   265 		super.writeUpdatesResult(updatedRowsCount);
   266 		printResultSeparator();
   267 		out.print(TerminalColor.Red, "Updated records: ");
   268 		out.println(updatedRowsCount);
   269 		out.bell();
   270 		out.flush();
   271 	}
   272 
   273 	@Override
   274 	public void writeEndDatabase() {
   275 		super.writeEndDatabase();
   276 		out.flush();
   277 	}
   278 
   279 	private void printResultSeparator() {
   280 		if (firstResult) {
   281 			firstResult = false;
   282 		} else {
   283 			out.println();
   284 		}
   285 	}
   286 
   287 	protected void printTableBorder(String border) {
   288 		if (asciiNostalgia) {
   289 			border = border.replaceAll("─", "-");
   290 			border = border.replaceAll("│", "|");
   291 			border = border.replaceAll("[╭┬╮├┼┤╰┴╯]", "+");
   292 		}
   293 
   294 		out.print(TerminalColor.Green, border);
   295 	}
   296 
   297 	protected void printTableIndent() {
   298 		out.print(" ");
   299 	}
   300 
   301 	/**
   302 	 * @return whether should print only ASCII characters instead of unlimited Unicode.
   303 	 */
   304 	protected boolean isAsciiNostalgia() {
   305 		return asciiNostalgia;
   306 	}
   307 }