java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java
author František Kučera <franta-hg@frantovo.cz>
Sun, 04 Feb 2024 16:10:37 +0100
branchv_0
changeset 255 099bb96f8d8d
parent 250 aae5009bd0af
permissions -rw-r--r--
tabular formatter: new option 'separateBy' to print horizontal separator on each change of given column
     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.ArrayList;
    31 import java.util.List;
    32 import java.util.Objects;
    33 import java.util.logging.Level;
    34 import java.util.logging.Logger;
    35 import java.util.regex.Pattern;
    36 
    37 /**
    38  * <p>
    39  * Prints human-readable output – tables of result sets and text messages with update counts.
    40  * </p>
    41  *
    42  * <p>
    43  * Longer values might break the table – overflow the cells – see alternative tabular formatters and
    44  * the {@linkplain #PROPERTY_TRIM} property.
    45  * </p>
    46  *
    47  * @author Ing. František Kučera (frantovo.cz)
    48  * @see TabularPrefetchingFormatter
    49  * @see TabularWrappingFormatter
    50  */
    51 @PropertyDeclaration(name = COLORFUL, defaultValue = "true", type = Boolean.class, description = COLORFUL_DESCRIPTION)
    52 @PropertyDeclaration(name = TabularFormatter.PROPERTY_ASCII, defaultValue = "false", type = Boolean.class, description = "whether to use ASCII table borders instead of unicode ones")
    53 @PropertyDeclaration(name = TabularFormatter.PROPERTY_TRIM, defaultValue = "false", type = Boolean.class, description = "whether to trim the values to fit the column width")
    54 @PropertyDeclaration(name = TabularFormatter.PROPERTY_HEADER_TYPE, defaultValue = "true", type = Boolean.class, description = "whether to print data types in column headers")
    55 @PropertyDeclaration(name = TabularFormatter.PROPERTY_SEPARATE_BY, defaultValue = "", type = String.class, description = "colum(s) whose change triggers separator printing")
    56 public class TabularFormatter extends AbstractFormatter {
    57 
    58 	private static final Logger log = Logger.getLogger(TabularFormatter.class.getName());
    59 	public static final String NAME = "tabular"; // bash-completion:formatter
    60 	private static final String HEADER_TYPE_PREFIX = " (";
    61 	private static final String HEADER_TYPE_SUFFIX = ")";
    62 	public static final String PROPERTY_ASCII = "ascii";
    63 	public static final String PROPERTY_TRIM = "trim";
    64 	public static final String PROPERTY_HEADER_TYPE = "headerTypes";
    65 	public static final String PROPERTY_SEPARATE_BY = "separateBy";
    66 	protected ColorfulPrintWriter out;
    67 	private boolean firstResult = true;
    68 	private int[] columnWidth;
    69 	/**
    70 	 * use ASCII borders instead of unicode ones
    71 	 */
    72 	private final boolean asciiNostalgia;
    73 	/**
    74 	 * Trim values if they are longer than cell size
    75 	 */
    76 	private final boolean trimValues;
    77 	/**
    78 	 * Print data type of each column in the header
    79 	 */
    80 	private final boolean printHeaderTypes;
    81 	/**
    82 	 * When values of columns (names specified by this pattern) changes
    83 	 * between two consecutive rows, horizontal separator is printed.
    84 	 */
    85 	private final Pattern separateBy;
    86 	/**
    87 	 * Indexes of columns that matches {@linkplain #separateBy}
    88 	 */
    89 	private final List<Boolean> separators = new ArrayList<>();
    90 	/**
    91 	 * Internal counter for buffered data.
    92 	 */
    93 	private int currentColumnsCount = 0;
    94 	/**
    95 	 * Buffered data to be compared and printed later.
    96 	 */
    97 	private final List<Object> currentRow = new ArrayList<>();
    98 	/**
    99 	 * Buffered data to be compared later.
   100 	 */
   101 	private final List<Object> previousRow = new ArrayList<>();
   102 
   103 	public TabularFormatter(FormatterContext formatterContext) {
   104 		super(formatterContext);
   105 		out = new ColorfulPrintWriter(formatterContext.getOutputStream());
   106 		asciiNostalgia = formatterContext.getProperties().getBoolean(PROPERTY_ASCII, false);
   107 		trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false);
   108 		printHeaderTypes = formatterContext.getProperties().getBoolean(PROPERTY_HEADER_TYPE, true);
   109 		out.setColorful(formatterContext.getProperties().getBoolean(COLORFUL, true));
   110 		separateBy = formatterContext.getProperties().getPattern(PROPERTY_SEPARATE_BY, null);
   111 	}
   112 
   113 	@Override
   114 	public void writeStartResultSet(ColumnsHeader header) {
   115 		super.writeStartResultSet(header);
   116 		printResultSeparator();
   117 
   118 		initColumnWidths(header.getColumnCount());
   119 		currentRow.clear();
   120 		previousRow.clear();
   121 		separators.clear();
   122 
   123 		printTableIndent();
   124 		printTableBorder("╭");
   125 
   126 		List<ColumnDescriptor> columnDescriptors = header.getColumnDescriptors();
   127 
   128 		if (separateBy != null) {
   129 			for (ColumnDescriptor cd : columnDescriptors) {
   130 				separators.add(separateBy.matcher(cd.getLabel()).matches());
   131 			}
   132 		}
   133 
   134 		for (ColumnDescriptor cd : columnDescriptors) {
   135 			// padding: make header cell at least same width as data cells in this column
   136 			int typeWidth = printHeaderTypes ? cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length() : 0;
   137 			cd.setLabel(rpad(cd.getLabel(), getColumnWidth(cd.getColumnNumber()) - typeWidth));
   138 			updateColumnWidth(cd.getColumnNumber(), cd.getLabel().length() + typeWidth);
   139 
   140 			if (!cd.isFirstColumn()) {
   141 				printTableBorder("┬");
   142 			}
   143 			printTableBorder(repeat('─', getColumnWidth(cd.getColumnNumber()) + 2));
   144 		}
   145 		printTableBorder("╮");
   146 		out.println();
   147 
   148 		for (ColumnDescriptor cd : columnDescriptors) {
   149 			if (cd.isFirstColumn()) {
   150 				printTableIndent();
   151 				printTableBorder("│ ");
   152 			} else {
   153 				printTableBorder(" │ ");
   154 			}
   155 			out.print(TerminalStyle.Bright, cd.getLabel());
   156 			if (printHeaderTypes) {
   157 				out.print(HEADER_TYPE_PREFIX);
   158 				out.print(cd.getTypeName());
   159 				out.print(HEADER_TYPE_SUFFIX);
   160 			}
   161 			if (cd.isLastColumn()) {
   162 				printTableBorder(" │");
   163 			}
   164 		}
   165 		out.println();
   166 
   167 		printTableIndent();
   168 		printTableBorder("├");
   169 		for (int i = 1; i <= header.getColumnCount(); i++) {
   170 			if (i > 1) {
   171 				printTableBorder("┼");
   172 			}
   173 			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   174 		}
   175 		printTableBorder("┤");
   176 		out.println();
   177 
   178 		out.flush();
   179 	}
   180 
   181 	/**
   182 	 * Must be called before {@linkplain #updateColumnWidth(int, int)} and
   183 	 * {@linkplain #getColumnWidth(int)} for each result set.
   184 	 *
   185 	 * @param columnCount number of columns in current result set
   186 	 */
   187 	protected void initColumnWidths(int columnCount) {
   188 		if (columnWidth == null) {
   189 			columnWidth = new int[columnCount];
   190 		}
   191 	}
   192 
   193 	protected void cleanColumnWidths() {
   194 		columnWidth = null;
   195 	}
   196 
   197 	@Override
   198 	public void writeColumnValue(Object value) {
   199 		if (separateBy == null) super.writeColumnValue(value);
   200 		writeColumnValueInternal(value);
   201 	}
   202 
   203 	protected void writeColumnValueInternal(Object value) {
   204 		if (separateBy == null) {
   205 			printColumnValue(value);
   206 		} else {
   207 			currentRow.add(value);
   208 			currentColumnsCount++;
   209 			int columnsCount = getCurrentColumnsHeader().getColumnCount();
   210 			if (currentColumnsCount % columnsCount == 0) {
   211 				if (!previousRow.isEmpty()) {
   212 					boolean hasChanges = false;
   213 					for (int i = 0; i < previousRow.size(); i++) {
   214 						Object previous = previousRow.get(i);
   215 						Object current = currentRow.get(i);
   216 						boolean isSepating = separators.get(i);
   217 						if (isSepating && !Objects.equals(previous, current)) {
   218 							hasChanges = true;
   219 							break;
   220 						}
   221 					}
   222 
   223 					if (hasChanges) {
   224 						printRecordSeparator();
   225 					}
   226 				}
   227 
   228 				for (Object o : currentRow) {
   229 					super.writeColumnValue(value);
   230 					printColumnValue(o);
   231 				}
   232 
   233 				previousRow.clear();
   234 				previousRow.addAll(currentRow);
   235 				currentRow.clear();
   236 				currentColumnsCount = 0;
   237 			}
   238 		}
   239 	}
   240 
   241 	protected void printColumnValue(Object value) {
   242 
   243 		if (isCurrentColumnFirst()) {
   244 			printTableIndent();
   245 			printTableBorder("│ ");
   246 		} else {
   247 			printTableBorder(" │ ");
   248 		}
   249 
   250 		printValueWithWhitespaceReplaced(toString(value));
   251 
   252 		if (isCurrentColumnLast()) {
   253 			printTableBorder(" │");
   254 		}
   255 
   256 	}
   257 
   258 	protected void printRecordSeparator() {
   259 		int columnCount = getCurrentColumnsHeader().getColumnCount();
   260 		printTableIndent();
   261 		printTableBorder("├");
   262 		for (int i = 1; i <= columnCount; i++) {
   263 			if (i > 1) {
   264 				printTableBorder("┼");
   265 			}
   266 			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   267 		}
   268 		printTableBorder("┤");
   269 		out.println();
   270 	}
   271 
   272 	protected void printValueWithWhitespaceReplaced(String text) {
   273 		Functions.printValueWithWhitespaceReplaced(out, text, TerminalColor.Cyan, TerminalColor.Red);
   274 	}
   275 
   276 	protected int getColumnWidth(int columnNumber) {
   277 		return columnWidth[columnNumber - 1];
   278 	}
   279 
   280 	private void setColumnWidth(int columnNumber, int width) {
   281 		columnWidth[columnNumber - 1] = width;
   282 	}
   283 
   284 	protected void updateColumnWidth(int columnNumber, int width) {
   285 		int oldWidth = getColumnWidth(columnNumber);
   286 		setColumnWidth(columnNumber, Math.max(width, oldWidth));
   287 
   288 	}
   289 
   290 	protected String toString(Object value) {
   291 		final int width = getColumnWidth(getCurrentColumnsCount());
   292 		String result;
   293 		if (value instanceof Number || value instanceof Boolean) {
   294 			result = lpad(String.valueOf(value), width);
   295 		} else {
   296 			if (value instanceof SQLXML) {
   297 				// TODO: move to a common method, share with other formatters
   298 				try {
   299 					value = ((SQLXML) value).getString();
   300 				} catch (SQLException e) {
   301 					log.log(Level.SEVERE, "Unable to format XML", e);
   302 				}
   303 			}
   304 
   305 			result = rpad(String.valueOf(value), width);
   306 		}
   307 		// ?	value = (boolean) value ? "✔" : "✗";
   308 
   309 		if (trimValues && result.length() > width) {
   310 			result = result.substring(0, width - 1) + "…";
   311 		}
   312 
   313 		return result;
   314 	}
   315 
   316 	@Override
   317 	public void writeStartRow() {
   318 		super.writeStartRow();
   319 		currentColumnsCount = 0;
   320 	}
   321 
   322 	@Override
   323 	public void writeEndRow() {
   324 		super.writeEndRow();
   325 		writeEndRowInternal();
   326 	}
   327 
   328 	public void writeEndRowInternal() {
   329 		out.println();
   330 		out.flush();
   331 	}
   332 
   333 	@Override
   334 	public void writeEndResultSet() {
   335 		int columnCount = getCurrentColumnsHeader().getColumnCount();
   336 		super.writeEndResultSet();
   337 
   338 		printTableIndent();
   339 		printTableBorder("╰");
   340 		for (int i = 1; i <= columnCount; i++) {
   341 			if (i > 1) {
   342 				printTableBorder("┴");
   343 			}
   344 			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   345 		}
   346 		printTableBorder("╯");
   347 		out.println();
   348 
   349 		cleanColumnWidths();
   350 
   351 		out.print(TerminalColor.Yellow, "Record count: ");
   352 		out.println(getCurrentRowCount());
   353 		out.bell();
   354 		out.flush();
   355 	}
   356 
   357 	@Override
   358 	public void writeUpdatesResult(int updatedRowsCount) {
   359 		super.writeUpdatesResult(updatedRowsCount);
   360 		printResultSeparator();
   361 		out.print(TerminalColor.Red, "Updated records: ");
   362 		out.println(updatedRowsCount);
   363 		out.bell();
   364 		out.flush();
   365 	}
   366 
   367 	@Override
   368 	public void writeEndDatabase() {
   369 		super.writeEndDatabase();
   370 		out.flush();
   371 	}
   372 
   373 	private void printResultSeparator() {
   374 		if (firstResult) {
   375 			firstResult = false;
   376 		} else {
   377 			out.println();
   378 		}
   379 	}
   380 
   381 	protected void printTableBorder(String border) {
   382 		if (asciiNostalgia) {
   383 			border = border.replaceAll("─", "-");
   384 			border = border.replaceAll("│", "|");
   385 			border = border.replaceAll("[╭┬╮├┼┤╰┴╯]", "+");
   386 		}
   387 
   388 		out.print(TerminalColor.Green, border);
   389 	}
   390 
   391 	protected void printTableIndent() {
   392 		out.print(" ");
   393 	}
   394 
   395 	/**
   396 	 * @return whether should print only ASCII characters instead of unlimited Unicode.
   397 	 */
   398 	protected boolean isAsciiNostalgia() {
   399 		return asciiNostalgia;
   400 	}
   401 }