Add pretty table rendering

* Add some border combinations + helper methods
* Introducing TableBuilder
* Fix tests
* Add deprecation tags for old table
This commit is contained in:
Eric Bottard
2015-10-19 18:50:44 +02:00
committed by Mark Pollack
parent 21a993d6bf
commit 29e0d6ae73
73 changed files with 3209 additions and 7 deletions

View File

@@ -78,11 +78,15 @@ javadoc {
]
//exclude "org/springframework/data/redis/config/**"
if (JavaVersion.current().isJava8Compatible()) {
options.addStringOption('Xdoclint:none', '-quiet')
}
}
title = "${rootProject.description} ${version} API"
}
jar {
manifest.attributes['Implementation-Title'] = 'spring-shell'
manifest.attributes['Implementation-Version'] = project.version

View File

@@ -0,0 +1,14 @@
package org.springframework.shell;
/**
* To be implemented by command result objects that can adapt to the terminal size when they are being rendered.
*
* <p>An object which does not implement this interface will simply be rendered by invoking its {@link #toString()}
* method.</p>
*
* @author Eric Bottard
*/
public interface TerminalSizeAware {
CharSequence render(int terminalWidth);
}

View File

@@ -19,6 +19,9 @@ import java.io.File;
import java.util.logging.Level;
import java.util.logging.Logger;
import jline.TerminalFactory;
import org.springframework.shell.TerminalSizeAware;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.event.AbstractShellStatusPublisher;
import org.springframework.shell.event.ParseResult;
@@ -141,6 +144,7 @@ public abstract class AbstractShell extends AbstractShellStatusPublisher impleme
return new CommandResult(true, result, null);
} catch (RuntimeException e) {
setShellStatus(Status.EXECUTION_FAILED, line, parseResult);
logger.log(Level.WARNING, e.getMessage(), e);
// We rely on execution strategy to log it
try {
logCommandIfRequired(line, false);
@@ -294,8 +298,11 @@ public abstract class AbstractShell extends AbstractShellStatusPublisher impleme
protected void handleExecutionResult(Object result) {
if (result instanceof Iterable<?>) {
for (Object o : (Iterable<?>) result) {
logger.info(o.toString());
handleExecutionResult(o);
}
} else if (result instanceof TerminalSizeAware) {
int width = TerminalFactory.get().getWidth();
logger.info(((TerminalSizeAware) result).render(width).toString());
} else {
logger.info(result.toString());
}

View File

@@ -29,7 +29,7 @@ import java.util.TreeMap;
* @see TableRenderer
*
* @author Gunnar Hillert
*
* @deprecated In favor of {@link org.springframework.shell.table.TableBuilder}
*/
public class Table {

View File

@@ -22,7 +22,8 @@ package org.springframework.shell.support.table;
* @see TableRenderer
*
* @author Gunnar Hillert
*
* @deprecated In favor of {@link org.springframework.shell.table.TableBuilder}
*
*/
public class TableHeader {

View File

@@ -20,17 +20,18 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.shell.support.util.StringUtils;
import com.google.common.base.Splitter;
import org.springframework.shell.support.util.StringUtils;
/**
* Contains utility methods for rendering data to a formatted console output.
* E.g. it provides helper methods for rendering ASCII-based data tables.
*
* @author Gunnar Hillert
* @author Thomas Risberg
*
* @deprecated In favor of {@link org.springframework.shell.table.TableBuilder}
*
*/
public final class TableRenderer {

View File

@@ -26,7 +26,8 @@ import java.util.Map;
*
* @author Gunnar Hillert
* @author Ilayaperumal Gopinathan
*
* @deprecated In favor of {@link org.springframework.shell.table.TableBuilder}
*
*/
public class TableRow {

View File

@@ -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);
}
}

View File

@@ -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 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.
*
* <p>Input array is guaranteed to contain lines that have length equal to {@cellWidth}. There
* is no guarantee on the input number of lines though.</p>
*/
String[] align(String[] text, int cellWidth, int cellHeight);
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}

View File

@@ -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.
*
* <p>One can control which properties are exposed (and their order). There is also
* a convenience constructor for adding a special header row.</p>
*
* @author Eric Bottard
*/
public class BeanListTableModel<T> extends TableModel {
private final List<BeanWrapper> data;
private final List<String> propertyNames;
private final List<Object> headerRow;
public BeanListTableModel(Class<T> clazz, Iterable<T> list) {
this.data = new ArrayList<BeanWrapper>();
for (T bean : list) {
this.data.add(new BeanWrapperImpl(bean));
}
this.headerRow = null;
propertyNames = new ArrayList<String>();
for (PropertyDescriptor propertyName : BeanUtils.getPropertyDescriptors(clazz)) {
if ("class".equals(propertyName.getName())) {
continue;
}
propertyNames.add(propertyName.getName());
}
}
public BeanListTableModel(Iterable<T> list, String... propertyNames) {
this.data = new ArrayList<BeanWrapper>();
for (T bean : list) {
this.data.add(new BeanWrapperImpl(bean));
}
this.headerRow = null;
this.propertyNames = Arrays.asList(propertyNames);
}
public BeanListTableModel(Iterable<T> list, LinkedHashMap<String, Object> header) {
this.data = new ArrayList<BeanWrapper>();
for (T bean : list) {
this.data.add(new BeanWrapperImpl(bean));
}
this.headerRow = new ArrayList<Object>(header.values());
propertyNames = new ArrayList<String>(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);
}
}
}

View File

@@ -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<String> constants = new ArrayList<String>();
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);
}
}
}

