From 256b244c0e21f3bfc77fc251ae47ca51e639fa0b Mon Sep 17 00:00:00 2001 From: dgelessus <dgelessus@users.noreply.github.com> Date: Wed, 13 May 2020 16:16:47 +0200 Subject: [PATCH] Separate command parsing into splitting and validation steps The splitting step splits the argument string into separate arguments according to a Parameters object, and associates the split arguments with the corresponding parameters. However, it performs absolutely no checking - splitArgs will split any input string without errors. (Any excess arguments that cannot be associated with a parameter are returned as a separate string.) The validation step takes the output of the splitting step and validates it against the Parameters object. Currently this only involves checking that there is no excess input and validating argument count constraints. This separation will make it easier to work with potentially incomplete or invalid command arguments, which is necessary for the completion and inspection features. It also makes the parsing code a little more organized. For now this change is purely internal to the command parsing code. The external API of Parameters and related classes has not changed (although they may be changed in the future). --- .../java/de/prob2/jupyter/CommandUtils.java | 66 +++++---- src/main/java/de/prob2/jupyter/Parameter.java | 20 +-- .../de/prob2/jupyter/PositionalParameter.java | 125 +++++++++--------- .../java/de/prob2/jupyter/SplitArguments.java | 40 ++++++ .../java/de/prob2/jupyter/SplitResult.java | 23 ++++ 5 files changed, 176 insertions(+), 98 deletions(-) create mode 100644 src/main/java/de/prob2/jupyter/SplitArguments.java create mode 100644 src/main/java/de/prob2/jupyter/SplitResult.java diff --git a/src/main/java/de/prob2/jupyter/CommandUtils.java b/src/main/java/de/prob2/jupyter/CommandUtils.java index 786e4f1..35713d5 100644 --- a/src/main/java/de/prob2/jupyter/CommandUtils.java +++ b/src/main/java/de/prob2/jupyter/CommandUtils.java @@ -89,53 +89,61 @@ public final class CommandUtils { } } - private static <T> @NotNull String parseSingleArg(final @NotNull ParsedArguments parsed, final @NotNull String remainingArgs, final @NotNull PositionalParameter<T> param) { - final Parameter.ParseResult<T> parsedSingleArg = param.parse(remainingArgs); - parsed.put(param, parsedSingleArg.getParsedArg()); - return parsedSingleArg.getRemainingArgString(); - } - - private static <T> void checkParsedParameter(final @NotNull ParsedArguments parsed, final @NotNull PositionalParameter<T> param) { - if (!parsed.containsKey(param)) { - if (param.isOptional()) { - parsed.put(param, param.getDefaultValue()); - } else { - throw new UserErrorException("Missing required parameter " + param.getIdentifier()); - } - } - } - - public static @NotNull ParsedArguments parseArgs(final @NotNull Parameters parameters, final @NotNull String argString) { - final ParsedArguments parsed = new ParsedArguments(Collections.emptyMap()); + public static @NotNull SplitResult splitArgs(final @NotNull Parameters parameters, final @NotNull String argString) { + final SplitArguments splitArgs = new SplitArguments(Collections.emptyMap()); String remainingArgs; if (parameters.getBodyParam().isPresent()) { - final String[] split = argString.split("\n", 2); - final PositionalParameter.RequiredRemainder bodyParam = parameters.getBodyParam().get(); - if (split.length < 2) { - throw new UserErrorException("Missing required body " + bodyParam.getIdentifier()); + final String[] argsAndBody = argString.split("\n", 2); + remainingArgs = argsAndBody[0]; + if (argsAndBody.length > 1) { + splitArgs.add(parameters.getBodyParam().get(), argsAndBody[1]); } - remainingArgs = split[0]; - parsed.put(bodyParam, split[1]); } else { remainingArgs = argString; } - for (final PositionalParameter<?> param : parameters.getPositionalParameters()) { + for (int i = 0; i < parameters.getPositionalParameters().size();) { + final PositionalParameter<?> param = parameters.getPositionalParameters().get(i); if (remainingArgs.isEmpty()) { break; } - remainingArgs = parseSingleArg(parsed, remainingArgs, param); + final Parameter.SplitResult splitSingleArg = param.split(remainingArgs); + splitArgs.add(param, splitSingleArg.getSplitArg()); + remainingArgs = splitSingleArg.getRemainingArgString(); + + if (!param.isRepeating()) { + i++; + } } - if (!remainingArgs.isEmpty()) { - throw new UserErrorException("Expected at most " + parameters.getPositionalParameters().size() + " arguments, got extra argument: " + remainingArgs); + + return new SplitResult(splitArgs, remainingArgs); + } + + private static <T> void validateSplitParameter(final @NotNull ParsedArguments parsed, final @NotNull SplitArguments splitArgs, final @NotNull Parameter<T> param) { + parsed.put(param, param.validate(splitArgs.get(param))); + } + + public static @NotNull ParsedArguments validateSplitArgs(final @NotNull Parameters parameters, final SplitResult split) { + if (!split.getRemaining().isEmpty()) { + throw new UserErrorException("Expected at most " + parameters.getPositionalParameters().size() + " arguments, got extra argument: " + split.getRemaining()); } + + final ParsedArguments parsed = new ParsedArguments(Collections.emptyMap()); + for (final PositionalParameter<?> param : parameters.getPositionalParameters()) { - checkParsedParameter(parsed, param); + validateSplitParameter(parsed, split.getArguments(), param); } + + parameters.getBodyParam().ifPresent(bodyParam -> validateSplitParameter(parsed, split.getArguments(), bodyParam)); + return parsed; } + public static @NotNull ParsedArguments parseArgs(final @NotNull Parameters parameters, final @NotNull String argString) { + return validateSplitArgs(parameters, splitArgs(parameters, argString)); + } + public static @NotNull Map<@NotNull String, @NotNull String> parsePreferences(final @NotNull List<@NotNull String> args) { final Map<String, String> preferences = new HashMap<>(); for (final String arg : args) { diff --git a/src/main/java/de/prob2/jupyter/Parameter.java b/src/main/java/de/prob2/jupyter/Parameter.java index bb1fcb9..f609eb1 100644 --- a/src/main/java/de/prob2/jupyter/Parameter.java +++ b/src/main/java/de/prob2/jupyter/Parameter.java @@ -1,23 +1,25 @@ package de.prob2.jupyter; +import java.util.List; + import com.google.common.base.MoreObjects; import org.jetbrains.annotations.NotNull; public abstract class Parameter<T> { - public static final class ParseResult<T> { - private final T parsedArg; + public static final class SplitResult { + private final @NotNull String splitArg; private final @NotNull String remainingArgString; - public ParseResult(final T parsedArg, final @NotNull String remainingArgString) { + public SplitResult(final @NotNull String splitArg, final @NotNull String remainingArgString) { super(); - this.parsedArg = parsedArg; + this.splitArg = splitArg; this.remainingArgString = remainingArgString; } - public T getParsedArg() { - return this.parsedArg; + public @NotNull String getSplitArg() { + return this.splitArg; } public @NotNull String getRemainingArgString() { @@ -37,11 +39,11 @@ public abstract class Parameter<T> { return this.identifier; } - public abstract boolean isOptional(); + public abstract boolean isRepeating(); - public abstract T getDefaultValue(); + public abstract Parameter.SplitResult split(final @NotNull String argString); - public abstract Parameter.ParseResult<T> parse(final @NotNull String argString); + public abstract T validate(final @NotNull List<@NotNull String> argValues); @Override public String toString() { diff --git a/src/main/java/de/prob2/jupyter/PositionalParameter.java b/src/main/java/de/prob2/jupyter/PositionalParameter.java index 35c8988..f3d448e 100644 --- a/src/main/java/de/prob2/jupyter/PositionalParameter.java +++ b/src/main/java/de/prob2/jupyter/PositionalParameter.java @@ -1,54 +1,72 @@ package de.prob2.jupyter; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import org.jetbrains.annotations.NotNull; public abstract class PositionalParameter<T> extends Parameter<T> { - public static final class RequiredSingle extends PositionalParameter<@NotNull String> { - public RequiredSingle(final @NotNull String identifier) { + public abstract static class ExactlyOne extends PositionalParameter<@NotNull String> { + protected ExactlyOne(final @NotNull String identifier) { super(identifier); } @Override - public boolean isOptional() { + public boolean isRepeating() { return false; } @Override - public @NotNull String getDefaultValue() { - throw new UnsupportedOperationException("Not an optional parameter"); + public @NotNull String validate(final @NotNull List<@NotNull String> argValues) { + if (argValues.isEmpty()) { + throw new UserErrorException("Missing required parameter " + this.getIdentifier()); + } else if (argValues.size() > 1) { + throw new AssertionError("Regular (single) required parameter " + this.getIdentifier() + " has more than one value, this should never happen!"); + } + + return argValues.get(0); + } + } + + public abstract static class ZeroOrOne extends PositionalParameter<@NotNull Optional<String>> { + protected ZeroOrOne(final @NotNull String identifier) { + super(identifier); + } + + @Override + public boolean isRepeating() { + return false; } @Override - public @NotNull Parameter.ParseResult<@NotNull String> parse(final @NotNull String argString) { - final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2); - return new Parameter.ParseResult<>(split[0], split.length > 1 ? split[1] : ""); + public @NotNull Optional<String> validate(final @NotNull List<@NotNull String> argValues) { + if (argValues.size() > 1) { + throw new AssertionError("Regular (single) optional parameter " + this.getIdentifier() + " has more than one value, this should never happen!"); + } + + return argValues.stream().findAny(); } } - public static final class OptionalSingle extends PositionalParameter<@NotNull Optional<String>> { - public OptionalSingle(final @NotNull String identifier) { + public static final class RequiredSingle extends PositionalParameter.ExactlyOne { + public RequiredSingle(final @NotNull String identifier) { super(identifier); } @Override - public boolean isOptional() { - return true; + public @NotNull Parameter.SplitResult split(final @NotNull String argString) { + return splitOnce(argString); } - - @Override - public @NotNull Optional<String> getDefaultValue() { - return Optional.empty(); + } + + public static final class OptionalSingle extends PositionalParameter.ZeroOrOne { + public OptionalSingle(final @NotNull String identifier) { + super(identifier); } @Override - public @NotNull Parameter.ParseResult<@NotNull Optional<String>> parse(final @NotNull String argString) { - final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2); - return new Parameter.ParseResult<>(Optional.of(split[0]), split.length > 1 ? split[1] : ""); + public @NotNull Parameter.SplitResult split(final @NotNull String argString) { + return splitOnce(argString); } } @@ -58,19 +76,22 @@ public abstract class PositionalParameter<T> extends Parameter<T> { } @Override - public boolean isOptional() { - return false; + public boolean isRepeating() { + return true; } @Override - public @NotNull List<@NotNull String> getDefaultValue() { - throw new UnsupportedOperationException("Not an optional parameter"); + public @NotNull Parameter.SplitResult split(final @NotNull String argString) { + return splitOnce(argString); } @Override - public @NotNull Parameter.ParseResult<@NotNull List<@NotNull String>> parse(final @NotNull String argString) { - final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString); - return new Parameter.ParseResult<>(Arrays.asList(split), ""); + public @NotNull List<@NotNull String> validate(final @NotNull List<@NotNull String> argValues) { + if (argValues.isEmpty()) { + throw new UserErrorException("Missing required parameter " + this.getIdentifier()); + } + + return argValues; } } @@ -80,61 +101,40 @@ public abstract class PositionalParameter<T> extends Parameter<T> { } @Override - public boolean isOptional() { + public boolean isRepeating() { return true; } @Override - public @NotNull List<@NotNull String> getDefaultValue() { - return Collections.emptyList(); + public @NotNull Parameter.SplitResult split(final @NotNull String argString) { + return splitOnce(argString); } @Override - public @NotNull Parameter.ParseResult<@NotNull List<@NotNull String>> parse(final @NotNull String argString) { - final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString); - return new Parameter.ParseResult<>(Arrays.asList(split), ""); + public @NotNull List<@NotNull String> validate(final @NotNull List<@NotNull String> argValues) { + return argValues; } } - public static final class RequiredRemainder extends PositionalParameter<@NotNull String> { + public static final class RequiredRemainder extends PositionalParameter.ExactlyOne { public RequiredRemainder(final @NotNull String identifier) { super(identifier); } @Override - public boolean isOptional() { - return false; - } - - @Override - public @NotNull String getDefaultValue() { - throw new UnsupportedOperationException("Not an optional parameter"); - } - - @Override - public Parameter.ParseResult<@NotNull String> parse(final @NotNull String argString) { - return new Parameter.ParseResult<>(argString, ""); + public @NotNull Parameter.SplitResult split(final @NotNull String argString) { + return new Parameter.SplitResult(argString, ""); } } - public static final class OptionalRemainder extends PositionalParameter<@NotNull Optional<String>> { + public static final class OptionalRemainder extends PositionalParameter.ZeroOrOne { public OptionalRemainder(final @NotNull String identifier) { super(identifier); } @Override - public boolean isOptional() { - return true; - } - - @Override - public @NotNull Optional<String> getDefaultValue() { - return Optional.empty(); - } - - @Override - public Parameter.ParseResult<@NotNull Optional<String>> parse(final @NotNull String argString) { - return new Parameter.ParseResult<>(Optional.of(argString), ""); + public @NotNull Parameter.SplitResult split(final @NotNull String argString) { + return new Parameter.SplitResult(argString, ""); } } @@ -142,4 +142,9 @@ public abstract class PositionalParameter<T> extends Parameter<T> { super(identifier); } + @NotNull + static Parameter.SplitResult splitOnce(final @NotNull String argString) { + final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2); + return new Parameter.SplitResult(split[0], split.length > 1 ? split[1] : ""); + } } diff --git a/src/main/java/de/prob2/jupyter/SplitArguments.java b/src/main/java/de/prob2/jupyter/SplitArguments.java new file mode 100644 index 0000000..906b1fb --- /dev/null +++ b/src/main/java/de/prob2/jupyter/SplitArguments.java @@ -0,0 +1,40 @@ +package de.prob2.jupyter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.MoreObjects; + +import org.jetbrains.annotations.NotNull; + +public final class SplitArguments { + private final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull String>> values; + + public SplitArguments(final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull String>> values) { + super(); + + this.values = new HashMap<>(values); + } + + public boolean containsKey(final @NotNull Parameter<?> parameter) { + return this.values.containsKey(parameter); + } + + public @NotNull List<@NotNull String> get(final @NotNull Parameter<?> parameter) { + return this.values.getOrDefault(parameter, Collections.emptyList()); + } + + public void add(final @NotNull Parameter<?> parameter, final String value) { + this.values.computeIfAbsent(parameter, p -> new ArrayList<>()).add(value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("values", this.values) + .toString(); + } +} diff --git a/src/main/java/de/prob2/jupyter/SplitResult.java b/src/main/java/de/prob2/jupyter/SplitResult.java new file mode 100644 index 0000000..deed42c --- /dev/null +++ b/src/main/java/de/prob2/jupyter/SplitResult.java @@ -0,0 +1,23 @@ +package de.prob2.jupyter; + +import org.jetbrains.annotations.NotNull; + +public final class SplitResult { + private final @NotNull SplitArguments arguments; + private final @NotNull String remaining; + + public SplitResult(final @NotNull SplitArguments arguments, final @NotNull String remaining) { + super(); + + this.arguments = arguments; + this.remaining = remaining; + } + + public @NotNull SplitArguments getArguments() { + return this.arguments; + } + + public @NotNull String getRemaining() { + return this.remaining; + } +} -- GitLab