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