View File

@@ -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 <a href="https://en.wikipedia.org/wiki/Box-drawing_character">https://en.wikipedia.org/wiki/Box-drawing_character</a>
*
* @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<Long, Character> CORNERS = new HashMap<Long, Character>();
private static Map<Character, Character> EQUIVALENTS = new HashMap<Character, Character>();
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;
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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;
/**
* This is used to specify where some components of a Table may be applied.
*
* <p>Some commonly used matchers can be created <i>via</i> {@link CellMatchers}.</p>
*
* @author Eric Bottard
*/
public interface CellMatcher {
/**
* Return whether a given cell of the table should match.
*/
public boolean matches(int row, int column, TableModel model);
}

View File

@@ -0,0 +1,73 @@
/*
* 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;
/**
* 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.
*/
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.
*/
public static CellMatcher row(final int theRow) {
return new CellMatcher() {
public boolean matches(int row, int column, TableModel model) {
return theRow == row;
}
};
}
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());
}
}
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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<String> result = new ArrayList<String>(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);
}
result.add(String.format("%-" + columnWidth + "s", line)); // right pad if necessary
}
return result.toArray(new String[result.size()]);
}
}

View File

@@ -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.
*
* <p>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 (<i>e.g.</i> format numbers).</p>
*
* @author Eric Bottard
*/
public interface Formatter {
public String[] format(Object value);
}

View File

@@ -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.
*
* <p>Typically used to render numbers which may or may not have a decimal point, or series of key-value pairs</p>
*
* @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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> result = new ArrayList<String>();
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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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;
for (int row = 0; row < text.length; row++) {
if (text[row] == null || text[row].trim().equals("")) {
blanksBefore++;
}
else {
break;
}
}
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);
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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;
/**
* 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;
}
}
}

View File

@@ -0,0 +1,361 @@
/*
* 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 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 <i>e.g.</i> in a spreadsheet
* program:<ol>
* <li>{@link #format(CellMatcher, Formatter) 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</li>
* <li>{@link #size(CellMatcher, SizeConstraints) size constraints} are then applied, which decide how
* much column real estate to allocate to cells</li>
* <li>{@link #wrap(CellMatcher, TextWrapper) text wrapping policies} are applied once the column sizes
* are known</li>
* <li>finally, {@link #align(CellMatcher, Aligner) alignment} strategies actually render
* text as a series of space-padded strings that draw nicely on screen.</li>
* </ol>
* 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}.
*
* <p>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.</p>
* @author Eric Bottard
*/
public class Table implements TerminalSizeAware {
private final int rows;
private final int columns;
private TableModel model;
private Map<CellMatcher, Formatter> formatters = new LinkedHashMap<CellMatcher, Formatter>();
private Map<CellMatcher, SizeConstraints> sizeConstraints = new LinkedHashMap<CellMatcher, SizeConstraints>();
private Map<CellMatcher, TextWrapper> wrappers = new LinkedHashMap<CellMatcher, TextWrapper>();
private Map<CellMatcher, Aligner> aligners = new LinkedHashMap<CellMatcher, Aligner>();
private List<BorderSpecification> borderSpecifications = new ArrayList<BorderSpecification>();
/**
* Construct a new Table with the given model and customizers.
* The passed in LinkedHashMap should be in reverse-insertion order (<i>i.e.</i> the first CellMatcher
* found in iteration order will "win").
*
* @see TableBuilder#build()
*/
/*package*/ Table(TableModel model,
LinkedHashMap<CellMatcher, Formatter> formatters,
LinkedHashMap<CellMatcher, SizeConstraints> sizeConstraints,
LinkedHashMap<CellMatcher, TextWrapper> wrappers,
LinkedHashMap<CellMatcher, Aligner> aligners,
List<BorderSpecification> 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<CellMatcher, Aligner> 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<CellMatcher, TextWrapper> 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<CellMatcher, SizeConstraints> 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<CellMatcher, Formatter> 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.
*
* <p>In all instance arrays, 'row' and 'column' are actually indices in-between
* table rows and columns. Hence, sizes are larger by one.</p>
* @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;
}
}
}

