java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java
branchv_0
changeset 255 099bb96f8d8d
parent 250 aae5009bd0af
     1.1 --- a/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java	Fri Sep 23 18:05:50 2022 +0200
     1.2 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/TabularFormatter.java	Sun Feb 04 16:10:37 2024 +0100
     1.3 @@ -27,9 +27,12 @@
     1.4  import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION;
     1.5  import java.sql.SQLException;
     1.6  import java.sql.SQLXML;
     1.7 +import java.util.ArrayList;
     1.8  import java.util.List;
     1.9 +import java.util.Objects;
    1.10  import java.util.logging.Level;
    1.11  import java.util.logging.Logger;
    1.12 +import java.util.regex.Pattern;
    1.13  
    1.14  /**
    1.15   * <p>
    1.16 @@ -49,6 +52,7 @@
    1.17  @PropertyDeclaration(name = TabularFormatter.PROPERTY_ASCII, defaultValue = "false", type = Boolean.class, description = "whether to use ASCII table borders instead of unicode ones")
    1.18  @PropertyDeclaration(name = TabularFormatter.PROPERTY_TRIM, defaultValue = "false", type = Boolean.class, description = "whether to trim the values to fit the column width")
    1.19  @PropertyDeclaration(name = TabularFormatter.PROPERTY_HEADER_TYPE, defaultValue = "true", type = Boolean.class, description = "whether to print data types in column headers")
    1.20 +@PropertyDeclaration(name = TabularFormatter.PROPERTY_SEPARATE_BY, defaultValue = "", type = String.class, description = "colum(s) whose change triggers separator printing")
    1.21  public class TabularFormatter extends AbstractFormatter {
    1.22  
    1.23  	private static final Logger log = Logger.getLogger(TabularFormatter.class.getName());
    1.24 @@ -58,6 +62,7 @@
    1.25  	public static final String PROPERTY_ASCII = "ascii";
    1.26  	public static final String PROPERTY_TRIM = "trim";
    1.27  	public static final String PROPERTY_HEADER_TYPE = "headerTypes";
    1.28 +	public static final String PROPERTY_SEPARATE_BY = "separateBy";
    1.29  	protected ColorfulPrintWriter out;
    1.30  	private boolean firstResult = true;
    1.31  	private int[] columnWidth;
    1.32 @@ -73,6 +78,27 @@
    1.33  	 * Print data type of each column in the header
    1.34  	 */
    1.35  	private final boolean printHeaderTypes;
    1.36 +	/**
    1.37 +	 * When values of columns (names specified by this pattern) changes
    1.38 +	 * between two consecutive rows, horizontal separator is printed.
    1.39 +	 */
    1.40 +	private final Pattern separateBy;
    1.41 +	/**
    1.42 +	 * Indexes of columns that matches {@linkplain #separateBy}
    1.43 +	 */
    1.44 +	private final List<Boolean> separators = new ArrayList<>();
    1.45 +	/**
    1.46 +	 * Internal counter for buffered data.
    1.47 +	 */
    1.48 +	private int currentColumnsCount = 0;
    1.49 +	/**
    1.50 +	 * Buffered data to be compared and printed later.
    1.51 +	 */
    1.52 +	private final List<Object> currentRow = new ArrayList<>();
    1.53 +	/**
    1.54 +	 * Buffered data to be compared later.
    1.55 +	 */
    1.56 +	private final List<Object> previousRow = new ArrayList<>();
    1.57  
    1.58  	public TabularFormatter(FormatterContext formatterContext) {
    1.59  		super(formatterContext);
    1.60 @@ -81,6 +107,7 @@
    1.61  		trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false);
    1.62  		printHeaderTypes = formatterContext.getProperties().getBoolean(PROPERTY_HEADER_TYPE, true);
    1.63  		out.setColorful(formatterContext.getProperties().getBoolean(COLORFUL, true));
    1.64 +		separateBy = formatterContext.getProperties().getPattern(PROPERTY_SEPARATE_BY, null);
    1.65  	}
    1.66  
    1.67  	@Override
    1.68 @@ -89,12 +116,21 @@
    1.69  		printResultSeparator();
    1.70  
    1.71  		initColumnWidths(header.getColumnCount());
    1.72 +		currentRow.clear();
    1.73 +		previousRow.clear();
    1.74 +		separators.clear();
    1.75  
    1.76  		printTableIndent();
    1.77  		printTableBorder("╭");
    1.78  
    1.79  		List<ColumnDescriptor> columnDescriptors = header.getColumnDescriptors();
    1.80  
    1.81 +		if (separateBy != null) {
    1.82 +			for (ColumnDescriptor cd : columnDescriptors) {
    1.83 +				separators.add(separateBy.matcher(cd.getLabel()).matches());
    1.84 +			}
    1.85 +		}
    1.86 +
    1.87  		for (ColumnDescriptor cd : columnDescriptors) {
    1.88  			// padding: make header cell at least same width as data cells in this column
    1.89  			int typeWidth = printHeaderTypes ? cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length() : 0;
    1.90 @@ -160,11 +196,49 @@
    1.91  
    1.92  	@Override
    1.93  	public void writeColumnValue(Object value) {
    1.94 -		super.writeColumnValue(value);
    1.95 +		if (separateBy == null) super.writeColumnValue(value);
    1.96  		writeColumnValueInternal(value);
    1.97  	}
    1.98  
    1.99  	protected void writeColumnValueInternal(Object value) {
   1.100 +		if (separateBy == null) {
   1.101 +			printColumnValue(value);
   1.102 +		} else {
   1.103 +			currentRow.add(value);
   1.104 +			currentColumnsCount++;
   1.105 +			int columnsCount = getCurrentColumnsHeader().getColumnCount();
   1.106 +			if (currentColumnsCount % columnsCount == 0) {
   1.107 +				if (!previousRow.isEmpty()) {
   1.108 +					boolean hasChanges = false;
   1.109 +					for (int i = 0; i < previousRow.size(); i++) {
   1.110 +						Object previous = previousRow.get(i);
   1.111 +						Object current = currentRow.get(i);
   1.112 +						boolean isSepating = separators.get(i);
   1.113 +						if (isSepating && !Objects.equals(previous, current)) {
   1.114 +							hasChanges = true;
   1.115 +							break;
   1.116 +						}
   1.117 +					}
   1.118 +
   1.119 +					if (hasChanges) {
   1.120 +						printRecordSeparator();
   1.121 +					}
   1.122 +				}
   1.123 +
   1.124 +				for (Object o : currentRow) {
   1.125 +					super.writeColumnValue(value);
   1.126 +					printColumnValue(o);
   1.127 +				}
   1.128 +
   1.129 +				previousRow.clear();
   1.130 +				previousRow.addAll(currentRow);
   1.131 +				currentRow.clear();
   1.132 +				currentColumnsCount = 0;
   1.133 +			}
   1.134 +		}
   1.135 +	}
   1.136 +
   1.137 +	protected void printColumnValue(Object value) {
   1.138  
   1.139  		if (isCurrentColumnFirst()) {
   1.140  			printTableIndent();
   1.141 @@ -181,6 +255,20 @@
   1.142  
   1.143  	}
   1.144  
   1.145 +	protected void printRecordSeparator() {
   1.146 +		int columnCount = getCurrentColumnsHeader().getColumnCount();
   1.147 +		printTableIndent();
   1.148 +		printTableBorder("├");
   1.149 +		for (int i = 1; i <= columnCount; i++) {
   1.150 +			if (i > 1) {
   1.151 +				printTableBorder("┼");
   1.152 +			}
   1.153 +			printTableBorder(repeat('─', getColumnWidth(i) + 2));
   1.154 +		}
   1.155 +		printTableBorder("┤");
   1.156 +		out.println();
   1.157 +	}
   1.158 +
   1.159  	protected void printValueWithWhitespaceReplaced(String text) {
   1.160  		Functions.printValueWithWhitespaceReplaced(out, text, TerminalColor.Cyan, TerminalColor.Red);
   1.161  	}
   1.162 @@ -226,6 +314,12 @@
   1.163  	}
   1.164  
   1.165  	@Override
   1.166 +	public void writeStartRow() {
   1.167 +		super.writeStartRow();
   1.168 +		currentColumnsCount = 0;
   1.169 +	}
   1.170 +
   1.171 +	@Override
   1.172  	public void writeEndRow() {
   1.173  		super.writeEndRow();
   1.174  		writeEndRowInternal();