3 * Copyright © 2013 František Kučera (frantovo.cz)
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, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 package info.globalcode.sql.dk.formatting;
20 import info.globalcode.sql.dk.ColorfulPrintWriter;
21 import static info.globalcode.sql.dk.ColorfulPrintWriter.*;
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 java.util.List;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
31 * Prints human-readable output – tables of result sets and text messages with update counts.
35 * Longer values might break the table – overflow the cells – see alternative tabular formatters
36 * and the {@linkplain #PROPERTY_TRIM} property.
39 * @author Ing. František Kučera (frantovo.cz)
40 * @see TabularPrefetchingFormatter
41 * @see TabularWrappingFormatter
43 public class TabularFormatter extends AbstractFormatter {
45 public static final String NAME = "tabular"; // bash-completion:formatter
46 private static final String HEADER_TYPE_PREFIX = " (";
47 private static final String HEADER_TYPE_SUFFIX = ")";
48 public static final String PROPERTY_ASCII = "ascii";
49 public static final String PROPERTY_COLORFUL = "color";
50 public static final String PROPERTY_TRIM = "trim";
51 private static final String NBSP = " ";
52 private static final Pattern whitespaceToReplace = Pattern.compile("\\n|\\r|\\t|" + NBSP);
53 protected ColorfulPrintWriter out;
54 private boolean firstResult = true;
55 private int[] columnWidth;
57 * use ASCII borders instead of unicode ones
59 private final boolean asciiNostalgia;
61 * Trim values if they are longer than cell size
63 private final boolean trimValues;
65 public TabularFormatter(FormatterContext formatterContext) {
66 super(formatterContext);
67 out = new ColorfulPrintWriter(formatterContext.getOutputStream());
68 asciiNostalgia = formatterContext.getProperties().getBoolean(PROPERTY_ASCII, false);
69 trimValues = formatterContext.getProperties().getBoolean(PROPERTY_TRIM, false);
70 out.setColorful(formatterContext.getProperties().getBoolean(PROPERTY_COLORFUL, true));
74 public void writeStartResultSet(ColumnsHeader header) {
75 super.writeStartResultSet(header);
76 printResultSeparator();
78 initColumnWidths(header.getColumnCount());
81 printTableBorder("╭");
83 List<ColumnDescriptor> columnDescriptors = header.getColumnDescriptors();
85 for (ColumnDescriptor cd : columnDescriptors) {
86 // padding: make header cell at least same width as data cells in this column
87 int typeWidth = cd.getTypeName().length() + HEADER_TYPE_PREFIX.length() + HEADER_TYPE_SUFFIX.length();
88 cd.setLabel(rpad(cd.getLabel(), getColumnWidth(cd.getColumnNumber()) - typeWidth));
89 updateColumnWidth(cd.getColumnNumber(), cd.getLabel().length() + typeWidth);
91 if (!cd.isFirstColumn()) {
92 printTableBorder("┬");
94 printTableBorder(repeat('─', getColumnWidth(cd.getColumnNumber()) + 2));
96 printTableBorder("╮");
99 for (ColumnDescriptor cd : columnDescriptors) {
100 if (cd.isFirstColumn()) {
102 printTableBorder("│ ");
104 printTableBorder(" │ ");
106 out.print(TerminalStyle.Bright, cd.getLabel());
107 out.print(HEADER_TYPE_PREFIX);
108 out.print(cd.getTypeName());
109 out.print(HEADER_TYPE_SUFFIX);
110 if (cd.isLastColumn()) {
111 printTableBorder(" │");
117 printTableBorder("├");
118 for (int i = 1; i <= header.getColumnCount(); i++) {
120 printTableBorder("┼");
122 printTableBorder(repeat('─', getColumnWidth(i) + 2));
124 printTableBorder("┤");
131 * Must be called before
132 * {@linkplain #updateColumnWidth(int, int)}
133 * and {@linkplain #getColumnWidth(int)}
134 * for each result set.
136 * @param columnCount number of columns in current result set
138 protected void initColumnWidths(int columnCount) {
139 if (columnWidth == null) {
140 columnWidth = new int[columnCount];
144 protected void cleanColumnWidths() {
149 public void writeColumnValue(Object value) {
150 super.writeColumnValue(value);
151 writeColumnValueInternal(value);
154 protected void writeColumnValueInternal(Object value) {
156 if (isCurrentColumnFirst()) {
158 printTableBorder("│ ");
160 printTableBorder(" │ ");
163 String valueString = toString(value);
164 printValueWithWhitespaceReplaced(valueString);
166 if (isCurrentColumnLast()) {
167 printTableBorder(" │");
172 protected int getColumnWidth(int columnNumber) {
173 return columnWidth[columnNumber - 1];
176 private void setColumnWidth(int columnNumber, int width) {
177 columnWidth[columnNumber - 1] = width;
180 protected void updateColumnWidth(int columnNumber, int width) {
181 int oldWidth = getColumnWidth(columnNumber);
182 setColumnWidth(columnNumber, Math.max(width, oldWidth));
186 protected String toString(Object value) {
187 final int width = getColumnWidth(getCurrentColumnsCount());
189 if (value instanceof Number || value instanceof Boolean) {
190 result = lpad(String.valueOf(value), width);
192 result = rpad(String.valueOf(value), width);
194 // ? value = (boolean) value ? "✔" : "✗";
196 if (trimValues && result.length() > width) {
197 result = result.substring(0, width - 1) + "…";
204 public void writeEndRow() {
206 writeEndRowInternal();
209 public void writeEndRowInternal() {
215 public void writeEndResultSet() {
216 int columnCount = getCurrentColumnsHeader().getColumnCount();
217 super.writeEndResultSet();
220 printTableBorder("╰");
221 for (int i = 1; i <= columnCount; i++) {
223 printTableBorder("┴");
225 printTableBorder(repeat('─', getColumnWidth(i) + 2));
227 printTableBorder("╯");
232 out.print(TerminalColor.Yellow, "Record count: ");
233 out.println(getCurrentRowCount());
239 public void writeUpdatesResult(int updatedRowsCount) {
240 super.writeUpdatesResult(updatedRowsCount);
241 printResultSeparator();
242 out.print(TerminalColor.Red, "Updated records: ");
243 out.println(updatedRowsCount);
249 public void writeEndDatabase() {
250 super.writeEndDatabase();
254 private void printResultSeparator() {
262 protected void printTableBorder(String border) {
263 if (asciiNostalgia) {
264 border = border.replaceAll("─", "-");
265 border = border.replaceAll("│", "|");
266 border = border.replaceAll("[╭┬╮├┼┤╰┴╯]", "+");
269 out.print(TerminalColor.Green, border);
272 protected void printTableIndent() {
276 protected void printValueWithWhitespaceReplaced(String valueString) {
278 Matcher m = whitespaceToReplace.matcher(valueString);
281 while (m.find(start)) {
283 out.print(TerminalColor.Cyan, valueString.substring(start, m.start()));
287 out.print(TerminalColor.Red, "↲");
290 out.print(TerminalColor.Red, "⏎");
293 out.print(TerminalColor.Red, "↹");
296 out.print(TerminalColor.Red, "⎵");
299 throw new IllegalStateException("Unexpected whitespace token: „" + m.group() + "“");
305 out.print(TerminalColor.Cyan, valueString.substring(start, valueString.length()));