View File

@@ -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 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<CellMatcher, Formatter> formatters = new LinkedHashMap<CellMatcher, Formatter>();
private final Map<CellMatcher, SizeConstraints> sizeConstraints = new LinkedHashMap<CellMatcher, SizeConstraints>();
private final Map<CellMatcher, TextWrapper> wrappers = new LinkedHashMap<CellMatcher, TextWrapper>();
private final LinkedHashMap<CellMatcher, Aligner> aligners = new LinkedHashMap<CellMatcher, Aligner>();
private final List<BorderSpecification> borderSpecifications = new ArrayList<BorderSpecification>();
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:<ul>
* <li>{@link DefaultFormatter default formatting} using {@literal toString()}</li>
* <li>{@link AutoSizeConstraints sizing strategy} trying to use the maximum space, resorting to splitting lines on
* spaces</li>
* <li>{@link DelimiterTextWrapper wrapping text} on space characters</li>
* <li>{@link SimpleHorizontalAligner left aligning} text.</li>
* </ul>
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
public TableBuilder addInnerBorder(BorderStyle style) {
this.addBorder(0, 0, model.getRowCount(), model.getColumnCount(), INNER, style);
return this;
}
private <K, V> LinkedHashMap<K, V> reverse(Map<K, V> original) {
LinkedHashMap<K, V> result = new LinkedHashMap<K, V>(original.size());
List<Map.Entry<K, V>> entries = new ArrayList<Map.Entry<K, V>>(original.entrySet());
for (int i = entries.size() - 1; i >= 0; i--) {
Map.Entry<K, V> 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());
}
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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;
/**
* 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.
*/
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);
}
};
}
}

View File

@@ -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<T> {
public static final int DEFAULT_ROW_CAPACITY = 3;
private List<List<T>> rows = new ArrayList<List<T>>();
private int previousRowSize = -1;
private boolean frozen;
public TableModelBuilder<T> 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<T>(previousRowSize == -1 ? DEFAULT_ROW_CAPACITY : previousRowSize));
return this;
}
public TableModelBuilder<T> 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);
}
};
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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;
/**
* 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.
*/
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();
}
}

View File

@@ -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 strategy for applying text wrapping/cropping given a cell width.
*/
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).
*/
String[] wrap(String[] original, int columnWidth);
}

View File

