@@ -69,6 +70,11 @@
spring-shell-starter
${project.version}
+
+ org.springframework.shell
+ spring-shell-table
+ ${project.version}
+
org.jline
diff --git a/spring-shell-core/pom.xml b/spring-shell-core/pom.xml
index 15b7a225..10ee7954 100644
--- a/spring-shell-core/pom.xml
+++ b/spring-shell-core/pom.xml
@@ -23,11 +23,6 @@
org.springframework.boot
spring-boot-starter-validation
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
org.jline
jline
@@ -36,6 +31,19 @@
org.jline
jline-terminal-jna
+
+
+ org.springframework.shell
+ spring-shell-table
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
org.assertj
assertj-core
diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java
index d1341a1d..86b6b8eb 100644
--- a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java
+++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java
@@ -19,9 +19,11 @@ package org.springframework.shell.result;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.ResultHandler;
+import org.springframework.shell.TerminalSizeAware;
/**
* Used for explicit configuration of {@link org.springframework.shell.ResultHandler}s.
@@ -47,4 +49,10 @@ public class ResultHandlerConfig {
iterableResultHandler().setDelegate(mainResultHandler());
}
+ @Bean
+ @ConditionalOnClass(TerminalSizeAware.class)
+ public TerminalSizeAwareResultHandler terminalSizeAwareResultHandler() {
+ return new TerminalSizeAwareResultHandler();
+ }
+
}
diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/TerminalSizeAwareResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/TerminalSizeAwareResultHandler.java
new file mode 100644
index 00000000..8fa9679a
--- /dev/null
+++ b/spring-shell-core/src/main/java/org/springframework/shell/result/TerminalSizeAwareResultHandler.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.result;
+
+import org.springframework.shell.ResultHandler;
+import org.springframework.shell.TerminalSizeAware;
+
+/**
+ * A ResultHandler that prints {@link TerminalSizeAware} according to the {@link org.jline.terminal.Terminal} size.
+ *
+ * @author Eric Bottard
+ */
+public class TerminalSizeAwareResultHandler extends TerminalAwareResultHandler implements ResultHandler {
+
+
+ @Override
+ public void handleResult(TerminalSizeAware result) {
+ CharSequence toPrint = result.render(terminal.getWidth());
+ terminal.writer().println(toPrint);
+ }
+}
diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/TableCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/TableCommands.java
new file mode 100644
index 00000000..f0bdf227
--- /dev/null
+++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/TableCommands.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.samples.standard;
+
+import org.springframework.shell.standard.ShellComponent;
+import org.springframework.shell.standard.ShellMethod;
+import org.springframework.shell.table.*;
+
+import java.util.Random;
+
+@ShellComponent
+public class TableCommands {
+
+ private static final String TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
+ "ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
+ "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " +
+ "voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " +
+ "non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
+
+ @ShellMethod("Showcase Table rendering")
+ public Table table() {
+ String[][] data = new String[3][3];
+ TableModel model = new ArrayTableModel(data);
+ TableBuilder tableBuilder = new TableBuilder(model);
+
+ Random r = new Random();
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ data[i][j] = TEXT.substring(0, TEXT.length() / 2 + r.nextInt(TEXT.length() / 2));
+ tableBuilder.on(at(i, j)).addAligner(SimpleHorizontalAligner.values()[j]);
+ tableBuilder.on(at(i, j)).addAligner(SimpleVerticalAligner.values()[i]);
+ }
+ }
+
+ return tableBuilder.addFullBorder(BorderStyle.fancy_light).build();
+ }
+
+ public static CellMatcher at(final int theRow, final int col) {
+ return new CellMatcher() {
+ @Override
+ public boolean matches(int row, int column, TableModel model) {
+ return row == theRow && column == col;
+ }
+ };
+ }
+}
diff --git a/spring-shell-starter/pom.xml b/spring-shell-starter/pom.xml
index 9494ff65..32e41a3a 100644
--- a/spring-shell-starter/pom.xml
+++ b/spring-shell-starter/pom.xml
@@ -36,6 +36,10 @@
org.springframework.shell
spring-shell-jcommander-adapter
+
+ org.springframework.shell
+ spring-shell-table
+
diff --git a/spring-shell-table/pom.xml b/spring-shell-table/pom.xml
new file mode 100644
index 00000000..eea45b44
--- /dev/null
+++ b/spring-shell-table/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ 4.0.0
+
+ spring-shell-table
+ Spring Shell Table
+ jar
+
+
+ org.springframework.shell
+ spring-shell-parent
+ 2.0.0.BUILD-SNAPSHOT
+
+
+ Library to Display Fancy ASCII art Tables
+
+
+
+ org.springframework
+ spring-beans
+
+
+
+
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/TerminalSizeAware.java b/spring-shell-table/src/main/java/org/springframework/shell/TerminalSizeAware.java
new file mode 100644
index 00000000..00bf20db
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/TerminalSizeAware.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell;
+
+/**
+ * To be implemented by command result objects that can adapt to the terminal size when they are being rendered.
+ *
+ * An object which does not implement this interface will simply be rendered by invoking its {@link Object#toString()}
+ * method.
+ *
+ * @author Eric Bottard
+ */
+public interface TerminalSizeAware {
+
+ CharSequence render(int terminalWidth);
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/AbsoluteWidthSizeConstraints.java b/spring-shell-table/src/main/java/org/springframework/shell/table/AbsoluteWidthSizeConstraints.java
new file mode 100644
index 00000000..a32e7721
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/AbsoluteWidthSizeConstraints.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A cell sizing strategy that forces a fixed width, expressed in number of characters.
+ *
+ * @author Eric Bottard
+ */
+public class AbsoluteWidthSizeConstraints implements SizeConstraints {
+
+ private final int width;
+
+ public AbsoluteWidthSizeConstraints(int width) {
+ this.width = width;
+ }
+
+
+ @Override
+ public Extent width(String[] raw, int previous, int tableWidth) {
+ return new Extent(width, width);
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/Aligner.java b/spring-shell-table/src/main/java/org/springframework/shell/table/Aligner.java
new file mode 100644
index 00000000..e81bc547
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/Aligner.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A strategy interface for performing text alignment.
+ *
+ * @author Eric Bottard
+ */
+public interface Aligner {
+
+ /**
+ * Perform text alignment, returning a String array that MUST contain {@code cellHeight}
+ * lines, each of which MUST be {@code cellWidth} chars in length.
+ *
+ *
+ * Input array is guaranteed to contain lines that have length equal to {@code cellWidth}.
+ * There is no guarantee on the input number of lines though.
+ *
+ *
+ * @param text the text to align
+ * @param cellWidth the width of of the table cell
+ * @param cellHeight the height of the table cell
+ * @return the aligned text, in a {@code cellHeight} element array
+ */
+ String[] align(String[] text, int cellWidth, int cellHeight);
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java b/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java
new file mode 100644
index 00000000..1b7baf81
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import org.springframework.util.Assert;
+
+/**
+ * A TableModel backed by a row-first array.
+ *
+ * @author Eric Bottard
+ */
+public class ArrayTableModel extends TableModel {
+
+ private Object[][] data;
+
+ public ArrayTableModel(Object[][] data) {
+ this.data = data;
+ int width = data.length > 0 ? data[0].length : 0;
+ for (int row = 0; row < data.length; row++) {
+ Assert.isTrue(width == data[row].length, "All rows of array data must be of same length");
+ }
+ }
+
+ public int getRowCount() {
+ return data.length;
+ }
+
+ public int getColumnCount() {
+ return data.length > 0 ? data[0].length : 0;
+ }
+
+ public Object getValue(int row, int column) {
+ return data[row][column];
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/AutoSizeConstraints.java b/spring-shell-table/src/main/java/org/springframework/shell/table/AutoSizeConstraints.java
new file mode 100644
index 00000000..7789427f
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/AutoSizeConstraints.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A SizeConstraints implementation that splits lines at space boundaries
+ * and returns an extent with minimal and maximal width requirements.
+ *
+ * @author Eric Bottard
+ */
+public class AutoSizeConstraints implements SizeConstraints {
+
+ @Override
+ public Extent width(String[] raw, int tableWidth, int nbColumns) {
+ int max = 0;
+ int min = 0;
+ for (String line : raw) {
+ String[] words = line.split(" ");
+ for (String word : words) {
+ min = Math.max(min, word.length());
+ }
+ max = Math.max(max, line.length());
+ }
+ return new Extent(min, max);
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java b/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java
new file mode 100644
index 00000000..958a831e
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.beans.PropertyDescriptor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+
+/**
+ * A table model that is backed by a list of beans.
+ *
+ * One can control which properties are exposed (and their order). There is also
+ * a convenience constructor for adding a special header row.
+ *
+ * @author Eric Bottard
+ */
+public class BeanListTableModel extends TableModel {
+
+ private final List data;
+
+ private final List propertyNames;
+
+ private final List headerRow;
+
+ public BeanListTableModel(Class clazz, Iterable list) {
+ this.data = new ArrayList();
+ for (T bean : list) {
+ this.data.add(new BeanWrapperImpl(bean));
+ }
+ this.headerRow = null;
+ propertyNames = new ArrayList();
+ for (PropertyDescriptor propertyName : BeanUtils.getPropertyDescriptors(clazz)) {
+ if ("class".equals(propertyName.getName())) {
+ continue;
+ }
+ propertyNames.add(propertyName.getName());
+ }
+ }
+
+ public BeanListTableModel(Iterable list, String... propertyNames) {
+ this.data = new ArrayList();
+ for (T bean : list) {
+ this.data.add(new BeanWrapperImpl(bean));
+ }
+ this.headerRow = null;
+ this.propertyNames = Arrays.asList(propertyNames);
+ }
+
+ public BeanListTableModel(Iterable list, LinkedHashMap header) {
+ this.data = new ArrayList();
+ for (T bean : list) {
+ this.data.add(new BeanWrapperImpl(bean));
+ }
+ this.headerRow = new ArrayList(header.values());
+ propertyNames = new ArrayList(header.keySet());
+ }
+
+ @Override
+ public int getRowCount() {
+ return headerRow == null ? data.size() : 1 + data.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return propertyNames.size();
+ }
+
+ @Override
+ public Object getValue(int row, int column) {
+ if (headerRow != null && row == 0) {
+ return headerRow.get(column);
+ }
+ else {
+ int rowToUse = headerRow == null ? row : row - 1;
+ String propertyName = propertyNames.get(column);
+ return data.get(rowToUse).getPropertyValue(propertyName);
+ }
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java b/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java
new file mode 100644
index 00000000..f7227efc
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * This represents a directive to set some borders on cells of a table.
+ * Multiple specifications can be combined on a single table.
+ *
+ * @author Eric Bottard
+ */
+public class BorderSpecification {
+
+ public static final int NONE = 0;
+
+ public static final int TOP = 1;
+
+ public static final int BOTTOM = 2;
+
+ public static final int LEFT = 4;
+
+ public static final int RIGHT = 8;
+
+ public static final int INNER_VERTICAL = 16;
+
+ public static final int INNER_HORIZONTAL = 32;
+
+ public static final int OUTLINE = TOP | BOTTOM | LEFT | RIGHT;
+
+ public static final int FULL = OUTLINE | INNER_HORIZONTAL | INNER_VERTICAL;
+
+ public static final int INNER = INNER_HORIZONTAL | INNER_VERTICAL;
+
+ private final int row1, row2, column1, column2;
+
+ private final int match;
+
+ private final BorderStyle style;
+
+ /**
+ * Specifications are created by {@link Table#addBorder(int, int, int, int, int, BorderStyle)}.
+ */
+ /*default*/ BorderSpecification(int row1, int column1, int row2, int column2, int match, BorderStyle style) {
+ this.row1 = row1;
+ this.row2 = row2;
+ this.column1 = column1;
+ this.column2 = column2;
+ this.match = match;
+ this.style = style;
+ }
+
+ /**
+ * Does this specification result in the need to paint a vertical bar at row,column?
+ */
+ /*default*/ char verticals(int row, int column) {
+ boolean result = (match & LEFT) == LEFT && column == column1;
+ result |= (match & INNER_VERTICAL) == INNER_VERTICAL && column > column1 && column < column2;
+ result |= (match & RIGHT) == RIGHT && column == column2;
+
+ result &= row >= row1;
+ result &= row < row2;
+ return result ? style.verticalGlyph() : BorderStyle.NONE;
+ }
+
+ /**
+ * Does this specification result in the need to paint an horizontal bar at row,column?
+ */
+ /*default*/ char horizontals(int row, int column) {
+ boolean result = (match & TOP) == TOP && row == row1;
+ result |= (match & INNER_HORIZONTAL) == INNER_HORIZONTAL && row > row1 && row < row2;
+ result |= (match & BOTTOM) == BOTTOM && row == row2;
+
+ result &= column >= column1;
+ result &= column < column2;
+ return result ? style.horizontalGlyph() : BorderStyle.NONE;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s[(%d, %d)->(%d, %d), %s, %s]", getClass().getSimpleName(), row1, column1, row2, column2, style, matchConstants());
+ }
+
+ private String matchConstants() {
+ try {
+ for (String field : new String[] {"NONE", "INNER", "FULL", "OUTLINE"}) {
+ int value = ReflectionUtils.findField(getClass(), field).getInt(null);
+ if (match == value) {
+ return field;
+ }
+ }
+ List constants = new ArrayList();
+ for (String field : new String[] {"TOP", "BOTTOM", "LEFT", "RIGHT", "INNER_HORIZONTAL", "INNER_VERTICAL"}) {
+ int value = ReflectionUtils.findField(getClass(), field).getInt(null);
+ if ((match & value) == value) {
+ constants.add(field);
+ }
+ }
+ return StringUtils.collectionToDelimitedString(constants, "|");
+ }
+ catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/BorderStyle.java b/spring-shell-table/src/main/java/org/springframework/shell/table/BorderStyle.java
new file mode 100644
index 00000000..b6b69ea7
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/BorderStyle.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.shell.table;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides support for different styles of borders, using simple or fancy ascii art.
+ *
+ * @see https://en.wikipedia.org/wiki/Box-drawing_character
+ *
+ * @author Eric Bottard
+ */
+public enum BorderStyle {
+
+ /**
+ * A simplistic style, using characters that ought to always be available in all systems (pipe and minus).
+ */
+ oldschool('|', '-'),
+
+ /**
+ * A border style that uses dedicated light box drawing characters from the unicode set.
+ */
+ fancy_light('│', '─'),
+
+ /**
+ * A border style that uses dedicated fat box drawing characters from the unicode set.
+ */
+ fancy_heavy('┃', '━'),
+
+ /**
+ * A border style that uses dedicated double-light box drawing characters from the unicode set.
+ */
+ fancy_double('║', '═'),
+
+ /**
+ * A border style that uses space characters, giving some space between columns.
+ */
+ air(' ', ' '),
+
+ /**
+ * A border style that uses dedicated double dash light box drawing characters from the unicode set.
+ */
+ fancy_light_double_dash('╎', '╌'),
+
+ /**
+ * A border style that uses dedicated double dash light box drawing characters from the unicode set.
+ */
+ fancy_light_triple_dash('┆', '┄'),
+
+ /**
+ * A border style that uses dedicated double dash light box drawing characters from the unicode set.
+ */
+ fancy_light_quadruple_dash('┊', '┈'),
+
+ /**
+ * A border style that uses dedicated double dash heavy box drawing characters from the unicode set.
+ */
+ fancy_heavy_double_dash('╏', '╍'),
+
+ /**
+ * A border style that uses dedicated double dash heavy box drawing characters from the unicode set.
+ */
+ fancy_heavy_triple_dash('┇', '┅'),
+
+ /**
+ * A border style that uses dedicated double dash heavy box drawing characters from the unicode set.
+ */
+ fancy_heavy_quadruple_dash('┋', '┉'),
+
+ ;
+
+ private char vertical;
+
+ private char horizontal;
+
+ public static final char NONE = '\u0000';
+
+ private static Map CORNERS = new HashMap();
+
+ private static Map EQUIVALENTS = new HashMap();
+
+ public char verticalGlyph() {
+ return vertical;
+ }
+
+ public char horizontalGlyph() {
+ return horizontal;
+ }
+
+ static {
+ registerCorners("─│┌┐└┘├┤┬┴┼");
+ registerCorners("━┃┏┓┗┛┣┫┳┻╋");
+
+ // double dashes
+ registerCorners("╌╎┌┐└┘├┤┬┴┼");
+ registerCorners("╍╏┏┓┗┛┣┫┳┻╋");
+
+ // triple dashes
+ registerCorners("┈┆┌┐└┘├┤┬┴┼");
+ registerCorners("┅┇┏┓┗┛┣┫┳┻╋");
+
+ // quad dashes
+ registerCorners("┈┊┌┐└┘├┤┬┴┼");
+ registerCorners("┉┋┏┓┗┛┣┫┳┻╋");
+
+ // double lines
+ registerCorners("═║╔╗╚╝╠╣╦╩╬");
+ // oldschool
+ registerCorners("-|+++++++++");
+ // air style
+ registerCorners(" ");
+
+ // Register some mixed-style combinations
+ // light + heavy
+ registerCorner('│', '│', '━', NONE, '┥');
+ registerCorner('│', '│', NONE, '━', '┝');
+ registerCorner('┃', NONE, '─', '─', '┸');
+ registerCorner(NONE, '┃', '─', '─', '┰');
+ // heavy + light
+ registerCorner('┃', '┃', '─', NONE, '┨');
+ registerCorner('┃', '┃', NONE, '─', '┠');
+ registerCorner('│', NONE, '━', '━', '┷');
+ registerCorner(NONE, '│', '━', '━', '┯');
+ // double + single
+ registerCorner('║', '║', '─', NONE, '╢');
+ registerCorner('║', '║', NONE, '─', '╟');
+ registerCorner('│', NONE, '═', '═', '╧');
+ registerCorner(NONE, '│', '═', '═', '╤');
+ // single + double
+ registerCorner('│', '│', '═', NONE, '╡');
+ registerCorner('│', '│', NONE, '═', '╞');
+ registerCorner('║', NONE, '─', '─', '╨');
+ registerCorner(NONE, '║', '─', '─', '╥');
+ // heavy + light, 90°
+ registerCorner('┃', '│', '━', '─', '╃');
+ registerCorner('│', '┃', '─', '━', '╆');
+ registerCorner('┃', '│', '─', '━', '╄');
+ registerCorner('│', '┃', '━', '─', '╅');
+ // light crossing (heavy or double)
+ registerCorner('│', '│', '━', '━', '┿');
+ registerCorner('│', '│', '═', '═', '╪');
+ registerCorner('┃', '┃', '─', '─', '╂');
+ registerCorner('║', '║', '─', '─', '╫');
+
+ // Dashed variants crossing others behave like regular corners
+ registerSameCorners(fancy_light_double_dash, fancy_light);
+ registerSameCorners(fancy_light_triple_dash, fancy_light);
+ registerSameCorners(fancy_light_quadruple_dash, fancy_light);
+ registerSameCorners(fancy_heavy_double_dash, fancy_heavy);
+ registerSameCorners(fancy_heavy_triple_dash, fancy_heavy);
+ registerSameCorners(fancy_heavy_quadruple_dash, fancy_heavy);
+
+
+ // Air-style glyphs are easy to combine with others. Register some combinations
+ registerMixedWithAirCombinations(oldschool.vertical, oldschool.horizontal);
+ registerMixedWithAirCombinations(fancy_light.vertical, fancy_light.horizontal);
+ registerMixedWithAirCombinations(fancy_double.vertical, fancy_double.horizontal);
+ registerMixedWithAirCombinations(fancy_heavy.vertical, fancy_heavy.horizontal);
+
+ registerMixedWithAirCombinations(fancy_light_double_dash.vertical, fancy_light_double_dash.horizontal);
+ registerMixedWithAirCombinations(fancy_light_triple_dash.vertical, fancy_light_triple_dash.horizontal);
+ registerMixedWithAirCombinations(fancy_light_quadruple_dash.vertical, fancy_light_quadruple_dash.horizontal);
+ registerMixedWithAirCombinations(fancy_heavy_double_dash.vertical, fancy_heavy_double_dash.horizontal);
+ registerMixedWithAirCombinations(fancy_heavy_triple_dash.vertical, fancy_heavy_triple_dash.horizontal);
+ registerMixedWithAirCombinations(fancy_heavy_quadruple_dash.vertical, fancy_heavy_quadruple_dash.horizontal);
+ }
+
+ /**
+ * Register the fact that for corner purposes, style1 behaves like style2.
+ */
+ private static void registerSameCorners(BorderStyle style1, BorderStyle style2) {
+ EQUIVALENTS.put(style1.horizontal, style2.horizontal);
+ EQUIVALENTS.put(style1.vertical, style2.vertical);
+ }
+
+ private static void registerMixedWithAirCombinations(char vertical, char horizontal) {
+ registerCorner(vertical, vertical, ' ', NONE, vertical);
+ registerCorner(vertical, vertical, NONE, ' ', vertical);
+ registerCorner(vertical, vertical, ' ', ' ', vertical);
+ registerCorner(' ', NONE, horizontal, horizontal, horizontal);
+ registerCorner(NONE, ' ', horizontal, horizontal, horizontal);
+ registerCorner(' ', ' ', horizontal, horizontal, horizontal);
+
+ }
+
+ /**
+ * Register corner glyphs for a given set, not taking care of mixed style intersections.
+ */
+ private static void registerCorners(String list) {
+ char horizontal = list.charAt(0);
+ char vertical = list.charAt(1);
+ registerCorner(NONE, vertical, NONE, horizontal, list.charAt(2));
+ registerCorner(NONE, vertical, horizontal, NONE, list.charAt(3));
+ registerCorner(vertical, NONE, NONE, horizontal, list.charAt(4));
+ registerCorner(vertical, NONE, horizontal, NONE, list.charAt(5));
+ registerCorner(vertical, vertical, NONE, horizontal, list.charAt(6));
+ registerCorner(vertical, vertical, horizontal, NONE, list.charAt(7));
+ registerCorner(NONE, vertical, horizontal, horizontal, list.charAt(8));
+ registerCorner(vertical, NONE, horizontal, horizontal, list.charAt(9));
+ registerCorner(vertical, vertical, horizontal, horizontal, list.charAt(10));
+
+ }
+
+ private static void registerCorner(char above, char below, char left, char right, char corner) {
+ long key = key(above, below, left, right);
+ CORNERS.put(key, corner);
+ }
+
+ public static char intersection(char above, char below, char left, char right) {
+ above = EQUIVALENTS.get(above) != null ? EQUIVALENTS.get(above) : above;
+ below = EQUIVALENTS.get(below) != null ? EQUIVALENTS.get(below) : below;
+ left = EQUIVALENTS.get(left) != null ? EQUIVALENTS.get(left) : left;
+ right = EQUIVALENTS.get(right) != null ? EQUIVALENTS.get(right) : right;
+ Character character = CORNERS.get(key(above, below, left, right));
+ return character != null ? character : NONE;
+ }
+
+ private static long key(char above, char below, char left, char right) {
+ return (long) above << 48 | (long) below << 32 | (long) left << 16 | (long) right;
+ }
+
+ BorderStyle(char vertical, char horizontal) {
+ this.vertical = vertical;
+ this.horizontal = horizontal;
+ }
+
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/CellMatcher.java b/spring-shell-table/src/main/java/org/springframework/shell/table/CellMatcher.java
new file mode 100644
index 00000000..aee78ec3
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/CellMatcher.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * This is used to specify where some components of a Table may be applied.
+ *
+ * Some commonly used matchers can be created via {@link CellMatchers}.
+ *
+ * @author Eric Bottard
+ */
+public interface CellMatcher {
+
+ /**
+ * @return whether a given cell of the table should match.
+ * @param row the row being tested.
+ * @param column the column being tested
+ * @param model the data model of the table
+ */
+ public boolean matches(int row, int column, TableModel model);
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/CellMatchers.java b/spring-shell-table/src/main/java/org/springframework/shell/table/CellMatchers.java
new file mode 100644
index 00000000..d0228314
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/CellMatchers.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * Contains factory methods for commonly used {@link CellMatcher}s.
+ *
+ * @author Eric Bottard
+ */
+public class CellMatchers {
+
+ /**
+ * @return a matcher that applies to every cell of the table.
+ */
+ public static CellMatcher table() {
+ return new CellMatcher() {
+ public boolean matches(int row, int column, TableModel model) {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * @return a matcher that applies to every cell of some column of the table.
+ * @param col the column to select
+ */
+ public static CellMatcher column(final int col) {
+ return new CellMatcher() {
+ public boolean matches(int row, int column, TableModel model) {
+ return col == column;
+ }
+ };
+ }
+
+ /**
+ * @return a matcher that applies to every cell of some row of the table.
+ * @param theRow the row to select
+ */
+ public static CellMatcher row(final int theRow) {
+ return new CellMatcher() {
+ public boolean matches(int row, int column, TableModel model) {
+ return theRow == row;
+ }
+ };
+ }
+
+ /**
+ * @param theRow the row to select
+ * @param col the column to select
+ * @return a matcher that applies to exactly one cell of the table, identified by its row and colum.
+ */
+ public static CellMatcher at(final int theRow, final int col) {
+ return new CellMatcher() {
+ @Override
+ public boolean matches(int row, int column, TableModel model) {
+ return row == theRow && column == col;
+ }
+ };
+ }
+
+ /**
+ * @param clazz the type that cells should contain
+ * @return a matcher that matches cells whose content is of a certain type
+ */
+ public static CellMatcher ofType(final Class> clazz) {
+ return new CellMatcher() {
+ @Override
+ public boolean matches(int row, int column, TableModel model) {
+ Object value = model.getValue(row, column);
+ if (value == null) {
+ return false;
+ }
+ else {
+ return clazz.isAssignableFrom(value.getClass());
+ }
+ }
+ };
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/DebugAligner.java b/spring-shell-table/src/main/java/org/springframework/shell/table/DebugAligner.java
new file mode 100644
index 00000000..b1a9bd74
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/DebugAligner.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.Arrays;
+
+import org.springframework.util.Assert;
+
+/**
+ * A decorator Aligner that checks the Aligner invariants contract, useful for debugging.
+ *
+ * @author Eric Bottard
+ */
+public class DebugAligner implements Aligner {
+
+ private final Aligner delegate;
+
+ public DebugAligner(Aligner delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String[] align(String[] text, int cellWidth, int cellHeight) {
+ String[] result = delegate.align(text, cellWidth, cellHeight);
+ Assert.isTrue(result.length == cellHeight, String.format("%s had the wrong number of lines (%d), expected %d",
+ Arrays.asList(result), result.length, cellHeight));
+ for (String s : result) {
+ Assert.isTrue(s.length() == cellWidth, String.format("'%s' had wrong length (%d), expected %d", s, s.length(),
+ cellWidth));
+ }
+ return result;
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/DebugTextWrapper.java b/spring-shell-table/src/main/java/org/springframework/shell/table/DebugTextWrapper.java
new file mode 100644
index 00000000..1e105172
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/DebugTextWrapper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import org.springframework.util.Assert;
+
+/**
+ * A TextWrapper that delegates to another but makes sure that the contract is not violated.
+ *
+ * @author Eric Bottard
+ */
+public class DebugTextWrapper implements TextWrapper {
+
+ private final TextWrapper delegate;
+
+ public DebugTextWrapper(TextWrapper delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String[] wrap(String[] original, int columnWidth) {
+ String[] result = delegate.wrap(original, columnWidth);
+ for (String s : result) {
+ Assert.isTrue(s.length() == columnWidth, String.format("'%s' has the wrong length (%d), expected %d", s, s.length(), columnWidth));
+ }
+ return result;
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java b/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java
new file mode 100644
index 00000000..c11c711b
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A very simple formatter that uses {@link Object#toString()} and splits on newlines.
+ *
+ * @author Eric Bottard
+ */
+public class DefaultFormatter implements Formatter {
+
+ public String[] format(Object value) {
+ return value == null ? new String[] {""} : value.toString().split("\n");
+ }
+
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/DelimiterTextWrapper.java b/spring-shell-table/src/main/java/org/springframework/shell/table/DelimiterTextWrapper.java
new file mode 100644
index 00000000..b4a3422b
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/DelimiterTextWrapper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A Text wrapper that wraps at "word" boundaries. The default delimiter is the space character.
+ *
+ * @author Eric Bottard
+ */
+public class DelimiterTextWrapper implements TextWrapper {
+
+ private final char delimiter;
+
+ public DelimiterTextWrapper() {
+ this(' ');
+ }
+
+ public DelimiterTextWrapper(char delimiter) {
+ this.delimiter = delimiter;
+ }
+
+ @Override
+ public String[] wrap(String[] original, int columnWidth) {
+ List result = new ArrayList(original.length);
+ for (String line : original) {
+ while (line.length() > columnWidth) {
+ int split = line.lastIndexOf(delimiter, columnWidth);
+ String toAdd = split == -1 ? line.substring(0, columnWidth) : line.substring(0, split);
+ result.add(String.format("%-" + columnWidth + "s", toAdd));
+ line = line.substring(split == -1 ? columnWidth : split + 1);
+ }
+ if (columnWidth > 0) {
+ result.add(String.format("%-" + columnWidth + "s", line)); // right pad if necessary
+ }
+ }
+ return result.toArray(new String[result.size()]);
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java b/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java
new file mode 100644
index 00000000..36f1573a
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A Formatter is responsible for the initial rendering of a value to lines of text.
+ *
+ * Note that this representation is likely to be altered later in the pipeline, for the
+ * purpose of text wrapping and aligning. The role of a formatter is merely to give the
+ * raw text representation (e.g. format numbers).
+ *
+ * @author Eric Bottard
+ */
+public interface Formatter {
+
+ public String[] format(Object value);
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueHorizontalAligner.java b/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueHorizontalAligner.java
new file mode 100644
index 00000000..30fde797
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueHorizontalAligner.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A text alignment strategy that aligns text horizontally so that all instances of some special character(s)
+ * line up perfectly in a column.
+ *
+ * Typically used to render numbers which may or may not have a decimal point, or series of key-value pairs
+ *
+ * @author Eric Bottard
+ */
+public class KeyValueHorizontalAligner implements Aligner {
+
+ private final String delimiter;
+
+ public KeyValueHorizontalAligner(String delimiter) {
+ this.delimiter = delimiter;
+ }
+
+ @Override
+ public String[] align(String[] text, int cellWidth, int cellHeight) {
+
+ String[] result = new String[cellHeight];
+ int alignOffset = 0;
+ for (String line : text) {
+ alignOffset = Math.max(alignOffset, line.trim().indexOf(delimiter));
+ }
+ int i = 0;
+ for (String line : text) {
+ String trimmed = line.trim();
+ int offset = trimmed.indexOf(delimiter);
+ if (offset >= 0) {
+ // It is possible that aligning would trigger overflow
+ // Make sure not to
+ int offsetToUse = Math.min(alignOffset - offset, cellWidth - trimmed.length());
+ result[i++] = pad(offsetToUse, cellWidth - trimmed.length() - offsetToUse, trimmed);
+ }
+ else {
+ result[i++] = pad(0, cellWidth - line.length(), line);
+ }
+
+ }
+ return result;
+ }
+
+ private String pad(int left, int right, String original) {
+ StringBuilder sb = new StringBuilder(left + original.length() + right);
+ for (int i = 0; i < left; i++) {
+ sb.append(' ');
+ }
+ sb.append(original);
+ for (int i = 0; i < right; i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueSizeConstraints.java b/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueSizeConstraints.java
new file mode 100644
index 00000000..84550f85
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueSizeConstraints.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A SizeConstraints implementation that is tailored to rendering a series
+ * of {@literal key = value} pairs. Computes extents so that equal signs (or any other
+ * configurable delimiter) line up vertically.
+ *
+ * @author Eric Bottard
+ */
+public class KeyValueSizeConstraints implements SizeConstraints {
+
+ private final String delimiter;
+
+ public KeyValueSizeConstraints(String delimiter) {
+ this.delimiter = delimiter;
+ }
+
+ private static String leftTrim(String raw) {
+ int start = 0;
+ int length = raw.length();
+ while (start < length && raw.charAt(start) == ' ') {
+ start++;
+ }
+ return raw.substring(start);
+ }
+
+ private static String rightTrim(String raw) {
+ int end = raw.length();
+ while (end > 0 && raw.charAt(end - 1) == ' ') {
+ end--;
+ }
+ return raw.substring(0, end);
+ }
+
+ @Override
+ public Extent width(String[] raw, int tableWidth, int nbColumns) {
+
+ // We need to make sure we take care of the case where we have
+ // k = long-value
+ // long-key = v
+ // as the real maximal extent is size(long-key) + size( = ) + size(long-value)
+
+ // The minimal extent in the example above is size(long-value)
+
+ int maxLeft = 0;
+ int maxRight = 0;
+ int min = 0;
+ for (String line : raw) {
+ String lineToConsider = line.trim();
+ int offset = lineToConsider.indexOf(delimiter);
+
+ if (offset != -1) {
+ // Compute minimal case (line can be split, decide where to put the delimiter)
+ String minimalLeftPart = lineToConsider.substring(0, offset).trim();
+ String minimalRightPart = lineToConsider.substring(offset + delimiter.length()).trim();
+ int left = minimalLeftPart.length();
+ int right = minimalRightPart.length();
+ int case1 = Math.max(left, right + leftTrim(delimiter).length());
+ int case2 = Math.max(left + rightTrim(delimiter).length(), right);
+ int bestMin = Math.min(case1, case2);
+ min = Math.max(min, bestMin);
+
+ // Compute maximal case (sum of worst case on left and right)
+ maxLeft = Math.max(maxLeft, offset);
+ int after = lineToConsider.length() - offset - delimiter.length();
+ maxRight = Math.max(maxRight, after);
+ }
+ else {
+ min = Math.max(min, lineToConsider.length());
+ }
+
+ }
+
+ return new Extent(min, maxLeft + delimiter.length() + maxRight);
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueTextWrapper.java b/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueTextWrapper.java
new file mode 100644
index 00000000..18bcf65c
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/KeyValueTextWrapper.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A TextWrapper implementation tailored for key-value rendering (working in concert
+ * with {@link KeyValueSizeConstraints}, {@link KeyValueHorizontalAligner}), that tries its
+ * best to vertically align some delimiter character (default '=').
+ */
+public class KeyValueTextWrapper implements TextWrapper {
+
+ private final String delimiter;
+
+ public KeyValueTextWrapper() {
+ this("=");
+ }
+
+ public KeyValueTextWrapper(String delimiter) {
+ this.delimiter = delimiter;
+ }
+
+ @Override
+ public String[] wrap(String[] original, int columnWidth) {
+ List result = new ArrayList();
+ for (String line : original) {
+ line = line.trim();
+ while (line.length() > columnWidth) {
+ int cut = line.lastIndexOf(delimiter, columnWidth);
+ if (cut == -1) {
+ cut = columnWidth;
+ }
+ else if (cut + delimiter.length() <= columnWidth) {
+ cut = cut + delimiter.length();
+ }
+ result.add(rightPad(line.substring(0, cut), columnWidth));
+ line = line.substring(cut).trim();
+ }
+ if (line.length() > 0) {
+ result.add(rightPad(line, columnWidth));
+ }
+ }
+ return result.toArray(new String[result.size()]);
+ }
+
+ private String rightPad(String raw, int width) {
+ StringBuilder result = new StringBuilder(raw);
+ for (int i = raw.length(); i < width; i++) {
+ result.append(' ');
+ }
+ return result.toString();
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java b/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java
new file mode 100644
index 00000000..7ea3b3dc
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.Map;
+
+/**
+ * A formatter suited for key-value pairs, that renders each mapping on a new line.
+ *
+ * @author Eric Bottard
+ */
+public class MapFormatter implements Formatter {
+
+ private final String separator;
+
+ public MapFormatter(String separator) {
+ this.separator = separator;
+ }
+
+ @Override
+ public String[] format(Object value) {
+ Map, ?> map = (Map, ?>) value;
+ String[] result = new String[map.size()];
+ int i = 0;
+ for (Map.Entry, ?> kv : map.entrySet()) {
+ result[i++] = kv.getKey() + separator + kv.getValue();
+ }
+ return result;
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/NoWrapSizeConstraints.java b/spring-shell-table/src/main/java/org/springframework/shell/table/NoWrapSizeConstraints.java
new file mode 100644
index 00000000..8e7a0d0b
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/NoWrapSizeConstraints.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A sizing strategy that will impose the longest line width on cells.
+ *
+ * @author Eric Bottard
+ */
+public class NoWrapSizeConstraints implements SizeConstraints {
+
+ @Override
+ public Extent width(String[] raw, int tableWidth, int nbColumns) {
+ int max = 0;
+ for (String line : raw) {
+ max = Math.max(max, line.length());
+ }
+ return new Extent(max, max);
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/SimpleHorizontalAligner.java b/spring-shell-table/src/main/java/org/springframework/shell/table/SimpleHorizontalAligner.java
new file mode 100644
index 00000000..bdd6749f
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/SimpleHorizontalAligner.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * An horizontal alignment strategy that allows alignment to the left, center or right.
+ *
+ * @author Eric Bottard
+ */
+public enum SimpleHorizontalAligner implements Aligner {
+
+ left, center, right;
+
+ @Override
+ public String[] align(String[] text, int cellWidth, int cellHeight) {
+ String[] result = new String[cellHeight];
+ for (int i = 0; i < cellHeight; i++) {
+ String line = (i < text.length && text[i] != null) ? text[i].trim() : "";
+
+ int paddingToDistribute = cellWidth - line.length();
+
+ int padLeft;
+ int padRight;
+
+ switch (this) {
+ case center: {
+ int carry = paddingToDistribute % 2;
+ paddingToDistribute = paddingToDistribute - carry;
+ padLeft = padRight = paddingToDistribute / 2;
+ padRight += carry;
+ break;
+ }
+ case right: {
+ padLeft = paddingToDistribute;
+ padRight = 0;
+ break;
+ }
+ case left: {
+ padLeft = 0;
+ padRight = paddingToDistribute;
+ break;
+ }
+ default:
+ throw new AssertionError();
+ }
+ StringBuilder sb = new StringBuilder(cellWidth);
+ for (int j = 0; j < padLeft; j++) {
+ sb.append(' ');
+ }
+ sb.append(line);
+ for (int j = 0; j < padRight; j++) {
+ sb.append(' ');
+ }
+
+ result[i] = sb.toString();
+ }
+ return result;
+ }
+
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/SimpleVerticalAligner.java b/spring-shell-table/src/main/java/org/springframework/shell/table/SimpleVerticalAligner.java
new file mode 100644
index 00000000..f3e230c5
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/SimpleVerticalAligner.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.Arrays;
+
+/**
+ * Alignment strategy that allows simple vertical alignment to top, middle or bottom.
+ *
+ * @author Eric Bottard
+ */
+public enum SimpleVerticalAligner implements Aligner {
+
+ top, middle, bottom;
+
+ @Override
+ public String[] align(String[] text, int cellWidth, int cellHeight) {
+ String[] result = new String[cellHeight];
+ int blanksBefore = 0;
+ int blanksAfter = 0;
+ boolean atLeastOneNonEmptyRow = false;
+ for (int row = 0; row < text.length; row++) {
+ if (text[row] == null || text[row].trim().equals("")) {
+ blanksBefore++;
+ }
+ else {
+ atLeastOneNonEmptyRow = true;
+ break;
+ }
+ }
+ // In case of full blank, don't count blank rows twice
+ if (atLeastOneNonEmptyRow) {
+ for (int row = text.length - 1; row >= 0; row--) {
+ if (text[row] == null || text[row].trim().equals("")) {
+ blanksAfter++;
+ }
+ else {
+ break;
+ }
+ }
+ }
+ String filler = spaces(cellWidth);
+
+ int padBefore;
+ int padAfter;
+ int nonBlankLines = text.length - blanksAfter - blanksBefore;
+ int paddingToDistribute = cellHeight - nonBlankLines;
+
+ switch (this) {
+ case middle: {
+ int carry = paddingToDistribute % 2;
+ paddingToDistribute = paddingToDistribute - carry;
+ padBefore = padAfter = paddingToDistribute / 2;
+ padAfter += carry;
+ break;
+ }
+ case bottom: {
+ padBefore = paddingToDistribute;
+ padAfter = 0;
+ break;
+ }
+ case top: {
+ padBefore = 0;
+ padAfter = paddingToDistribute;
+ break;
+ }
+ default:
+ throw new AssertionError();
+ }
+
+ Arrays.fill(result, 0, padBefore, filler);
+ System.arraycopy(text, blanksBefore, result, padBefore, nonBlankLines);
+ Arrays.fill(result, result.length - padAfter, result.length, filler);
+
+ return result;
+ }
+
+ private String spaces(int width) {
+ char[] data = new char[width];
+ Arrays.fill(data, ' ');
+ return new String(data);
+ }
+
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/SizeConstraints.java b/spring-shell-table/src/main/java/org/springframework/shell/table/SizeConstraints.java
new file mode 100644
index 00000000..cec862c6
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/SizeConstraints.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import org.springframework.util.Assert;
+
+/**
+ * Strategy for computing the dimensions of a table cell.
+ *
+ * @author Eric Bottard
+ */
+public interface SizeConstraints {
+
+ /**
+ * @return the minimum and maximum width of the cell, given its raw content.
+ * @param raw the raw String representation of the cell contents (may be reformatted later, eg wrapped)
+ * @param tableWidth the whole available width for the table
+ * @param nbColumns the number of columns in the table
+ */
+ Extent width(String[] raw, int tableWidth, int nbColumns);
+
+ /**
+ * Holds both a minimum and maximum width.
+ *
+ * @author Eric Bottard
+ */
+ class Extent {
+
+ public final int min;
+
+ public final int max;
+
+ public Extent(int min, int max) {
+ Assert.isTrue(min <= max, "min must be less than max");
+ Assert.isTrue(0 <= min, "min and max must be positive");
+ this.min = min;
+ this.max = max;
+ }
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/Table.java b/spring-shell-table/src/main/java/org/springframework/shell/table/Table.java
new file mode 100644
index 00000000..8b7b8402
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/Table.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import static org.springframework.shell.table.BorderSpecification.NONE;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.shell.TerminalSizeAware;
+
+/**
+ * This is the central API for table rendering. A Table object is constructed with a given
+ * TableModel, which holds raw table contents. Its rendering logic is then altered by applying
+ * various customizations, in a fashion very similar to what is used e.g. in a spreadsheet
+ * program:
+ * {@link #formatters formatters} know how to derive character data out of raw data. For
+ * example, numbers are
+ * formatted according to a Locale, or Maps are emitted as a series of {@literal key=value} lines
+ * {@link #sizeConstraints size constraints} are then applied, which decide how
+ * much column real estate to allocate to cells
+ * {@link #wrappers text wrapping policies} are applied once the column sizes
+ * are known
+ * finally, {@link #aligners alignment} strategies actually render
+ * text as a series of space-padded strings that draw nicely on screen.
+ *
+ * All those customizations are applied selectively on the Table cells thanks to a {@link CellMatcher}: One can
+ * decide to right pad column number 3, or to format in a certain way all instances of {@literal java.util.Map}.
+ *
+ * Of course, all of those customizations often work hand in hand, and not all combinations make sense:
+ * one needs to anticipate the fact that text will be split using the ' ' (space) character to properly
+ * calculate column sizes.
+ * @author Eric Bottard
+ */
+public class Table implements TerminalSizeAware {
+
+ private final int rows;
+
+ private final int columns;
+
+ private TableModel model;
+
+ private Map formatters = new LinkedHashMap();
+
+ private Map sizeConstraints = new LinkedHashMap();
+
+ private Map wrappers = new LinkedHashMap();
+
+ private Map aligners = new LinkedHashMap();
+
+ private List borderSpecifications = new ArrayList();
+
+ /**
+ * Construct a new Table with the given model and customizers.
+ * The passed in LinkedHashMap should be in reverse-insertion order (i.e. the first CellMatcher
+ * found in iteration order will "win").
+ *
+ * @see TableBuilder#build()
+ */
+ /*package*/ Table(TableModel model,
+ LinkedHashMap formatters,
+ LinkedHashMap sizeConstraints,
+ LinkedHashMap wrappers,
+ LinkedHashMap aligners,
+ List borderSpecifications) {
+ this.model = model;
+ this.formatters = formatters;
+ this.sizeConstraints = sizeConstraints;
+ this.wrappers = wrappers;
+ this.aligners = aligners;
+ this.borderSpecifications = borderSpecifications;
+ rows = model.getRowCount();
+ columns = model.getColumnCount();
+
+ }
+
+ public TableModel getModel() {
+ return model;
+ }
+
+ public String render(int totalAvailableWidth) {
+ StringBuilder result = new StringBuilder();
+
+ int[] cellHeights = new int[rows];
+ int[] cellWidths;
+ int[] minCellWidths = new int[columns];
+ int[] maxCellWidths = new int[columns];
+
+ String[][][] subLines = new String[rows][columns][];
+
+ Borders borders = new Borders();
+ int widthAvailableForContents = totalAvailableWidth - borders.getNumberOfVerticalBorders();
+
+ // First, compute desired column widths
+ for (int row = 0; row < rows; row++) {
+ for (int column = 0; column < columns; column++) {
+ Object value = model.getValue(row, column);
+ String[] lines = getFormatter(row, column).format(value);
+ subLines[row][column] = lines;
+
+ SizeConstraints.Extent extent = getSizeConstraints(row, column).width(lines, widthAvailableForContents, columns);
+
+ minCellWidths[column] = Math.max(minCellWidths[column], extent.min);
+ maxCellWidths[column] = Math.max(maxCellWidths[column], extent.max);
+
+ }
+ }
+
+
+ cellWidths = computeColumnWidths(widthAvailableForContents, minCellWidths, maxCellWidths);
+ // Now that widths are known, apply wrapping & render
+ for (int row = 0; row < rows; row++) {
+ for (int column = 0; column < columns; column++) {
+ subLines[row][column] = getWrapper(row, column).wrap(subLines[row][column], cellWidths[column]);
+ cellHeights[row] = Math.max(cellHeights[row], subLines[row][column].length);
+ }
+ for (int column = 0; column < columns; column++) {
+ for (Map.Entry kv : aligners.entrySet()) {
+ if (kv.getKey().matches(row, column, model)) {
+ subLines[row][column] = kv.getValue().align(subLines[row][column], cellWidths[column], cellHeights[row]);
+ }
+ }
+ }
+ }
+
+
+ for (int row = 0; row < rows; row++) {
+
+ // TOP CELL BORDER
+ int before = result.length();
+ for (int column = 0; column < columns; column++) {
+ borders.paintCorner(row, column, result);
+ borders.paintHorizontal(row, column, cellWidths[column], result);
+ }
+ borders.paintCorner(row, columns, result);
+ if (result.length() > before) {
+ result.append('\n');
+ }
+
+ for (int subRow = 0; subRow < cellHeights[row]; subRow++) {
+ for (int column = 0; column < columns; column++) {
+ // LEFT CELL BORDER
+ borders.paintVertical(row, column, result);
+ String[] lines = subLines[row][column];
+ result.append(lines[subRow]);
+ }
+ // TABLE RIGHT BORDER
+ borders.paintVertical(row, columns, result);
+ result.append("\n");
+ }
+ }
+
+ // TABLE BOTTOM BORDER
+ int before = result.length();
+ for (int column = 0; column < columns; column++) {
+ borders.paintCorner(rows, column, result);
+ borders.paintHorizontal(rows, column, cellWidths[column], result);
+ }
+
+ // TABLE BOTTOM RIGHT CORNER
+ borders.paintCorner(rows, columns, result);
+ if (result.length() > before) {
+ result.append('\n');
+ }
+ return result.toString();
+ }
+
+ private int[] computeColumnWidths(int availableWidth, int[] minCellWidths, int[] maxCellWidths) {
+
+ int[] cellWidths;
+ int minTableWidth = 0, maxTableWidth = 0;
+ for (int column = 0; column < columns; column++) {
+ minTableWidth += minCellWidths[column];
+ maxTableWidth += maxCellWidths[column];
+ }
+
+ // Can use max desired width
+ if (maxTableWidth <= availableWidth) {
+ cellWidths = maxCellWidths;
+ } // will overflow
+ else if (minTableWidth >= availableWidth) {
+ cellWidths = minCellWidths;
+ } // Redistribute nicely
+ else {
+ int W = availableWidth - minTableWidth;
+ int D = maxTableWidth - minTableWidth;
+ cellWidths = new int[columns];
+ for (int column = 0; column < columns; column++) {
+ cellWidths[column] = minCellWidths[column] + W * (maxCellWidths[column] - minCellWidths[column]) / D;
+ }
+ }
+ return cellWidths;
+ }
+
+ private TextWrapper getWrapper(int row, int column) {
+ for (Map.Entry kv : wrappers.entrySet()) {
+ if (kv.getKey().matches(row, column, model)) {
+ return kv.getValue();
+ }
+ }
+ throw new AssertionError("Can't be reached thanks to the whole-table default");
+ }
+
+ private SizeConstraints getSizeConstraints(int row, int column) {
+ for (Map.Entry kv : sizeConstraints.entrySet()) {
+ if (kv.getKey().matches(row, column, model)) {
+ return kv.getValue();
+ }
+ }
+ throw new AssertionError("Can't be reached thanks to the whole-table default");
+ }
+
+ private Formatter getFormatter(int row, int column) {
+ for (Map.Entry kv : formatters.entrySet()) {
+ if (kv.getKey().matches(row, column, model)) {
+ return kv.getValue();
+ }
+ }
+ throw new AssertionError("Can't be reached thanks to the whole-table default");
+ }
+
+ /**
+ * An instance of this class knows where to paint border glyphs.
+ *
+ * In all instance arrays, 'row' and 'column' are actually indices in-between
+ * table rows and columns. Hence, sizes are larger by one.
+ * @author Eric Bottard
+ */
+ private class Borders {
+
+ /**
+ * Glyph to paint a vertical line at row,col.
+ */
+ private char[][] verticals;
+
+ /**
+ * Glyph to paint a horizontal line at row,col.
+ */
+ private char[][] horizontals;
+
+ /**
+ * The type of corner, if any, to paint at row,col.
+ */
+ private char[][] corners;
+
+ /**
+ * True if at least one vertical bar exists in that col.
+ */
+ private boolean[] vFillers;
+
+ /**
+ * True if at least one horizontal bar exists in that row.
+ */
+ private boolean[] hFillers;
+
+ public Borders() {
+ verticals = new char[rows][columns + 1];
+ horizontals = new char[rows + 1][columns];
+ corners = new char[rows + 1][columns + 1];
+ vFillers = new boolean[columns + 1];
+ hFillers = new boolean[rows + 1];
+ init();
+ }
+
+ private void init() {
+
+ for (int row = 0; row <= rows; row++) {
+ for (int column = 0; column <= columns; column++) {
+ for (BorderSpecification bs : borderSpecifications) {
+ if (row < rows) {
+ char verticalThere = bs.verticals(row, column);
+ if (verticalThere != BorderStyle.NONE) {
+ this.verticals[row][column] = verticalThere;
+ vFillers[column] |= true;
+ }
+ }
+ if (column < columns) {
+ char horizontalThere = bs.horizontals(row, column);
+ if (horizontalThere != BorderStyle.NONE) {
+ this.horizontals[row][column] = horizontalThere;
+ hFillers[row] |= true;
+ }
+ }
+ }
+ }
+ }
+
+ // Compute corners when horizontals & verticals intersect
+ for (int row = 0; row <= rows; row++) {
+ for (int column = 0; column <= columns; column++) {
+ char left = (column - 1 >= 0) ? horizontals[row][column - 1] : NONE;
+ char right = (column < columns) ? horizontals[row][column] : NONE;
+ char above = (row - 1 >= 0) ? verticals[row - 1][column] : NONE;
+ char below = (row < rows) ? verticals[row][column] : NONE;
+ corners[row][column] = BorderStyle.intersection(above, below, left, right);
+ }
+ }
+ }
+
+ private void paintCorner(int row, int column, StringBuilder stringBuilder) {
+ if (corners[row][column] != NONE) {
+ stringBuilder.append(corners[row][column]);
+ } // If there is a border in same row|column, paint filler
+ else if (vFillers[column] && hFillers[row]) {
+ stringBuilder.append(' ');
+ }
+ }
+
+ private void paintVertical(int row, int column, StringBuilder stringBuilder) {
+ if (verticals[row][column] != NONE) {
+ stringBuilder.append(verticals[row][column]);
+ }
+ else if (vFillers[column]) {
+ stringBuilder.append(' ');
+ }
+ }
+
+ private void paintHorizontal(int row, int column, int width, StringBuilder stringBuilder) {
+ if (horizontals[row][column] != NONE) {
+ for (int i = 0; i < width; i++) {
+ stringBuilder.append(horizontals[row][column]);
+ }
+ }
+ else if (hFillers[row]) {
+ for (int i = 0; i < width; i++) {
+ stringBuilder.append(' ');
+ }
+ }
+ }
+
+ /**
+ * Return the number of vertical borders, and hence the space consumed by those.
+ */
+ public int getNumberOfVerticalBorders() {
+ int result = 0;
+ for (boolean b : vFillers) {
+ if (b) {
+ result++;
+ }
+ }
+ return result;
+ }
+ }
+
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/TableBuilder.java b/spring-shell-table/src/main/java/org/springframework/shell/table/TableBuilder.java
new file mode 100644
index 00000000..2a3bebda
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/TableBuilder.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import static org.springframework.shell.table.BorderSpecification.FULL;
+import static org.springframework.shell.table.BorderSpecification.INNER;
+import static org.springframework.shell.table.BorderSpecification.INNER_VERTICAL;
+import static org.springframework.shell.table.BorderSpecification.OUTLINE;
+import static org.springframework.shell.table.SimpleHorizontalAligner.left;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.util.Assert;
+
+/**
+ * A builder class to incrementally configure a Table.
+ * @author Eric Bottard
+ */
+public class TableBuilder {
+
+ private final TableModel model;
+
+ private final Map formatters = new LinkedHashMap();
+
+ private final Map sizeConstraints = new LinkedHashMap();
+
+ private final Map wrappers = new LinkedHashMap();
+
+ private final LinkedHashMap aligners = new LinkedHashMap();
+
+ private final List borderSpecifications = new ArrayList();
+
+ private final int rows;
+
+ private final int columns;
+
+ /**
+ * Construct a table with the given model. The table will use the following
+ * strategies for all cells, unless overridden:
+ * {@link DefaultFormatter default formatting} using {@literal toString()}
+ * {@link AutoSizeConstraints sizing strategy} trying to use the maximum space, resorting to splitting lines on
+ * spaces
+ * {@link DelimiterTextWrapper wrapping text} on space characters
+ * {@link SimpleHorizontalAligner left aligning} text.
+ *
+ *
+ * @param model the data model of the table to construct
+ */
+
+ public TableBuilder(TableModel model) {
+ this.model = model;
+ rows = model.getRowCount();
+ columns = model.getColumnCount();
+
+ formatters.put(CellMatchers.table(), new DefaultFormatter());
+ sizeConstraints.put(CellMatchers.table(), new AutoSizeConstraints());
+ wrappers.put(CellMatchers.table(), new DelimiterTextWrapper());
+ aligners.put(CellMatchers.table(), left);
+
+ }
+
+ private TableBuilder addBorder(int top, int left, int bottom, int right, int match, BorderStyle style) {
+ Assert.isTrue(top >= 0 && top < rows, "top row must be positive and less than total number of rows");
+ Assert.isTrue(left >= 0 && left < columns, "left column must be positive and less than total number of columns");
+ Assert.isTrue(bottom > top && bottom <= rows, "bottom row must be greater than top and less than total number of rows");
+ Assert.isTrue(right >= left && right <= columns, "right column must be greater than left and less than total number of columns");
+ Assert.notNull(style, "style cannot be null");
+ borderSpecifications.add(new BorderSpecification(top, left, bottom, right, match, style));
+ return this;
+ }
+
+ public TableModel getModel() {
+ return model;
+ }
+
+ public CellMatcherStub on(CellMatcher matcher) {
+ return new CellMatcherStub(matcher);
+ }
+
+ public Table build() {
+ return new Table(model,
+ reverse(formatters),
+ reverse(sizeConstraints),
+ reverse(wrappers),
+ aligners,
+ borderSpecifications);
+ }
+
+ public BorderStub paintBorder(BorderStyle style, int match) {
+ return new BorderStub(style, match);
+ }
+
+ // Convenience methods for borders
+
+ /**
+ * Set a border on the outline of the whole table.
+ * @param style the style to apply
+ * @return this, for method chaining
+ */
+ public TableBuilder addOutlineBorder(BorderStyle style) {
+ this.addBorder(0, 0, model.getRowCount(), model.getColumnCount(), OUTLINE, style);
+ return this;
+ }
+
+ /**
+ * Set a border on the outline of the whole table, as well as around the first row.
+ * @param style the style to apply
+ * @return this, for method chaining
+ */
+ public TableBuilder addHeaderBorder(BorderStyle style) {
+ this.addBorder(0, 0, 1, model.getColumnCount(), OUTLINE, style);
+ return addOutlineBorder(style);
+ }
+
+ /**
+ * Set a border around each and every cell of the table.
+ *
+ * @param style the style to apply
+ * @return this, for method chaining
+ */
+ public TableBuilder addFullBorder(BorderStyle style) {
+ this.addBorder(0, 0, model.getRowCount(), model.getColumnCount(), FULL, style);
+ return this;
+ }
+
+ /**
+ * Set a border on the outline of the whole table, around the first row and draw vertical lines
+ * around each column.
+ *
+ * @param style the style to apply
+ * @return this, for method chaining
+ */
+ public TableBuilder addHeaderAndVerticalsBorders(BorderStyle style) {
+ this.addBorder(0, 0, 1, model.getColumnCount(), OUTLINE, style);
+ this.addBorder(0, 0, model.getRowCount(), model.getColumnCount(), OUTLINE | INNER_VERTICAL, style);
+ return this;
+ }
+
+ /**
+ * Set a border on the inner verticals and horizontals of the table, but not on the outline.
+ *
+ * @param style the style to apply
+ * @return this, for method chaining
+ */
+ public TableBuilder addInnerBorder(BorderStyle style) {
+ this.addBorder(0, 0, model.getRowCount(), model.getColumnCount(), INNER, style);
+ return this;
+ }
+
+ private LinkedHashMap reverse(Map original) {
+ LinkedHashMap result = new LinkedHashMap(original.size());
+ List> entries = new ArrayList>(original.entrySet());
+ for (int i = entries.size() - 1; i >= 0; i--) {
+ Map.Entry entry = entries.get(i);
+ result.put(entry.getKey(), entry.getValue());
+ }
+ return result;
+ }
+
+ public class CellMatcherStub {
+
+ private final CellMatcher cellMatcher;
+
+ private CellMatcherStub(CellMatcher cellMatcher) {
+ this.cellMatcher = cellMatcher;
+ }
+
+ public CellMatcherStub addFormatter(Formatter formatter) {
+ formatters.put(this.cellMatcher, formatter);
+ return this;
+ }
+
+ public CellMatcherStub addSizer(SizeConstraints sizer) {
+ sizeConstraints.put(this.cellMatcher, sizer);
+ return this;
+ }
+
+ public CellMatcherStub addWrapper(TextWrapper textWrapper) {
+ wrappers.put(this.cellMatcher, textWrapper);
+ return this;
+ }
+
+ public CellMatcherStub addAligner(Aligner aligner) {
+ aligners.put(this.cellMatcher, aligner);
+ return this;
+ }
+
+ public CellMatcherStub on(CellMatcher other) {
+ return TableBuilder.this.on(other);
+ }
+
+ public TableBuilder and() {
+ return TableBuilder.this;
+ }
+
+ public Table build() {
+ return TableBuilder.this.build();
+ }
+ }
+
+ public class BorderStub {
+
+ private final BorderStyle style;
+
+ private final int match;
+
+ private BorderStub(BorderStyle style, int match) {
+ this.style = style;
+ this.match = match;
+ }
+
+ public TopLeft fromRowColumn(int row, int column) {
+ return new TopLeft(row, column);
+ }
+
+ public TopLeft fromTopLeft() {
+ return new TopLeft(0, 0);
+ }
+
+ public class TopLeft {
+ private final int row;
+
+ private final int column;
+
+ private TopLeft(int row, int column) {
+ this.row = row;
+ this.column = column;
+ }
+
+ public TableBuilder toRowColumn(int row, int column) {
+ TableBuilder.this.addBorder(TopLeft.this.row, TopLeft.this.column, row, column, BorderStub.this.match, BorderStub.this.style);
+ return TableBuilder.this;
+ }
+
+ public TableBuilder toBottomRight() {
+ return toRowColumn(model.getRowCount(), model.getColumnCount());
+ }
+ }
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java
new file mode 100644
index 00000000..381c261d
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * Abstracts away the contract a {@link Table} will use to retrieve tabular data.
+ *
+ * @author Eric Bottard
+ */
+public abstract class TableModel {
+
+ /**
+ * @return the number of rows that can be queried.
+ * Values between 0 and {@code rowCount-1} inclusive are valid values.
+ */
+ public abstract int getRowCount();
+
+ /**
+ * @return the number of columns that can be queried.
+ * Values between 0 and {@code columnCount-1} inclusive are valid values.
+ */
+ public abstract int getColumnCount();
+
+ /**
+ * @return the data value to be displayed at a given row and column, which may be null.
+ * @param row the row that is being queried
+ * @param column the column that is being queried
+ */
+ public abstract Object getValue(int row, int column);
+
+ /**
+ * @return a transposed view of this model, where rows become columns and vice-versa.
+ */
+ public TableModel transpose() {
+ return new TableModel() {
+ @Override
+ public int getRowCount() {
+ return TableModel.this.getColumnCount();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return TableModel.this.getRowCount();
+ }
+
+ @Override
+ public Object getValue(int row, int column) {
+ return TableModel.this.getValue(column, row);
+ }
+ };
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java
new file mode 100644
index 00000000..12c76acc
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+/**
+ * Helper class to build a TableModel incrementally.
+ *
+ * @author Eric Bottard
+ */
+public class TableModelBuilder {
+
+ public static final int DEFAULT_ROW_CAPACITY = 3;
+
+ private List> rows = new ArrayList>();
+
+ private int previousRowSize = -1;
+
+ private boolean frozen;
+
+ public TableModelBuilder addRow() {
+ Assert.isTrue(!frozen, "TableModel has already been built, builder can't be altered anymore");
+ int nbRows = rows.size();
+ if (previousRowSize != -1) {
+ int currentRowSize = rows.get(nbRows - 1).size();
+ Assert.isTrue(currentRowSize == previousRowSize,
+ "Can't switch to next row, as the current one does not have as many elements as the previous one");
+ }
+ if (rows.size() > 0) {
+ previousRowSize = rows.get(0).size();
+ }
+ rows.add(new ArrayList(previousRowSize == -1 ? DEFAULT_ROW_CAPACITY : previousRowSize));
+ return this;
+ }
+
+ public TableModelBuilder addValue(T value) {
+ Assert.isTrue(!frozen, "TableModel has already been built, builder can't be altered anymore");
+ if (previousRowSize != -1 && rows.get(rows.size() - 1).size() == previousRowSize) {
+ throw new IllegalArgumentException("Can't add another value to current row");
+ }
+ rows.get(rows.size() - 1).add(value);
+ return this;
+ }
+
+ public TableModel build() {
+ frozen = true;
+ return new TableModel() {
+ @Override
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return rows.isEmpty() ? 0 : rows.get(0).size();
+ }
+
+ @Override
+ public Object getValue(int row, int column) {
+ return rows.get(row).get(column);
+ }
+ };
+
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/Tables.java b/spring-shell-table/src/main/java/org/springframework/shell/table/Tables.java
new file mode 100644
index 00000000..52289e08
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/Tables.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+import java.util.Map;
+
+/**
+ * Utility class used to create and configure typical Tables.
+ *
+ * @author Eric Bottard
+ */
+public class Tables {
+
+ /**
+ * Install all the necessary formatters, aligners, etc for key-value rendering of Maps.
+ *
+ * @param builder the builder to configure
+ * @param delimiter the delimiter to apply between keys and values
+ * @return buider for method chaining
+ */
+ public static TableBuilder configureKeyValueRendering(TableBuilder builder, String delimiter) {
+ return builder.on(CellMatchers.ofType(Map.class))
+ .addFormatter(new MapFormatter(delimiter))
+ .addAligner(new KeyValueHorizontalAligner(delimiter.trim()))
+ .addSizer(new KeyValueSizeConstraints(delimiter))
+ .addWrapper(new KeyValueTextWrapper(delimiter)).and();
+ }
+}
diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/TextWrapper.java b/spring-shell-table/src/main/java/org/springframework/shell/table/TextWrapper.java
new file mode 100644
index 00000000..5838174d
--- /dev/null
+++ b/spring-shell-table/src/main/java/org/springframework/shell/table/TextWrapper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.table;
+
+/**
+ * A strategy for applying text wrapping/cropping given a cell width.
+ *
+ * @author Eric Bottard
+ */
+public interface TextWrapper {
+
+ /**
+ * @return a list of lines where each line length MUST be equal to {@code columnWidth} (padding with spaces if
+ * appropriate). There is no constraint on the number of lines returned however (typically, will be greater than
+ * the input number if wrapping occurred).
+ *
+ * @param original the text in its original form
+ * @param columnWidth the width to conform to
+ */
+ String[] wrap(String[] original, int columnWidth);
+}