diff --git a/cypher-shell/src/dist/cypher-shell b/cypher-shell/src/dist/cypher-shell index 1c569f36..66a0246d 100755 --- a/cypher-shell/src/dist/cypher-shell +++ b/cypher-shell/src/dist/cypher-shell @@ -85,6 +85,6 @@ if [ -z "${JARPATH}" ]; then exit 1 fi -exec "$JAVA_CMD" ${JAVA_OPTS:-} \ +exec "$JAVA_CMD" ${JAVA_OPTS:-} -Dterminal.columns=${COLUMNS:-} \ -jar "$JARPATH" \ "$@" diff --git a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java index aab634dd..125c5c95 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,7 +33,7 @@ public class CypherShell implements StatementExecuter, Connector, TransactionHan protected CommandHelper commandHelper; public CypherShell(@Nonnull Logger logger) { - this(logger, new BoltStateHandler(), new PrettyPrinter(logger.getFormat())); + this(logger, new BoltStateHandler(), new PrettyPrinter(logger.getFormat(), logger.getWidth(), logger.getWrap())); } protected CypherShell(@Nonnull Logger logger, @@ -81,7 +82,11 @@ public void execute(@Nonnull final String cmdString) throws ExitException, Comma */ protected void executeCypher(@Nonnull final String cypher) throws CommandException { final Optional result = boltStateHandler.runCypher(cypher, queryParams); - result.ifPresent(boltResult -> logger.printOut(prettyPrinter.format(boltResult))); + result.ifPresent(boltResult -> prettyPrinter.format(boltResult, printer())); + } + + private Consumer printer() { + return (text) -> {if (text!=null && !text.trim().isEmpty()) logger.printOut(text);}; } @Override @@ -136,7 +141,7 @@ public void beginTransaction() throws CommandException { @Override public Optional> commitTransaction() throws CommandException { Optional> results = boltStateHandler.commitTransaction(); - results.ifPresent(boltResult -> boltResult.forEach(result -> logger.printOut(prettyPrinter.format(result)))); + results.ifPresent(boltResult -> boltResult.forEach(result -> prettyPrinter.format(result, printer()))); return results; } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/Main.java b/cypher-shell/src/main/java/org/neo4j/shell/Main.java index e0bc75f8..81e5a12c 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/Main.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/Main.java @@ -88,6 +88,8 @@ private Logger instantiateLogger(@Nonnull CliArgs cliArgs) { if (cliArgs.isStringShell() && Format.AUTO.equals(cliArgs.getFormat())) { logger.setFormat(Format.PLAIN); } + logger.setWidth(cliArgs.getWidth()); + logger.setWrap(cliArgs.getWrap()); return logger; } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java b/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java index b7591697..1355ce03 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java @@ -11,9 +11,7 @@ import javax.annotation.Nonnull; import java.io.IOException; -import static org.fusesource.jansi.internal.CLibrary.STDIN_FILENO; -import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; -import static org.fusesource.jansi.internal.CLibrary.isatty; +import static org.fusesource.jansi.internal.CLibrary.*; import static org.neo4j.shell.system.Utils.isWindows; public interface ShellRunner { @@ -93,6 +91,23 @@ static boolean isInputInteractive() { } } + static int ttyColumns() { + String cols = System.getProperty("terminal.columns"); + if (cols != null && !cols.trim().isEmpty()) return Integer.parseInt(cols); + if (isOutputInteractive()) { + try { + WinSize winSize = new WinSize(); + if (ioctl(STDOUT_FILENO, TIOCSWINSZ, winSize) == 0) { + System.err.printf("row %d col %d px x %d px y %d%n",winSize.ws_row,winSize.ws_col,winSize.ws_xpixel,winSize.ws_ypixel); + if (winSize.ws_col > 0) return winSize.ws_col; + } + } catch (Throwable ignored) { + // system is not using libc (like Alpine Linux) + } + } + return -1; + } + /** * Checks if STDOUT is a TTY. In case TTY checking is not possible (lack of libc), then the check falls back to * the built in Java {@link System#console()} which checks if EITHER STDIN or STDOUT has been redirected. diff --git a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java index 224de20c..3aab94da 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java @@ -5,11 +5,7 @@ import net.sourceforge.argparse4j.impl.action.StoreTrueArgumentAction; import net.sourceforge.argparse4j.impl.choice.CollectionArgumentChoice; import net.sourceforge.argparse4j.impl.type.BooleanArgumentType; -import net.sourceforge.argparse4j.inf.ArgumentGroup; -import net.sourceforge.argparse4j.inf.ArgumentParser; -import net.sourceforge.argparse4j.inf.ArgumentParserException; -import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; -import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.*; import java.io.PrintWriter; import java.util.regex.Matcher; @@ -88,6 +84,9 @@ public static CliArgs parse(@Nonnull String... args) { cliArgs.setNonInteractive(ns.getBoolean("force-non-interactive")); + cliArgs.setWidth(ns.getInt("width")); + cliArgs.setWrap(ns.getBoolean("wrap")); + cliArgs.setVersion(ns.getBoolean("version")); return cliArgs; @@ -165,6 +164,15 @@ private static ArgumentParser setupParser() .dest("force-non-interactive") .action(new StoreTrueArgumentAction()); + parser.addArgument("--width") + .help("terminal width, only for table format") + .type(new WidthArgumentType()) + .setDefault(-1); + parser.addArgument("--wrap") + .help("wrap table colum values if table is too narrow, default true") + .type(new BooleanArgumentType()) + .setDefault(true); + parser.addArgument("-v", "--version") .help("print version of cypher-shell and exit") .action(new StoreTrueArgumentAction()); @@ -177,4 +185,16 @@ private static ArgumentParser setupParser() } + private static class WidthArgumentType implements ArgumentType { + @Override + public Integer convert(ArgumentParser parser, Argument arg, String value) throws ArgumentParserException { + try { + int result = Integer.parseInt(value); + if (result < 1) throw new NumberFormatException(value); + return result; + } catch (NumberFormatException nfe) { + throw new ArgumentParserException("Invalid width value: "+value,parser); + } + } + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java index 6cc1e2a2..70e6d9ec 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java @@ -1,5 +1,7 @@ package org.neo4j.shell.cli; +import org.neo4j.shell.ShellRunner; + import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Optional; @@ -17,6 +19,8 @@ public class CliArgs { private boolean debugMode; private boolean nonInteractive = false; private boolean version = false; + private int width = ShellRunner.ttyColumns(); + private boolean wrap = true; /** * Set the scheme to the primary value, or if null, the fallback value. @@ -158,4 +162,23 @@ public void setVersion(boolean version) { public boolean isStringShell() { return cypher.isPresent(); } + + public void setWidth(Integer width) { + if (width != null && width > 0) { + this.width = width; + } + } + + @Nonnull + public int getWidth() { + return width; + } + + public boolean getWrap() { + return wrap; + } + + public void setWrap(boolean wrap) { + this.wrap = wrap; + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java b/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java index 85bde103..4ebda151 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java @@ -7,6 +7,7 @@ import org.neo4j.shell.exception.AnsiFormattedException; import javax.annotation.Nonnull; +import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; @@ -23,6 +24,8 @@ public class AnsiLogger implements Logger { private final PrintStream err; private final boolean debug; private Format format; + private int width; + private boolean wrap = true; public AnsiLogger(final boolean debug) { this(debug, Format.VERBOSE, System.out, System.err); @@ -66,6 +69,19 @@ private static boolean isOutputInteractive() { return 1 == isatty(STDOUT_FILENO) && 1 == isatty(STDERR_FILENO); } + public int getWidth() { + return width != -1 || isOutputInteractive() ? width : -1; + } + + @Override + public boolean getWrap() { + return wrap; + } + + public void setWrap(boolean wrap) { + this.wrap = wrap; + } + @Nonnull @Override public PrintStream getOutputStream() { @@ -94,6 +110,11 @@ public boolean isDebugEnabled() { return debug; } + @Override + public void setWidth(int width) { + this.width = width; + } + @Override public void printError(@Nonnull Throwable throwable) { printError(getFormattedMessage(throwable)); @@ -131,9 +152,9 @@ String getFormattedMessage(@Nonnull final Throwable e) { cause.getMessage() != null && cause.getMessage().contains("Missing username")) { // Username and password was not specified msg = msg.append(cause.getMessage()) - .append("\nPlease specify --username, and optionally --password, as argument(s)") - .append("\nor as environment variable(s), NEO4J_USERNAME, and NEO4J_PASSWORD respectively.") - .append("\nSee --help for more info."); + .append("\nPlease specify --username, and optionally --password, as argument(s)") + .append("\nor as environment variable(s), NEO4J_USERNAME, and NEO4J_PASSWORD respectively.") + .append("\nSee --help for more info."); } else { if (cause.getMessage() != null) { msg = msg.append(cause.getMessage()); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java b/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java index c91c3b8e..827bf733 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java @@ -92,4 +92,11 @@ default void printIfPlain(@Nonnull String text) { printOut(text); } } + + void setWidth(int width); + int getWidth(); + + boolean getWrap(); + + void setWrap(boolean wrap); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java index 0af5eeaa..679f740a 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java @@ -11,6 +11,7 @@ import org.neo4j.shell.state.BoltResult; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -24,15 +25,18 @@ */ public interface OutputFormatter { + enum Capablities {info, plan, result, footer, statistics} + String COMMA_SEPARATOR = ", "; String COLON_SEPARATOR = ": "; String COLON = ":"; String SPACE = " "; String NEWLINE = System.getProperty("line.separator"); - @Nonnull String format(@Nonnull BoltResult result); + void format(@Nonnull BoltResult result, @Nonnull Consumer output); - @Nonnull default String formatValue(@Nonnull final Value value) { + @Nonnull default String formatValue(final Value value) { + if (value == null) return ""; TypeRepresentation type = (TypeRepresentation) value.type(); switch (type.constructor()) { case LIST_TyCon: @@ -161,6 +165,7 @@ static boolean isNotBlank(String string) { return ""; } + Set capabilities(); List INFO = asList("Version", "Planner", "Runtime"); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java index 17026f5d..bfa629fc 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java @@ -5,7 +5,8 @@ import javax.annotation.Nonnull; -import static java.util.Arrays.asList; +import java.util.Set; +import java.util.function.Consumer; /** * Print the result from neo4j in a intelligible fashion. @@ -14,17 +15,19 @@ public class PrettyPrinter { private final StatisticsCollector statisticsCollector; private final OutputFormatter outputFormatter; - public PrettyPrinter(@Nonnull Format format) { + public PrettyPrinter(@Nonnull Format format, int width, boolean wrap) { this.statisticsCollector = new StatisticsCollector(format); - this.outputFormatter = format == Format.VERBOSE ? new TableOutputFormatter() : new SimpleOutputFormatter(); + this.outputFormatter = format == Format.VERBOSE ? new TableOutputFormatter(width, wrap) : new SimpleOutputFormatter(); } - public String format(@Nonnull final BoltResult result) { - String infoOutput = outputFormatter.formatInfo(result.getSummary()); - String planOutput = outputFormatter.formatPlan(result.getSummary()); - String statistics = statisticsCollector.collect(result.getSummary()); - String resultOutput = outputFormatter.format(result); - String footer = outputFormatter.formatFooter(result); - return OutputFormatter.joinNonBlanks(OutputFormatter.NEWLINE, asList(infoOutput, planOutput, resultOutput, footer, statistics)); + public void format(@Nonnull final BoltResult result, Consumer outputConsumer) { + Set capabilities = outputFormatter.capabilities(); + + if (capabilities.contains(OutputFormatter.Capablities.result)) outputFormatter.format(result,outputConsumer); + + if (capabilities.contains(OutputFormatter.Capablities.info)) outputConsumer.accept(outputFormatter.formatInfo(result.getSummary())); + if (capabilities.contains(OutputFormatter.Capablities.plan)) outputConsumer.accept(outputFormatter.formatPlan(result.getSummary())); + if (capabilities.contains(OutputFormatter.Capablities.footer)) outputConsumer.accept(outputFormatter.formatFooter(result)); + if (capabilities.contains(OutputFormatter.Capablities.statistics)) outputConsumer.accept(statisticsCollector.collect(result.getSummary())); } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java index 6bd5ef6c..3618fd78 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java @@ -5,25 +5,26 @@ import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.shell.state.BoltResult; -import java.util.List; +import javax.annotation.Nonnull; +import java.util.EnumSet; +import java.util.Iterator; import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; -import javax.annotation.Nonnull; - public class SimpleOutputFormatter implements OutputFormatter { - @Override - @Nonnull - public String format(@Nonnull final BoltResult result) { - StringBuilder sb = new StringBuilder(); - List records = result.getRecords(); - if (!records.isEmpty()) { - sb.append(records.get(0).keys().stream().collect(Collectors.joining(COMMA_SEPARATOR))); - sb.append("\n"); - sb.append(records.stream().map(this::formatRecord).collect(Collectors.joining("\n"))); + public void format(@Nonnull BoltResult result, @Nonnull Consumer output) { + Iterator records = result.iterate(); + if (records.hasNext()) { + Record firstRow = records.next(); + output.accept(String.join(COMMA_SEPARATOR,firstRow.keys())); + output.accept(formatRecord(firstRow)); + while (records.hasNext()) { + output.accept(formatRecord(records.next())); + } } - return sb.toString(); } @Nonnull @@ -38,4 +39,9 @@ public String formatInfo(@Nonnull ResultSummary summary) { Map info = OutputFormatter.info(summary); return info.entrySet().stream().map( e -> String.format("%s: %s",e.getKey(),e.getValue())).collect(Collectors.joining(NEWLINE)); } + + @Override + public Set capabilities() { + return EnumSet.of(Capablities.info, Capablities.statistics, Capablities.result); + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java index a928657e..57f0c43b 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java @@ -1,51 +1,88 @@ package org.neo4j.shell.prettyprint; import org.neo4j.driver.internal.InternalRecord; -import org.neo4j.driver.internal.util.Iterables; import org.neo4j.driver.internal.value.MapValue; +import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.Values; import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.shell.state.BoltResult; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; +import java.util.function.Consumer; +import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; public class TableOutputFormatter implements OutputFormatter { + private static final int SAMPLE_ROWS = 100; + private final int maxWidth; + private final boolean wrap; + + public TableOutputFormatter(int maxWidth, boolean wrap) { + this.maxWidth = maxWidth; + this.wrap = wrap; + } + @Override - @Nonnull - public String format(@Nonnull final BoltResult result) { - List data = result.getRecords().stream().map(r -> new MapValue(r.asMap(v -> v))).collect(Collectors.toList()); - return formatValues(data); + public void format(@Nonnull BoltResult result, @Nonnull Consumer output) { + Iterator rows = result.iterate(); + if (!rows.hasNext()) return; + + + Record firstRow = rows.next(); + String[] columns = firstRow.keys().toArray(new String[0]); + if (columns.length == 0) return; + + List topRows = sampleRows(rows, firstRow, SAMPLE_ROWS); + + formatValues(columns,topRows, rows, output); } - @Nonnull - String formatValues(@Nonnull List data) { - if (data.isEmpty()) return ""; - List columns = Iterables.asList(data.get(0).keys()); - if (columns.isEmpty()) return ""; + private List sampleRows(Iterator rows, Record firstRow, int count) { + List topRows = new ArrayList<>(count); + topRows.add(firstRow); + while (rows.hasNext() && topRows.size() < count) topRows.add(rows.next()); + return topRows; + } - StringBuilder sb = new StringBuilder(); - Map columnSizes = calculateColumnSizes(columns, data); - String headerLine = createString(columns, columnSizes); - int lineWidth = headerLine.length() - 2; + private MapValue rowToValue(Record firstRow) { + return new MapValue(firstRow.asMap(v -> v)); + } + + private void formatValues(String[] columns, @Nonnull List topRows, Iterator records, Consumer output) { + int[] columnSizes = calculateColumnSizes(columns, topRows); + + int totalWidth = 1; + for (int columnSize : columnSizes) totalWidth += columnSize + 3; + + if (maxWidth != -1) { + float ratio = (float)maxWidth / (float)totalWidth; + totalWidth = 1; + for (int i = 0; i < columnSizes.length; i++) { + columnSizes[i] = Math.round(columnSizes[i]*ratio); // todo better distribution (of remainder) + totalWidth += columnSizes[i] + 3; + } + } + + StringBuilder builder = new StringBuilder(totalWidth); + String headerLine = createString(columns, columnSizes, builder); + int lineWidth = totalWidth - 2; String dashes = "+" + OutputFormatter.repeat('-', lineWidth) + "+"; - sb.append(dashes).append(NEWLINE); - sb.append(headerLine).append(NEWLINE); - sb.append(dashes).append(NEWLINE); + output.accept(dashes); + output.accept(headerLine); + output.accept(dashes); - for (Value record : data) { - sb.append(createString(columns, columnSizes, record)).append(NEWLINE); + for (Record record : topRows) { + output.accept(createString(columns, columnSizes, record, builder)); } - sb.append(dashes).append(NEWLINE); - return sb.toString(); + while (records.hasNext()) { + output.accept(createString(columns, columnSizes, records.next(), builder)); + } + output.accept(dashes); } @Nonnull @@ -56,42 +93,63 @@ public String formatFooter(@Nonnull BoltResult result) { } @Nonnull - private String createString(@Nonnull List columns, @Nonnull Map columnSizes, @Nonnull Value m) { - StringBuilder sb = new StringBuilder("|"); - for (String column : columns) { + private String createString(@Nonnull String[] columns, @Nonnull int[] columnSizes, @Nonnull Record m, StringBuilder sb) { + sb.setLength(0); + String[] row = new String[columns.length]; + for (int i = 0; i < row.length; i++) { + row[i] = formatValue(m.get(i)); + } + formatRow(sb, columnSizes, row); + return sb.toString(); + } + + private void formatRow(StringBuilder sb, int[] columnSizes, String[] row) { + sb.append("|"); + boolean remainder = false; + for (int i = 0; i < row.length; i++) { sb.append(" "); - Integer length = columnSizes.get(column); - String txt = formatValue(m.get(column)); - String value = OutputFormatter.rightPad(txt, length); - sb.append(value); + int length = columnSizes[i]; + String txt = row[i]; + if (txt != null) { + if (txt.length() > length) { + row[i] = txt.substring(length); + remainder = true; + } else row[i] = null; + sb.append(OutputFormatter.rightPad(txt, length)); + } else { + sb.append(OutputFormatter.repeat(' ', length)); + } sb.append(" |"); } - return sb.toString(); + if (wrap && remainder) { + sb.append(OutputFormatter.NEWLINE); + formatRow(sb, columnSizes, row); + } } @Nonnull - private String createString(@Nonnull List columns, @Nonnull Map columnSizes) { - StringBuilder sb = new StringBuilder("|"); - for (String column : columns) { + private String createString(@Nonnull String[] columns, @Nonnull int[] columnSizes, StringBuilder sb) { + sb.setLength(0); + sb.append("|"); + for (int i = 0; i < columns.length; i++) { sb.append(" "); - sb.append(OutputFormatter.rightPad(column, columnSizes.get(column))); + sb.append(OutputFormatter.rightPad(columns[i], columnSizes[i])); sb.append(" |"); } return sb.toString(); } @Nonnull - private Map calculateColumnSizes(@Nonnull List columns, @Nonnull List data) { - Map columnSizes = new LinkedHashMap<>(); - for (String column : columns) { - columnSizes.put(column, column.length()); + private int[] calculateColumnSizes(@Nonnull String[] columns, @Nonnull List data) { + int[] columnSizes = new int[columns.length]; + for (int i = 0; i < columns.length; i++) { + columnSizes[i] = columns[i].length(); } - for (Value record : data) { - for (String column : columns) { - int len = formatValue(record.get(column)).length(); - int existing = columnSizes.get(column); - if (existing < len) { - columnSizes.put(column, len); + for (Record record : data) { + for (int i = 0; i < columns.length; i++) { + int len = formatValue(record.get(i)).length(); + if (columnSizes[i] < len) { + columnSizes[i] = len; } } } @@ -102,7 +160,12 @@ private Map calculateColumnSizes(@Nonnull List columns, @Nonnull public String formatInfo(@Nonnull ResultSummary summary) { Map info = OutputFormatter.info(summary); - return formatValues(Collections.singletonList(new MapValue(info))); + if (info.isEmpty()) return ""; + String[] columns = info.keySet().toArray(new String[info.size()]); + StringBuilder sb = new StringBuilder(); + Record record = new InternalRecord(asList(columns), info.values().toArray(new Value[info.size()])); + formatValues(columns, Collections.singletonList(record), Collections.emptyIterator(), sb::append); + return sb.toString(); } @Override @@ -111,4 +174,9 @@ public String formatPlan(@Nullable ResultSummary summary) { if (summary == null || !summary.hasPlan()) return ""; return new TablePlanFormatter().formatPlan(summary.plan()); } + + @Override + public Set capabilities() { + return EnumSet.allOf(Capablities.class); + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java index e6cbad1a..00c2714d 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java @@ -4,28 +4,20 @@ import org.neo4j.driver.v1.summary.ResultSummary; import javax.annotation.Nonnull; +import java.util.Iterator; import java.util.List; /** - * A class holds the result from executing some Cypher. + * @author mh + * @since 26.01.18 */ -public class BoltResult { - private final List records; - private final ResultSummary summary; - - public BoltResult(@Nonnull List records, @Nonnull ResultSummary summary) { - - this.records = records; - this.summary = summary; - } +public interface BoltResult { + @Nonnull + List getRecords(); @Nonnull - public List getRecords() { - return records; - } + Iterator iterate(); @Nonnull - public ResultSummary getSummary() { - return summary; - } + ResultSummary getSummary(); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java index b1901813..9f2f2865 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java @@ -175,7 +175,7 @@ private Optional getBoltResult(@Nonnull String cypher, @Nonnull Map< } // calling list()/consume() is what actually executes cypher on the server - return Optional.of(new BoltResult(statementResult.list(), statementResult.consume())); + return Optional.of(new StatementBoltResult(statementResult)); } /** @@ -230,7 +230,7 @@ private Optional> captureResults(@Nonnull List trans List results = executeWithRetry(transactionStatements, (statement, transaction) -> { // calling list()/consume() is what actually executes cypher on the server StatementResult sr = transaction.run(statement); - return new BoltResult(sr.list(), sr.consume()); + return new ListBoltResult(sr.list(), sr.consume()); }); clearTransactionStatements(); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/ListBoltResult.java b/cypher-shell/src/main/java/org/neo4j/shell/state/ListBoltResult.java new file mode 100644 index 00000000..c32582d6 --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/ListBoltResult.java @@ -0,0 +1,40 @@ +package org.neo4j.shell.state; + +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.summary.ResultSummary; + +import javax.annotation.Nonnull; +import java.util.Iterator; +import java.util.List; + +/** + * A class holds the result from executing some Cypher. + */ +public class ListBoltResult implements BoltResult { + private final List records; + private final ResultSummary summary; + + public ListBoltResult(@Nonnull List records, @Nonnull ResultSummary summary) { + + this.records = records; + this.summary = summary; + } + + @Override + @Nonnull + public List getRecords() { + return records; + } + + @Override + @Nonnull + public Iterator iterate() { + return records.iterator(); + } + + @Override + @Nonnull + public ResultSummary getSummary() { + return summary; + } +} diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/StatementBoltResult.java b/cypher-shell/src/main/java/org/neo4j/shell/state/StatementBoltResult.java new file mode 100644 index 00000000..e52146cb --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/StatementBoltResult.java @@ -0,0 +1,40 @@ +package org.neo4j.shell.state; + +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.summary.ResultSummary; + +import javax.annotation.Nonnull; +import java.util.Iterator; +import java.util.List; + +/** + * @author mh + * @since 26.01.18 + */ +public class StatementBoltResult implements BoltResult { + + private final StatementResult result; + + public StatementBoltResult(StatementResult result) { + this.result = result; + } + + @Nonnull + @Override + public List getRecords() { + return result.list(); + } + + @Nonnull + @Override + public Iterator iterate() { + return result; + } + + @Nonnull + @Override + public ResultSummary getSummary() { + return result.summary(); + } +} diff --git a/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java b/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java index cb3a9deb..2aea5ac6 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java @@ -4,6 +4,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.mockito.Mockito; import org.neo4j.driver.v1.Driver; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; @@ -18,10 +19,12 @@ import org.neo4j.shell.prettyprint.PrettyPrinter; import org.neo4j.shell.state.BoltResult; import org.neo4j.shell.state.BoltStateHandler; +import org.neo4j.shell.state.ListBoltResult; import org.neo4j.shell.test.OfflineTestShell; import java.io.IOException; import java.util.Optional; +import java.util.function.Consumer; import static java.util.Arrays.asList; import static junit.framework.TestCase.assertTrue; @@ -30,11 +33,7 @@ import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.contains; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class CypherShellTest { @Rule @@ -135,7 +134,7 @@ public void executeOfflineThrows() throws CommandException { public void setParamShouldAddParamWithSpecialCharactersAndValue() throws CommandException { Value value = mock(Value.class); Record recordMock = mock(Record.class); - BoltResult boltResult = mock(BoltResult.class); + BoltResult boltResult = mock(ListBoltResult.class); when(mockedBoltStateHandler.runCypher(anyString(), anyMap())).thenReturn(Optional.of(boltResult)); when(boltResult.getRecords()).thenReturn(asList(recordMock)); @@ -153,7 +152,7 @@ public void setParamShouldAddParamWithSpecialCharactersAndValue() throws Command public void setParamShouldAddParam() throws CommandException { Value value = mock(Value.class); Record recordMock = mock(Record.class); - BoltResult boltResult = mock(BoltResult.class); + BoltResult boltResult = mock(ListBoltResult.class); when(mockedBoltStateHandler.runCypher(anyString(), anyMap())).thenReturn(Optional.of(boltResult)); when(boltResult.getRecords()).thenReturn(asList(recordMock)); @@ -171,13 +170,13 @@ public void setParamShouldAddParam() throws CommandException { public void executeShouldPrintResult() throws CommandException { Driver mockedDriver = mock(Driver.class); Session session = mock(Session.class); - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); BoltStateHandler boltStateHandler = mock(BoltStateHandler.class); when(boltStateHandler.isConnected()).thenReturn(true); when(boltStateHandler.runCypher(anyString(), anyMap())).thenReturn(Optional.of(result)); - when(mockedPrettyPrinter.format(result)).thenReturn("999"); + doAnswer((a) -> { ((Consumer)a.getArguments()[1]).accept("999"); return null;}).when(mockedPrettyPrinter).format(any(BoltResult.class), anyObject()); when(mockedDriver.session()).thenReturn(session); OfflineTestShell shell = new OfflineTestShell(logger, boltStateHandler, mockedPrettyPrinter); @@ -187,11 +186,11 @@ public void executeShouldPrintResult() throws CommandException { @Test public void commitShouldPrintResult() throws CommandException { - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); BoltStateHandler boltStateHandler = mock(BoltStateHandler.class); - when(mockedPrettyPrinter.format(result)).thenReturn("999"); + doAnswer((a) -> { ((Consumer)a.getArguments()[1]).accept("999"); return null;}).when(mockedPrettyPrinter).format(any(BoltResult.class), anyObject()); when(boltStateHandler.commitTransaction()).thenReturn(Optional.of(asList(result))); OfflineTestShell shell = new OfflineTestShell(logger, boltStateHandler, mockedPrettyPrinter); diff --git a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java index f7a8c893..24dafea4 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java @@ -38,6 +38,22 @@ public void testForceNonInteractiveIsParsed() { CliArgHelper.parse(asArray("--non-interactive")).getNonInteractive()); } + @Test + public void testWidth() { + assertEquals("width 120",120, CliArgHelper.parse("--width 120".split(" ")).getWidth()); + assertNull("invalid width", CliArgHelper.parse("--width 0".split(" "))); + assertNull("invalid width", CliArgHelper.parse("--width -1".split(" "))); + assertNull("invalid width",CliArgHelper.parse("--width foo".split(" "))); + } + + @Test + public void testWrap() { + assertTrue("wrap true", CliArgHelper.parse("--wrap true".split(" ")).getWrap()); + assertFalse("wrap false", CliArgHelper.parse("--wrap false".split(" ")).getWrap()); + assertTrue("default wrap", CliArgHelper.parse().getWrap()); + assertNull("invalid wrap",CliArgHelper.parse("--wrap foo".split(" "))); + } + @Test public void testDebugIsNotDefault() { assertFalse("Debug should not be the default mode", @@ -120,8 +136,9 @@ public void nonsenseArgsGiveError() throws Exception { assertNull(cliargs); - assertTrue(bout.toString().startsWith("usage: cypher-shell [-h]")); - assertTrue(bout.toString().contains("cypher-shell: error: unrecognized arguments: '-notreally'")); + String output = bout.toString(); + assertTrue(output, output.startsWith("usage: cypher-shell [-h]")); + assertTrue(output, output.contains("cypher-shell: error: unrecognized arguments: '-notreally'")); } @Test diff --git a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java index acbbd314..a7dbbec1 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java @@ -58,6 +58,20 @@ public void setFailBehavior() throws Exception { assertEquals(FailBehavior.FAIL_AT_END, cliArgs.getFailBehavior()); } + @Test + public void setWidth() throws Exception { + assertEquals(-1, cliArgs.getWidth()); + + cliArgs.setWidth(null); + assertEquals(-1, cliArgs.getWidth()); + + cliArgs.setWidth(120); + assertEquals(120, cliArgs.getWidth()); + + cliArgs.setWidth(0); + assertEquals(120, cliArgs.getWidth()); + } + @Test public void setFormat() throws Exception { // default diff --git a/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java b/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java index c3801ad9..b9a1d028 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java @@ -57,7 +57,7 @@ public void printException() throws Exception { @Test public void printExceptionWithDebug() throws Exception { - logger = new AnsiLogger(true, Format.VERBOSE, out, err); + Logger logger = new AnsiLogger(true, Format.VERBOSE, out, err); logger.printError(new Throwable("bam")); verify(err).println(contains("java.lang.Throwable: bam")); verify(err).println(contains("at org.neo4j.shell.log.AnsiLoggerTest.printExceptionWithDebug")); diff --git a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java index 2a2ba8f4..d03dd943 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java @@ -12,47 +12,49 @@ import org.neo4j.driver.v1.types.Relationship; import org.neo4j.shell.cli.Format; import org.neo4j.shell.state.BoltResult; +import org.neo4j.shell.state.ListBoltResult; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableMap; -import static org.hamcrest.CoreMatchers.any; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.neo4j.driver.internal.util.Iterables.map; public class PrettyPrinterTest { - private final PrettyPrinter plainPrinter = new PrettyPrinter(Format.PLAIN); - private final PrettyPrinter verbosePrinter = new PrettyPrinter(Format.VERBOSE); + private final PrettyPrinter plainPrinter = new PrettyPrinter(Format.PLAIN,-1, false); + private final PrettyPrinter verbosePrinter = new PrettyPrinter(Format.VERBOSE, -1, true); @Test public void returnStatisticsForEmptyRecords() throws Exception { // given ResultSummary resultSummary = mock(ResultSummary.class); SummaryCounters summaryCounters = mock(SummaryCounters.class); - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); + + when(result.iterate()).thenReturn(Collections.emptyIterator()); - when(result.getRecords()).thenReturn(Collections.emptyList()); when(result.getSummary()).thenReturn(resultSummary); when(resultSummary.counters()).thenReturn(summaryCounters); when(summaryCounters.labelsAdded()).thenReturn(1); when(summaryCounters.nodesCreated()).thenReturn(10); // when - String actual = verbosePrinter.format(result); + StringBuilder actual = new StringBuilder(); + verbosePrinter.format(result, actual::append); + // then - assertThat(actual, containsString("Added 10 nodes, Added 1 labels")); + assertThat(actual.toString(), containsString("Added 10 nodes, Added 1 labels")); } @Test @@ -73,12 +75,13 @@ public void prettyPrintProfileInformation() throws Exception { Map argumentMap = Values.parameters("Version", "3.1", "Planner", "COST", "Runtime", "INTERPRETED").asMap(v -> v); when(plan.arguments()).thenReturn(argumentMap); - BoltResult result = mock(BoltResult.class); - when(result.getRecords()).thenReturn(Collections.emptyList()); + BoltResult result = mock(ListBoltResult.class); + when(result.iterate()).thenReturn(Collections.emptyIterator()); when(result.getSummary()).thenReturn(resultSummary); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then String expected = @@ -110,12 +113,13 @@ public void prettyPrintExplainInformation() throws Exception { Map argumentMap = Values.parameters("Version", "3.1", "Planner", "COST", "Runtime", "INTERPRETED").asMap(v -> v); when(plan.arguments()).thenReturn(argumentMap); - BoltResult result = mock(BoltResult.class); - when(result.getRecords()).thenReturn(Collections.emptyList()); + BoltResult result = mock(ListBoltResult.class); + when(result.iterate()).thenReturn(Collections.emptyIterator()); when(result.getSummary()).thenReturn(resultSummary); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then String expected = @@ -131,7 +135,7 @@ public void prettyPrintExplainInformation() throws Exception { @Test public void prettyPrintList() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record1 = mock(Record.class); Record record2 = mock(Record.class); @@ -149,20 +153,23 @@ public void prettyPrintList() throws Exception { when(record1.values()).thenReturn(asList(value1, value2)); when(record2.values()).thenReturn(asList(value2)); - when(result.getRecords()).thenReturn(asList(record1, record2)); + List records = asList(record1, record2); + when(result.iterate()).thenReturn(records.iterator()); + when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then - assertThat(actual, is("col1, col2\n[val1_1, val1_2], [val2_1]\n[val2_1]")); + assertThat(actual, is("col1, col2\n[val1_1, val1_2], [val2_1]\n[val2_1]\n")); } @Test public void prettyPrintNode() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -181,21 +188,22 @@ public void prettyPrintNode() throws Exception { when(record.keys()).thenReturn(asList("col1", "col2")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then assertThat(actual, is("col1, col2\n" + - "(:label1:label2 {prop2: prop2_value, prop1: prop1_value})")); + "(:label1:label2 {prop2: prop2_value, prop1: prop1_value})\n")); } @Test public void prettyPrintRelationships() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -214,19 +222,20 @@ public void prettyPrintRelationships() throws Exception { when(record.keys()).thenReturn(asList("rel")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then - assertThat(actual, is("rel\n[:RELATIONSHIP_TYPE {prop2: prop2_value, prop1: prop1_value}]")); + assertThat(actual, is("rel\n[:RELATIONSHIP_TYPE {prop2: prop2_value, prop1: prop1_value}]\n")); } @Test public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws Exception { - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value relVal = mock(Value.class); @@ -260,21 +269,22 @@ public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws when(record.keys()).thenReturn(asList("rel", "node")); when(record.values()).thenReturn(asList(relVal, nodeVal)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then assertThat(actual, is("rel, node\n[:`RELATIONSHIP,TYPE` {prop2: prop2_value, prop1: \"prop1, value\"}], " + - "(:`label ``1`:label2 {prop1: \"prop1:value\", `1prop2`: \"\", ä: not-escaped})")); + "(:`label ``1`:label2 {prop1: \"prop1:value\", `1prop2`: \"\", ä: not-escaped})\n")); } @Test public void prettyPrintPaths() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -322,22 +332,23 @@ public void prettyPrintPaths() throws Exception { when(record.keys()).thenReturn(asList("path")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then assertThat(actual, is("path\n" + "(:start {prop1: prop1_value})-[:RELATIONSHIP_TYPE]->" + - "(:middle)<-[:RELATIONSHIP_TYPE]-(:end {prop2: prop2_value})")); + "(:middle)<-[:RELATIONSHIP_TYPE]-(:end {prop2: prop2_value})\n")); } @Test public void prettyPrintSingleNodePath() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -370,20 +381,21 @@ public void prettyPrintSingleNodePath() throws Exception { when(record.keys()).thenReturn(asList("path")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then - assertThat(actual, is("path\n(:start)-[:RELATIONSHIP_TYPE]->(:end)")); + assertThat(actual, is("path\n(:start)-[:RELATIONSHIP_TYPE]->(:end)\n")); } @Test public void prettyPrintThreeSegmentPath() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -434,15 +446,22 @@ public void prettyPrintThreeSegmentPath() throws Exception { when(record.keys()).thenReturn(asList("path")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = plainPrinter.format(result); + String actual = configureFormat(result); + // then assertThat(actual, is("path\n" + "(:start)-[:RELATIONSHIP_TYPE]->" + - "(:second)<-[:RELATIONSHIP_TYPE]-(:third)-[:RELATIONSHIP_TYPE]->(:end)")); + "(:second)<-[:RELATIONSHIP_TYPE]-(:third)-[:RELATIONSHIP_TYPE]->(:end)\n")); + } + + private String configureFormat(BoltResult result) { + StringBuilder actual = new StringBuilder(); + plainPrinter.format(result, (s) -> {if (s!=null && !s.trim().isEmpty()) actual.append(s).append(OutputFormatter.NEWLINE);}); + return actual.toString(); } } diff --git a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatTest.java b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatTest.java index 8fa811dd..70684a61 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatTest.java @@ -2,8 +2,6 @@ import org.hamcrest.CoreMatchers; import org.junit.Test; -import org.mockito.Matchers; -import org.mockito.Mockito; import org.neo4j.driver.internal.InternalNode; import org.neo4j.driver.internal.InternalPath; import org.neo4j.driver.internal.InternalRecord; @@ -13,18 +11,14 @@ import org.neo4j.driver.v1.summary.ProfiledPlan; import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.driver.v1.summary.StatementType; -import org.neo4j.driver.v1.summary.SummaryCounters; import org.neo4j.driver.v1.types.Node; import org.neo4j.driver.v1.types.Path; import org.neo4j.driver.v1.types.Relationship; -import org.neo4j.driver.v1.util.Function; import org.neo4j.shell.cli.Format; import org.neo4j.shell.state.BoltResult; +import org.neo4j.shell.state.ListBoltResult; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.*; -import java.util.stream.Stream; import static java.util.Arrays.asList; import static java.util.Collections.singletonMap; @@ -39,7 +33,7 @@ public class TableOutputFormatTest { - private final PrettyPrinter verbosePrinter = new PrettyPrinter(Format.VERBOSE); + private final PrettyPrinter verbosePrinter = new PrettyPrinter(Format.VERBOSE,-1, true); @Test @@ -60,24 +54,26 @@ public void prettyPrintPlanInformation() throws Exception { Map argumentMap = Values.parameters("Version", "3.1", "Planner", "COST", "Runtime", "INTERPRETED").asMap(v -> v); when(plan.arguments()).thenReturn(argumentMap); - BoltResult result = mock(BoltResult.class); - when(result.getRecords()).thenReturn(Collections.emptyList()); + BoltResult result = mock(ListBoltResult.class); + when(result.iterate()).thenReturn(Collections.emptyIterator()); when(result.getSummary()).thenReturn(resultSummary); // when - String actual = verbosePrinter.format(result); + StringBuilder actual = new StringBuilder(); + verbosePrinter.format(result, actual::append); + // then argumentMap.forEach((k,v) -> { - assertThat(actual, CoreMatchers.containsString("| "+k)); - assertThat(actual, CoreMatchers.containsString("| "+v.toString())); + assertThat(actual.toString(), CoreMatchers.containsString("| "+k)); + assertThat(actual.toString(), CoreMatchers.containsString("| "+v.toString())); }); } @Test public void prettyPrintNode() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -97,26 +93,27 @@ public void prettyPrintNode() throws Exception { recordMap.put("col1",value); recordMap.put("col2",value); when(record.keys()).thenReturn(asList("col1", "col2")); - when(record.get(eq("col1"))).thenReturn(value); - when(record.get(eq("col2"))).thenReturn(value); + when(record.get(eq(0))).thenReturn(value); + when(record.get(eq(1))).thenReturn(value); when(record.asMap(anyObject())).thenReturn(recordMap); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = verbosePrinter.format(result); + StringBuilder actual = new StringBuilder(); + verbosePrinter.format(result, actual::append); // then - assertThat(actual, containsString("| (:label1:label2 {prop2: prop2_value, prop1: prop1_value}) |")); + assertThat(actual.toString(), containsString("| (:label1:label2 {prop2: prop2_value, prop1: prop1_value}) |")); } @Test public void prettyPrintRelationships() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); @@ -133,23 +130,25 @@ public void prettyPrintRelationships() throws Exception { when(relationship.asMap(anyObject())).thenReturn(unmodifiableMap(propertiesAsMap)); when(record.keys()).thenReturn(asList("rel")); - when(record.get(eq("rel"))).thenReturn(value); + when(record.get(eq(0))).thenReturn(value); when(record.values()).thenReturn(asList(value)); when(record.asMap(anyObject())).thenReturn(Collections.singletonMap("rel",value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = verbosePrinter.format(result); + StringBuilder actual = new StringBuilder(); + verbosePrinter.format(result, actual::append); + // then - assertThat(actual, containsString("| [:RELATIONSHIP_TYPE {prop2: prop2_value, prop1: prop1_value}] |")); + assertThat(actual.toString(), containsString("| [:RELATIONSHIP_TYPE {prop2: prop2_value, prop1: prop1_value}] |")); } @Test public void prettyPrintPath() throws Exception { // given - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value value = mock(Value.class); Path path = mock(Path.class); @@ -197,22 +196,24 @@ public void prettyPrintPath() throws Exception { when(value.asPath()).thenReturn(path); when(record.keys()).thenReturn(asList("path")); - when(record.get(eq("path"))).thenReturn(value); + when(record.get(eq(0))).thenReturn(value); when(record.values()).thenReturn(asList(value)); when(record.asMap(anyObject())).thenReturn(Collections.singletonMap("path",value)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = verbosePrinter.format(result); + StringBuilder actual = new StringBuilder(); + verbosePrinter.format(result, actual::append); + // then - assertThat(actual, containsString("| (:L1)<-[:R1]-(:L2)-[:R2]->(:L3) |")); + assertThat(actual.toString(), containsString("| (:L1)<-[:R1]-(:L2)-[:R2]->(:L3) |")); } @Test public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws Exception { - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); Record record = mock(Record.class); Value relVal = mock(Value.class); @@ -247,22 +248,24 @@ public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws recordMap.put("rel",relVal); recordMap.put("node",nodeVal); when(record.keys()).thenReturn(asList("rel", "node")); - when(record.get(eq("rel"))).thenReturn(relVal); - when(record.get(eq("node"))).thenReturn(nodeVal); + when(record.get(eq(0))).thenReturn(relVal); + when(record.get(eq(1))).thenReturn(nodeVal); when(record.asMap(anyObject())).thenReturn(recordMap); when(record.values()).thenReturn(asList(relVal, nodeVal)); - when(result.getRecords()).thenReturn(asList(record)); + when(result.iterate()).thenReturn(asList(record).iterator()); when(result.getSummary()).thenReturn(mock(ResultSummary.class)); // when - String actual = verbosePrinter.format(result); + StringBuilder actual = new StringBuilder(); + verbosePrinter.format(result, actual::append); + // then - assertThat(actual, containsString("| [:`RELATIONSHIP,TYPE` {prop2: prop2_value, prop1: \"prop1, value\"}] |")); - assertThat(actual, containsString("| (:`label ``1`:label2 {prop1: \"prop1:value\", `1prop2`: \"\", ä: not-escaped})")); + assertThat(actual.toString(), containsString("| [:`RELATIONSHIP,TYPE` {prop2: prop2_value, prop1: \"prop1, value\"}] |")); + assertThat(actual.toString(), containsString("| (:`label ``1`:label2 {prop1: \"prop1:value\", `1prop2`: \"\", ä: not-escaped})")); } @Test @@ -289,6 +292,52 @@ public void twoRows() throws Exception assertThat( table, containsString( "| \"b\" | 43 |" ) ); } + @Test + public void wrapContent() throws Exception + { + // GIVEN + StatementResult result = mockResult( asList( "c1"), "a", "bb","ccc","dddd","eeeee" ); + // WHEN + StringBuilder sb = new StringBuilder(); + new TableOutputFormatter(6, true).format(new ListBoltResult(result.list(), result.summary()), (s) -> sb.append(s).append("\n")); + String table = sb.toString(); + // THEN + assertThat(table, is( + "+------+\n" + + "| c1 |\n" + + "+------+\n" + + "| \"a\" |\n" + + "| \"bb\" |\n" + + "| \"ccc |\n" + + "| \" |\n" + + "| \"ddd |\n" + + "| d\" |\n" + + "| \"eee |\n" + + "| ee\" |\n" + + "+------+\n")); + } + @Test + public void cutContent() throws Exception + { + // GIVEN + StatementResult result = mockResult( asList( "c1"), "a", "bb","ccc","dddd","eeeee" ); + // WHEN + StringBuilder sb = new StringBuilder(); + new TableOutputFormatter(6, false).format(new ListBoltResult(result.list(), result.summary()), (s) -> sb.append(s).append("\n")); + String table = sb.toString(); + // THEN + assertThat(table, is( + "+------+\n" + + "| c1 |\n" + + "+------+\n" + + "| \"a\" |\n" + + "| \"bb\" |\n" + + "| \"ccc |\n" + + "| \"ddd |\n" + + "| \"eee |\n" + + "+------+\n")); + } + @Test public void formatCollections() throws Exception { @@ -320,7 +369,9 @@ public void formatEntities() throws Exception } private String formatResult(StatementResult result) { - return new TableOutputFormatter().format(new BoltResult(result.list(), result.summary())); + StringBuilder sb = new StringBuilder(); + new TableOutputFormatter(-1, true).format(new ListBoltResult(result.list(), result.summary()), sb::append); + return sb.toString(); } private StatementResult mockResult(List cols, Object... data) { diff --git a/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java b/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java index bffcbb21..22bdb1fc 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java @@ -179,8 +179,8 @@ public void commitPurgesTheTransactionStatementsAndCollectsResults() throws Comm Session sessionMock = mock(Session.class); Driver driverMock = stubVersionInAnOpenSession(mock(StatementResult.class), sessionMock, "neo4j-version"); - BoltResult boltResultMock1 = mock(BoltResult.class); - BoltResult boltResultMock2 = mock(BoltResult.class); + BoltResult boltResultMock1 = mock(ListBoltResult.class); + BoltResult boltResultMock2 = mock(ListBoltResult.class); Record record1 = mock(Record.class); Record record2 = mock(Record.class);