@@ -0,0 +1,62 @@
/*
* 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.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.junit.Rule;
import org.junit.rules.TestName;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
/**
* Base class that allows reading a sample result rendering of a table, based on the actual
* class and method name of the test.
*
* @author Eric Bottard
*/
public class AbstractTestWithSample {
@Rule
public TestName testName = new TestName();
protected String sample() throws IOException {
String sampleName = String.format("%s-%s.txt",
this.getClass().getSimpleName(), testName.getMethodName());
InputStream stream = TableTest.class.getResourceAsStream(sampleName);
Assert.notNull(stream, "Can't find expected rendering result at " + sampleName);
return FileCopyUtils.copyToString(new InputStreamReader(stream)).replace("&", "");
}
/**
* Generate a simple rows x columns model made of chars.
*/
protected TableModel generate(int rows, int columns) {
Character[][] data = new Character[rows][columns];
for (int row = 0; row < rows; row++) {
data[row] = new Character[columns];
for (int column = 0; column < columns; column++) {
data[row][column] = (char) ('a' + row * columns + column);
}
}
return new ArrayTableModel(data);
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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 static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Tests for ArrayTableModel.
*
* @author Eric Bottard
*/
public class ArrayTableModelTest {
@Test
public void testValid() {
TableModel model = new ArrayTableModel(new String[][] {{"a", "b"}, {"c", "d"}});
assertThat(model.getColumnCount(), equalTo(2));
assertThat(model.getRowCount(), equalTo(2));
assertThat(model.getValue(0, 1), equalTo((Object) "b"));
}
@Test
public void testEmpty() {
TableModel model = new ArrayTableModel(new String[][] {});
assertThat(model.getColumnCount(), equalTo(0));
assertThat(model.getRowCount(), equalTo(0));
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidDimensions() {
new ArrayTableModel(new String[][] {{"a", "b"}, {"c", "d", "e"}});
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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 static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import org.junit.Test;
/**
* Test for BeanListTableModel.
*
* @author Eric Bottard
*/
public class BeanListTableModelTest extends AbstractTestWithSample {
@Test
public void testSimpleConstructor() throws IOException {
List<Person> data = data();
Table table = new TableBuilder(new BeanListTableModel<Person>(Person.class, data)).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testExplicitPropertyNames() throws IOException {
List<Person> data = data();
Table table = new TableBuilder(new BeanListTableModel<Person>(data, "lastName", "firstName")).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testHeaderRow() throws IOException {
List<Person> data = data();
LinkedHashMap<String, Object> header = new LinkedHashMap<String, Object>();
header.put("lastName", "Last Name");
header.put("firstName", "First Name");
Table table = new TableBuilder(new BeanListTableModel<Person>(data, header)).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
private List<Person> data() {
List<Person> data = new ArrayList<Person>();
data.add(new Person("Alice", "Clark", 12));
data.add(new Person("Bob", "Smith", 42));
data.add(new Person("Sarah", "Connor", 38));
return data;
}
public static class Person {
private int age;
private String firstName;
private String lastName;
public Person(String firstName, String lastName, int age) {
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
public int getAge() {
return age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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 static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.springframework.shell.table.BorderStyle.fancy_double;
import java.io.IOException;
import org.junit.Test;
/**
* Tests for convenience borders factory.
*
* @author Eric Bottard
*/
public class BorderFactoryTest extends AbstractTestWithSample {
@Test
public void testOutlineBorder() throws IOException {
TableModel model = generate(3, 3);
Table table = new TableBuilder(model).addOutlineBorder(fancy_double).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testFullBorder() throws IOException {
TableModel model = generate(3, 3);
Table table = new TableBuilder(model).addFullBorder(fancy_double).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testHeaderBorder() throws IOException {
TableModel model = generate(3, 3);
Table table = new TableBuilder(model).addHeaderBorder(fancy_double).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testHeaderAndVerticalsBorder() throws IOException {
TableModel model = generate(3, 3);
Table table = new TableBuilder(model).addHeaderAndVerticalsBorders(fancy_double).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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 static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import org.junit.Test;
/**
* Tests for BorderStyle rendering and combinations.
*
* @author Eric Bottard
*/
public class BorderStyleTests extends AbstractTestWithSample {
@Test
public void testOldSchool() throws IOException {
Table table = new TableBuilder(generate(2, 2)).addFullBorder(BorderStyle.oldschool).build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testFancySimple() throws IOException {
Table table = new TableBuilder(generate(2, 2)).addFullBorder(BorderStyle.fancy_light).build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testFancyHeavy() throws IOException {
Table table = new TableBuilder(generate(2, 2)).addFullBorder(BorderStyle.fancy_heavy).build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testFancyDouble() throws IOException {
Table table = new TableBuilder(generate(2, 2)).addFullBorder(BorderStyle.fancy_double).build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testAir() throws IOException {
Table table = new TableBuilder(generate(2, 2)).addFullBorder(BorderStyle.air).build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedOldSchoolWithAir() throws IOException {
Table table = new TableBuilder(generate(2, 2))
.addFullBorder(BorderStyle.air)
.addOutlineBorder(BorderStyle.oldschool)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedFancyLightAndHeavy() throws IOException {
Table table = new TableBuilder(generate(2, 2))
.addFullBorder(BorderStyle.fancy_heavy)
.addOutlineBorder(BorderStyle.fancy_light)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedFancyHeavyAndLight() throws IOException {
Table table = new TableBuilder(generate(2, 2))
.addFullBorder(BorderStyle.fancy_light)
.addOutlineBorder(BorderStyle.fancy_heavy)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedDoubleAndSingle() throws IOException {
Table table = new TableBuilder(generate(2, 2))
.addFullBorder(BorderStyle.fancy_light)
.addOutlineBorder(BorderStyle.fancy_double)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedSingleAndDouble() throws IOException {
Table table = new TableBuilder(generate(2, 2))
.addFullBorder(BorderStyle.fancy_double)
.addOutlineBorder(BorderStyle.fancy_light)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedLightInternalAndHeavy() throws IOException {
Table table = new TableBuilder(generate(3, 3))
.addFullBorder(BorderStyle.fancy_heavy)
.paintBorder(BorderStyle.fancy_light, BorderSpecification.OUTLINE).fromRowColumn(1, 1).toRowColumn(2, 2)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testMixedHeavyInternalAndLight() throws IOException {
Table table = new TableBuilder(generate(3, 3))
.addFullBorder(BorderStyle.fancy_light)
.paintBorder(BorderStyle.fancy_heavy, BorderSpecification.OUTLINE).fromRowColumn(1, 1).toRowColumn(2, 2)
.build();
assertThat(table.render(10), is(sample()));
}
@Test
public void testHeavyOutlineAndHeader_LightVerticals_AirHorizontals() throws IOException {
Table table = new TableBuilder(generate(4, 4))
.addOutlineBorder(BorderStyle.fancy_heavy)
.paintBorder(BorderStyle.fancy_light, BorderSpecification.INNER_VERTICAL).fromTopLeft().toBottomRight()
.paintBorder(BorderStyle.air, BorderSpecification.INNER_HORIZONTAL).fromTopLeft().toBottomRight()
.paintBorder(BorderStyle.fancy_heavy, BorderSpecification.OUTLINE).fromTopLeft().toRowColumn(1, 4)
.build();
assertThat(table.render(10), is(sample()));
}
}

View File

@@ -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 static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Tests for DelimiterTextWrapper.
*
* @author Eric Bottard
*/
public class DelimiterTextWrapperTest {
private TextWrapper wrapper = new DelimiterTextWrapper();
@Test
public void testNoWordSplit() {
String[] text = new String[] {"the quick brown fox jumps over the lazy dog."};
assertThat(wrapper.wrap(text, 10),
arrayContaining("the quick ", "brown fox ", "jumps over", "the lazy ", "dog. "));
}
@Test
public void testWordSplit() {
String[] text = new String[] {"the quick brown fox jumps over the lazy dog."};
assertThat(wrapper.wrap(text, 4),
arrayContaining("the ", "quic", "k ", "brow", "n ", "fox ", "jump", "s ", "over", "the ", "lazy", "dog."));
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
/**
* Tests related to rendering Maps.
*
* @author Eric Bottard
*/
public class KeyValueRenderingTests extends AbstractTestWithSample {
@Test
public void testRenderConstrained() throws IOException {
Map<String, String> values = new LinkedHashMap<String, String>();
values.put("a", "b");
values.put("long-key", "c");
values.put("d", "long-value");
TableModel model = new ArrayTableModel(new Object[][] {{"Thing", "Properties"}, {"Something", values}});
TableBuilder tableBuilder = new TableBuilder(model)
.addHeaderAndVerticalsBorders(BorderStyle.fancy_light);
Tables.configureKeyValueRendering(tableBuilder, " = ");
Table table = tableBuilder.build();
String result = table.render(10);
assertThat(result, is(sample()));
}
@Test
public void testRenderUnconstrained() throws IOException {
Map<String, String> values = new LinkedHashMap<String, String>();
values.put("a", "b");
values.put("long-key", "c");
values.put("d", "long-value");
TableModel model = new ArrayTableModel(new Object[][] {{"Thing", "Properties"}, {"Something", values}});
TableBuilder tableBuilder = new TableBuilder(model)
.addHeaderAndVerticalsBorders(BorderStyle.fancy_light);
Tables.configureKeyValueRendering(tableBuilder, " = ");
Table table = tableBuilder.build();
String result = table.render(80);
assertThat(result, is(sample()));
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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 static org.hamcrest.CoreMatchers.is;
import org.junit.Assert;
import org.junit.Test;
/**
* Tests for TableModelBuilder.
*
* @author Eric Bottard
*/
public class TableModelBuilderTests {
@Test
public void emptyModel() {
TableModelBuilder<Number> builder = new TableModelBuilder<Number>();
TableModel model = builder.build();
Assert.assertThat(model.getColumnCount(), is(0));
Assert.assertThat(model.getRowCount(), is(0));
}
@Test(expected = IllegalArgumentException.class)
public void testFrozen() {
TableModelBuilder<Number> builder = new TableModelBuilder<Number>();
builder.addRow().addValue(5);
builder.build();
builder.addRow();
}
@Test(expected = IllegalArgumentException.class)
public void testAddingTooManyValues() {
TableModelBuilder<Number> builder = new TableModelBuilder<Number>();
builder.addRow().addValue(5);
builder.addRow().addValue(1).addValue(2);
builder.build();
}
@Test(expected = IllegalArgumentException.class)
public void testAddingLessValues() {
TableModelBuilder<Number> builder = new TableModelBuilder<Number>();
builder.addRow().addValue(1).addValue(2);
builder.addRow().addValue(5);
builder.addRow();
builder.build();
}
@Test
public void simpleBuild() {
TableModelBuilder<Number> builder = new TableModelBuilder<Number>();
builder
.addRow()
.addValue(7).addValue(2)
.addRow()
.addValue(3).addValue(5.5)
.addRow()
.addValue(1).addValue(4);
TableModel model = builder.build();
Assert.assertThat(model.getColumnCount(), is(2));
Assert.assertThat(model.getRowCount(), is(3));
Assert.assertThat(model.getValue(1, 1), is((Object)5.5));
}
}

View File

@@ -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;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Tests for TableModel.
*
* @author Eric Bottard
*/
public class TableModelTest {
@Test
public void testTranspose() {
TableModel model = new ArrayTableModel(new String[][] {{"a", "b", "c"}, {"d", "e", "f"}});
assertThat(model.transpose().getColumnCount(), equalTo(2));
assertThat(model.transpose().getRowCount(), equalTo(3));
assertThat(model.transpose().getValue(2, 1), equalTo((Object) "f"));
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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 static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.shell.table.SimpleHorizontalAligner.*;
import static org.springframework.shell.table.SimpleVerticalAligner.*;
import java.io.IOException;
import org.junit.Test;
/**
* Tests for Table rendering.
*
* @author Eric Bottard
*/
public class TableTest extends AbstractTestWithSample {
@Test
public void testEmptyModel() {
TableModel model = new ArrayTableModel(new Object[0][0]);
Table table = new TableBuilder(model).build();
String result = table.render(80);
assertThat(result, equalTo(""));
}
@Test
public void testPreformattedModel() {
TableModel model = generate(2, 2);
Table table = new TableBuilder(model).build();
String result = table.render(80);
assertThat(result, equalTo("ab\ncd\n"));
}
@Test
public void testExpandingColumns() throws IOException {
TableModel model = new ArrayTableModel(new String[][] {{"a", "b"}, {"ccc", "d"}});
Table table = new TableBuilder(model).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testRightAlignment() throws IOException {
TableModel model = new ArrayTableModel(new String[][] {{"a\na\na", "bbb"}, {"ccc", "d"}});
Table table = new TableBuilder(model).on(CellMatchers.column(1)).addAligner(right).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testVerticalAlignment() throws IOException {
TableModel model = new ArrayTableModel(new String[][] {{"a\na\na", "bbb"}, {"ccc", "d"}});
Table table = new TableBuilder(model).on(CellMatchers.row(0)).addAligner(middle).build();
String result = table.render(80);
assertThat(result, equalTo(sample()));
}
@Test
public void testAutoWrapping() throws IOException {
TableModel model = new ArrayTableModel(new String[][] {{"this is a long line", "bbb"}, {"ccc", "d"}});
Table table = new TableBuilder(model).build();
String result = table.render(10);
assertThat(result, equalTo(sample()));
}
@Test
public void testOverflow() throws IOException {
TableModel model = new ArrayTableModel(new String[][] {{"this is a long line", "bbb"}, {"ccc", "d"}});
Table table = new TableBuilder(model).build();
String result = table.render(3);
assertThat(result, equalTo(sample()));
}
}

View File

@@ -0,0 +1,3 @@
Clark Alice&
Smith Bob &
ConnorSarah&

View File

@@ -0,0 +1,4 @@
Last NameFirst Name&
Clark Alice &
Smith Bob &
Connor Sarah &

View File

@@ -0,0 +1,3 @@
12AliceClark &
42Bob Smith &
38SarahConnor&

View File

@@ -0,0 +1,7 @@
╔═╦═╦═╗
║a║b║c║
╠═╬═╬═╣
║d║e║f║
╠═╬═╬═╣
║g║h║i║
╚═╩═╩═╝

View File

@@ -0,0 +1,6 @@
╔═╦═╦═╗
║a║b║c║
╠═╬═╬═╣
║d║e║f║
║g║h║i║
╚═╩═╩═╝

View File

@@ -0,0 +1,6 @@
╔═══╗
║abc║
╠═══╣
║def║
║ghi║
╚═══╝

View File

@@ -0,0 +1,5 @@
╔═══╗
║abc║
║def║
║ghi║
╚═══╝

View File

@@ -0,0 +1,5 @@
&
a b &
&
c d &
&

View File

@@ -0,0 +1,5 @@
╔═╦═╗
║a║b║
╠═╬═╣
║c║d║
╚═╩═╝

View File

@@ -0,0 +1,5 @@
┏━┳━┓
┃a┃b┃
┣━╋━┫
┃c┃d┃
┗━┻━┛

View File

@@ -0,0 +1,5 @@
┌─┬─┐
│a│b│
├─┼─┤
│c│d│
└─┴─┘

View File

@@ -0,0 +1,9 @@
┏━┯━┯━┯━┓
┃a│b│c│d┃
┣━┿━┿━┿━┫
┃e│f│g│h┃
┃ │ │ │ ┃
┃i│j│k│l┃
┃ │ │ │ ┃
┃m│n│o│p┃
┗━┷━┷━┷━┛

View File

@@ -0,0 +1,5 @@
╔═╤═╗
║a│b║
╟─┼─╢
║c│d║
╚═╧═╝

View File

@@ -0,0 +1,5 @@
┏━┯━┓
┃a│b┃
┠─┼─┨
┃c│d┃
┗━┷━┛

View File

@@ -0,0 +1,5 @@
┌─┰─┐
│a┃b│
┝━╋━┥
│c┃d│
└─┸─┘

View File

@@ -0,0 +1,7 @@
┌─┬─┬─┐
│a│b│c│
├─╆━╅─┤
│d┃e┃f│
├─╄━╃─┤
│g│h│i│
└─┴─┴─┘

View File

@@ -0,0 +1,7 @@
┏━┳━┳━┓
┃a┃b┃c┃
┣━╃─╄━┫
┃d│e│f┃
┣━╅─╆━┫
┃g┃h┃i┃
┗━┻━┻━┛

View File

@@ -0,0 +1,5 @@
+---+
|a b|
| |
|c d|
+---+

View File

@@ -0,0 +1,5 @@
┌─╥─┐
│a║b│
╞═╬═╡
│c║d│
└─╨─┘

View File

@@ -0,0 +1,5 @@
+-+-+
|a|b|
+-+-+
|c|d|
+-+-+

View File

@@ -0,0 +1,9 @@
┌─────────┬──────────┐
│Thing │Properties│
├─────────┼──────────┤
│Something│a = b │
│ │long-key │
│ │ = c │
│ │d = │
│ │long-value│
└─────────┴──────────┘

View File

@@ -0,0 +1,7 @@
┌─────────┬─────────────────────┐
│Thing │Properties │
├─────────┼─────────────────────┤
│Something│ a = b │
│ │long-key = c │
│ │ d = long-value│
└─────────┴─────────────────────┘

View File

@@ -0,0 +1,4 @@
this isbbb&
a long &
line &
ccc d &

View File

@@ -0,0 +1,5 @@
thisbbb&
is a &
long &
line &
ccc d &

View File

@@ -0,0 +1,4 @@
a bbb&
a &
a &
ccc d&

View File

@@ -0,0 +1,4 @@
a &
a bbb&
a &
cccd &