diff --git a/build.gradle b/build.gradle index be07f9a4..e971c02d 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { argparse4jVersion = '0.7.0' junitVersion = '4.12' - neo4jJavaVersion = '1.5.1' + neo4jJavaVersion = '1.6.0-beta01' findbugsVersion = '3.0.0' jansiVersion = '1.13' jlineVersion = '2.14.2' diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java index 9ac006d2..56a02d2b 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java @@ -170,19 +170,22 @@ public void paramsAndListVariables() throws CommandException { assertTrue(shell.getAll().isEmpty()); long randomLong = System.currentTimeMillis(); + String stringInput = "\"randomString\""; + shell.set("string", stringInput); Optional result = shell.set("bob", String.valueOf(randomLong)); assertTrue(result.isPresent()); assertEquals(randomLong, result.get()); - shell.execute("RETURN { bob }"); + shell.execute("RETURN { bob }, $string"); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger).printOut(captor.capture()); List queryResult = captor.getAllValues(); assertThat(queryResult.get(0), containsString("| { bob }")); - assertThat(queryResult.get(0), containsString("\n| " + randomLong+ " |\n")); + assertThat(queryResult.get(0), containsString("| " + randomLong + " | " + stringInput + " |")); assertEquals(randomLong, shell.getAll().get("bob")); + assertEquals("randomString", shell.getAll().get("string")); } @Test 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..c247fc75 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -10,14 +10,17 @@ import org.neo4j.shell.prettyprint.PrettyPrinter; import org.neo4j.shell.state.BoltResult; import org.neo4j.shell.state.BoltStateHandler; +import org.neo4j.shell.state.ParamValue; import javax.annotation.Nonnull; +import java.util.AbstractMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * A possibly interactive shell for evaluating cypher statements. @@ -25,7 +28,7 @@ public class CypherShell implements StatementExecuter, Connector, TransactionHandler, VariableHolder { // Final space to catch newline protected static final Pattern cmdNamePattern = Pattern.compile("^\\s*(?[^\\s]+)\\b(?.*)\\s*$"); - protected final Map queryParams = new HashMap<>(); + protected final Map queryParams = new HashMap<>(); private final Logger logger; private final BoltStateHandler boltStateHandler; private final PrettyPrinter prettyPrinter; @@ -80,7 +83,7 @@ public void execute(@Nonnull final String cmdString) throws ExitException, Comma * @param cypher non-empty cypher text to executeLine */ protected void executeCypher(@Nonnull final String cypher) throws CommandException { - final Optional result = boltStateHandler.runCypher(cypher, queryParams); + final Optional result = boltStateHandler.runCypher(cypher, getAll()); result.ifPresent(boltResult -> logger.printOut(prettyPrinter.format(boltResult))); } @@ -156,13 +159,13 @@ public Optional set(@Nonnull String name, @Nonnull String valueString) throws Co final BoltResult result = setParamsAndValidate(name, valueString); String parameterName = CypherVariablesFormatter.unescapedCypherVariable(name); final Object value = result.getRecords().get(0).get(parameterName).asObject(); - queryParams.put(parameterName, value); + queryParams.put(parameterName, new ParamValue(valueString, value)); return Optional.ofNullable(value); } private BoltResult setParamsAndValidate(@Nonnull String name, @Nonnull String valueString) throws CommandException { String cypher = "RETURN " + valueString + " as " + name; - final Optional result = boltStateHandler.runCypher(cypher, queryParams); + final Optional result = boltStateHandler.runCypher(cypher, getAll()); if (!result.isPresent() || result.get().getRecords().isEmpty()) { throw new CommandException("Failed to set value of parameter"); } @@ -172,6 +175,16 @@ private BoltResult setParamsAndValidate(@Nonnull String name, @Nonnull String va @Override @Nonnull public Map getAll() { + return queryParams.entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + value -> value.getValue().getValue())); + } + + @Nonnull + @Override + public Map getAllAsUserInput() { return queryParams; } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/VariableHolder.java b/cypher-shell/src/main/java/org/neo4j/shell/VariableHolder.java index c5452c36..c2398605 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/VariableHolder.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/VariableHolder.java @@ -1,6 +1,7 @@ package org.neo4j.shell; import org.neo4j.shell.exception.CommandException; +import org.neo4j.shell.state.ParamValue; import javax.annotation.Nonnull; import java.util.Map; @@ -22,4 +23,10 @@ public interface VariableHolder { */ @Nonnull Map getAll(); + + /** + * @return map of all currently set variables and their values corresponding to the user valueString + */ + @Nonnull + Map getAllAsUserInput(); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/commands/Param.java b/cypher-shell/src/main/java/org/neo4j/shell/commands/Param.java index a2ddb24a..a6d0be35 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/commands/Param.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/commands/Param.java @@ -7,6 +7,7 @@ import javax.annotation.Nonnull; import java.util.Collections; import java.util.List; +import java.util.function.BiPredicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -17,7 +18,10 @@ public class Param implements Command { // Match arguments such as "(key) (value with possible spaces)" where key and value are any strings private static final Pattern backtickPattern = Pattern.compile("^\\s*(?(`([^`])*`)+?):?\\s+(?.+)$"); + private static final Pattern backtickLambdaPattern = Pattern.compile("^\\s*(?(`([^`])*`)+?)\\s*=>\\s*(?.+)$"); private static final Pattern argPattern = Pattern.compile("^\\s*(?[\\p{L}_][\\p{L}0-9_]*):?\\s+(?.+)$"); + private static final Pattern lambdaPattern = Pattern.compile("^\\s*(?[\\p{L}_][\\p{L}0-9_]*)\\s*=>\\s*(?.+)$"); + private static final Pattern lambdaMapPattern = Pattern.compile("^\\s*(?[\\p{L}_][\\p{L}0-9_]*):\\s*=>\\s*(?.+)$"); public static final String COMMAND_NAME = ":param"; private final VariableHolder variableHolder; @@ -41,7 +45,7 @@ public String getDescription() { @Nonnull @Override public String getUsage() { - return "name value"; + return "name => value"; } @Nonnull @@ -58,21 +62,44 @@ public List getAliases() { @Override public void execute(@Nonnull final String argString) throws CommandException { - Matcher alphanumericMatcher = argPattern.matcher(argString); - if (alphanumericMatcher.matches()) { - variableHolder.set(alphanumericMatcher.group("key"), alphanumericMatcher.group("value")); - } else { - checkForBackticks(argString); + Matcher lambdaMapMatcher = lambdaMapPattern.matcher(argString); + if (lambdaMapMatcher.matches()) { + throw new CommandException(AnsiFormattedText.from("Incorrect usage.\nusage: ") + .bold().append(COMMAND_NAME).boldOff().append(" ").append(getUsage())); } + if (!assignIfValidParameter(argString)) { + throw new CommandException(AnsiFormattedText.from("Incorrect number of arguments.\nusage: ") + .bold().append(COMMAND_NAME).boldOff().append(" ").append(getUsage())); + } + } + + private boolean assignIfValidParameter(@Nonnull String argString) throws CommandException { + return setParameterIfItMatchesPattern(argString, lambdaPattern, assignIfValidParameter()) + || setParameterIfItMatchesPattern(argString, argPattern, assignIfValidParameter()) + || setParameterIfItMatchesPattern(argString, backtickLambdaPattern, backTickMatchPattern()) + || setParameterIfItMatchesPattern(argString, backtickPattern, backTickMatchPattern()); } - private void checkForBackticks(@Nonnull String argString) throws CommandException { - Matcher matcher = backtickPattern.matcher(argString); - if (argString.trim().startsWith("`") && matcher.matches() && matcher.group("key").length() > 2) { + private boolean setParameterIfItMatchesPattern(@Nonnull String argString, Pattern pattern, + BiPredicate matchingFunction) throws CommandException { + Matcher matcher = pattern.matcher(argString); + if (matchingFunction.test(argString, matcher)) { variableHolder.set(matcher.group("key"), matcher.group("value")); + return true; } else { - throw new CommandException(AnsiFormattedText.from("Incorrect number of arguments.\nusage: ") - .bold().append(COMMAND_NAME).boldOff().append(" ").append(getUsage())); + return false; } } + + private BiPredicate assignIfValidParameter() { + return (argString, matcher) -> matcher.matches(); + } + + private BiPredicate backTickMatchPattern() { + return (argString, backtickLambdaMatcher) -> { + return argString.trim().startsWith("`") + && backtickLambdaMatcher.matches() + && backtickLambdaMatcher.group("key").length() > 2; + }; + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/commands/Params.java b/cypher-shell/src/main/java/org/neo4j/shell/commands/Params.java index 6de44c5c..9200d390 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/commands/Params.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/commands/Params.java @@ -78,21 +78,21 @@ public void execute(@Nonnull final String argString) throws ExitException, Comma private void listParam(@Nonnull String name) throws CommandException { String parameterName = CypherVariablesFormatter.unescapedCypherVariable(name); - if (!variableHolder.getAll().containsKey(parameterName)) { + if (!this.variableHolder.getAllAsUserInput().containsKey(parameterName)) { throw new CommandException("Unknown parameter: " + name); } - listParam(name.length(), name, variableHolder.getAll().get(parameterName)); + listParam(name.length(), name, this.variableHolder.getAllAsUserInput().get(parameterName).getValueAsString()); } private void listParam(int leftColWidth, @Nonnull String key, @Nonnull Object value) { - logger.printOut(String.format("%-" + leftColWidth + "s: %s", key, value)); + logger.printOut(String.format(":param %-" + leftColWidth + "s => %s", key, value)); } private void listAllParams() { - List keys = variableHolder.getAll().keySet().stream().sorted().collect(Collectors.toList()); + List keys = variableHolder.getAllAsUserInput().keySet().stream().sorted().collect(Collectors.toList()); int leftColWidth = keys.stream().map((s) -> escape(s).length()).reduce(0, Math::max); - keys.stream().forEach(k -> listParam(leftColWidth, escape(k), variableHolder.getAll().get(k))); + keys.forEach(key -> listParam(leftColWidth, escape(key), variableHolder.getAllAsUserInput().get(key).getValueAsString())); } } 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 d2f4e0b4..5f81fb95 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 @@ -10,9 +10,13 @@ import org.neo4j.driver.v1.types.Relationship; import org.neo4j.shell.state.BoltResult; -import java.util.*; -import java.util.stream.Collectors; import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -31,23 +35,23 @@ public interface OutputFormatter { @Nonnull default String formatValue(@Nonnull final Value value) { TypeRepresentation type = (TypeRepresentation) value.type(); switch (type.constructor()) { - case LIST_TyCon: + case LIST: return listAsString(value.asList(this::formatValue)); - case MAP_TyCon: + case MAP: return mapAsString(value.asMap(this::formatValue)); - case NODE_TyCon: + case NODE: return nodeAsString(value.asNode()); - case RELATIONSHIP_TyCon: + case RELATIONSHIP: return relationshipAsString(value.asRelationship()); - case PATH_TyCon: + case PATH: return pathAsString(value.asPath()); - case ANY_TyCon: - case BOOLEAN_TyCon: - case STRING_TyCon: - case NUMBER_TyCon: - case INTEGER_TyCon: - case FLOAT_TyCon: - case NULL_TyCon: + case ANY: + case BOOLEAN: + case STRING: + case NUMBER: + case INTEGER: + case FLOAT: + case NULL: default: return value.toString(); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/ParamValue.java b/cypher-shell/src/main/java/org/neo4j/shell/state/ParamValue.java new file mode 100644 index 00000000..16a8853d --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/ParamValue.java @@ -0,0 +1,22 @@ +package org.neo4j.shell.state; + +/** + * Handles queryparams value and user inputString + */ +public class ParamValue { + private final String valueAsString; + private final Object value; + + public ParamValue(String valueAsString, Object value) { + this.valueAsString = valueAsString; + this.value = value; + } + + public Object getValue() { + return value; + } + + public String getValueAsString() { + return valueAsString; + } +} diff --git a/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamTest.java b/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamTest.java index b570d83b..411e16a0 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamTest.java @@ -8,6 +8,7 @@ import org.neo4j.shell.VariableHolder; import org.neo4j.shell.exception.CommandException; +import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.fail; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.Mockito.mock; @@ -44,12 +45,26 @@ public void shouldFailIfOneArg() throws CommandException { } @Test - public void setValue() throws CommandException { + public void setParam() throws CommandException { cmd.execute("bob 9"); verify(mockShell).set("bob", "9"); } + @Test + public void setLambdasAsParam() throws CommandException { + cmd.execute("bob => 9"); + + verify(mockShell).set("bob", "9"); + } + + @Test + public void setLambdasAsParamWithBackticks() throws CommandException { + cmd.execute("`bob` => 9"); + + verify(mockShell).set("`bob`", "9"); + } + @Test public void setSpecialCharacterParameter() throws CommandException { cmd.execute("bØb 9"); @@ -58,14 +73,21 @@ public void setSpecialCharacterParameter() throws CommandException { } @Test - public void setValueWithSpecialCharacters() throws CommandException { + public void setSpecialCharacterParameterForLambdaExpressions() throws CommandException { + cmd.execute("`first=>Name` => \"Bruce\""); + + verify(mockShell).set("`first=>Name`", "\"Bruce\""); + } + + @Test + public void setParamWithSpecialCharacters() throws CommandException { cmd.execute("`bob#` 9"); verify(mockShell).set("`bob#`", "9"); } @Test - public void setValueWithOddNoOfBackTicks() throws CommandException { + public void setParamWithOddNoOfBackTicks() throws CommandException { cmd.execute(" `bo `` sömething ``` 9"); verify(mockShell).set("`bo `` sömething ```", "9"); @@ -81,6 +103,16 @@ public void shouldFailForVariablesWithoutEscaping() throws CommandException { fail("Expected error"); } + @Test + public void shouldFailForVariablesMixingMapStyleAssignmentAndLambdas() throws CommandException { + thrown.expect(CommandException.class); + thrown.expectMessage(containsString("Incorrect usage")); + + cmd.execute("bob: => 9"); + + fail("Expected error"); + } + @Test public void shouldFailForEmptyVariables() throws CommandException { thrown.expect(CommandException.class); @@ -143,4 +175,10 @@ public void shouldNotExecuteEscapedCypher() throws CommandException { cmd.execute("bob \"RETURN 5 as bob\""); verify(mockShell).set("bob", "\"RETURN 5 as bob\""); } + + @Test + public void printUsage() throws CommandException { + String usage = cmd.getUsage(); + assertEquals(usage, "name => value"); + } } diff --git a/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamsTest.java b/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamsTest.java index 50f55df4..4481969d 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamsTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/commands/ParamsTest.java @@ -7,7 +7,9 @@ import org.neo4j.shell.VariableHolder; import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.log.Logger; +import org.neo4j.shell.state.ParamValue; +import java.util.AbstractMap; import java.util.HashMap; import static org.hamcrest.CoreMatchers.containsString; @@ -21,7 +23,7 @@ public class ParamsTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - private HashMap vars; + private HashMap vars; private Logger logger; private Params cmd; @@ -30,7 +32,7 @@ public void setup() throws CommandException { vars = new HashMap<>(); logger = mock(Logger.class); VariableHolder shell = mock(VariableHolder.class); - when(shell.getAll()).thenReturn(vars); + when(shell.getAllAsUserInput()).thenReturn(vars); cmd = new Params(logger, shell); } @@ -52,72 +54,74 @@ public void helpNotNull() { @Test public void runCommand() throws CommandException { // given - vars.put("var", 9); + String var = "var"; + int value = 9; + vars.put(var, new ParamValue(String.valueOf(value), value)); // when cmd.execute(""); // then - verify(logger).printOut("var: 9"); + verify(logger).printOut(":param var => 9"); verifyNoMoreInteractions(logger); } @Test public void runCommandAlignment() throws CommandException { // given - vars.put("var", 9); - vars.put("param", 99999); + vars.put("var", new ParamValue(String.valueOf(9), 9)); + vars.put("param", new ParamValue(String.valueOf(99999), 99999)); // when cmd.execute(""); // then - verify(logger).printOut("param: 99999"); - verify(logger).printOut("var : 9"); + verify(logger).printOut(":param param => 99999"); + verify(logger).printOut(":param var => 9"); verifyNoMoreInteractions(logger); } @Test public void runCommandWithArg() throws CommandException { // given - vars.put("var", 9); - vars.put("param", 9999); + vars.put("var", new ParamValue(String.valueOf(9), 9)); + vars.put("param", new ParamValue(String.valueOf(9999), 9999)); // when cmd.execute("var"); // then - verify(logger).printOut("var: 9"); + verify(logger).printOut(":param var => 9"); verifyNoMoreInteractions(logger); } @Test public void runCommandWithArgWithExtraSpace() throws CommandException { // given - vars.put("var", 9); - vars.put("param", 9999); + vars.put("var", new ParamValue(String.valueOf(9), 9)); + vars.put("param", new ParamValue(String.valueOf(9999), 9999)); // when cmd.execute(" var"); // then - verify(logger).printOut("var: 9"); + verify(logger).printOut(":param var => 9"); verifyNoMoreInteractions(logger); } @Test public void runCommandWithArgWithBackticks() throws CommandException { // given - vars.put("var", 9); - vars.put("param", 9999); + vars.put("var", new ParamValue(String.valueOf(9), 9)); + vars.put("param", new ParamValue(String.valueOf(9999), 9999)); // when cmd.execute("`var`"); // then - verify(logger).printOut("`var`: 9"); + verify(logger).printOut(":param `var` => 9"); verifyNoMoreInteractions(logger); } @Test public void runCommandWithSpecialCharacters() throws CommandException { // given - vars.put("var `", 9); - vars.put("param", 9999); + vars.put("var `", new ParamValue(String.valueOf(9), 9)); + vars.put("param", new ParamValue(String.valueOf(9999), 9999)); // when cmd.execute("`var ```"); // then - verify(logger).printOut("`var ```: 9"); + verify(logger).printOut(":param `var ``` => 9"); verifyNoMoreInteractions(logger); } @@ -127,7 +131,7 @@ public void runCommandWithUnknownArg() throws CommandException { thrown.expect(CommandException.class); thrown.expectMessage(containsString("Unknown parameter: bob")); // given - vars.put("var", 9); + vars.put("var", new ParamValue(String.valueOf(9), 9)); // when cmd.execute("bob"); } diff --git a/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeValue.java b/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeValue.java index 1c0b6daf..42f55995 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeValue.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeValue.java @@ -2,9 +2,20 @@ import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.value.Uncoercible; -import org.neo4j.driver.v1.types.*; +import org.neo4j.driver.v1.types.Entity; +import org.neo4j.driver.v1.types.IsoDuration; +import org.neo4j.driver.v1.types.Node; +import org.neo4j.driver.v1.types.Path; +import org.neo4j.driver.v1.types.Point; +import org.neo4j.driver.v1.types.Relationship; +import org.neo4j.driver.v1.types.Type; import org.neo4j.driver.v1.util.Function; +import java.time.ZonedDateTime; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; import java.util.List; import java.util.Map; @@ -168,6 +179,41 @@ public Path asPath() { throw new Uncoercible(getClass().getSimpleName(), "Path"); } + @Override + public LocalDate asLocalDate() { + throw new Uncoercible(getClass().getSimpleName(), "LocalDate"); + } + + @Override + public OffsetTime asOffsetTime() { + throw new Uncoercible(getClass().getSimpleName(), "OffsetTime"); + } + + @Override + public LocalTime asLocalTime() { + throw new Uncoercible(getClass().getSimpleName(), "LocalTime"); + } + + @Override + public LocalDateTime asLocalDateTime() { + throw new Uncoercible(getClass().getSimpleName(), "LocalDateTime"); + } + + @Override + public ZonedDateTime asZonedDateTime() { + throw new Uncoercible(getClass().getSimpleName(), "ZonedDateTime"); + } + + @Override + public IsoDuration asIsoDuration() { + throw new Uncoercible(getClass().getSimpleName(), "IsoDuration"); + } + + @Override + public Point asPoint() { + throw new Uncoercible(getClass().getSimpleName(), "Point"); + } + @Override public Value get(String key, Value defaultValue) { return null;