Skip to content

Commit 116514f

Browse files
committed
Simplify usage pattern builder and thereby fix some edge cases
1 parent 5ac6ba1 commit 116514f

File tree

4 files changed

+69
-49
lines changed

4 files changed

+69
-49
lines changed

src/main/java/net/kautler/command/api/Command.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@
5151
* @param <M> the class of the messages for which this command can be triggered
5252
*/
5353
public interface Command<M> {
54+
/**
55+
* The regex pattern string for one parameter separator character. It matches one whitespaces except newline.
56+
*/
57+
String PARAMETER_SEPARATOR_CHARACTER = "[\\s&&[^\\n]]";
58+
5459
/**
5560
* The pattern that is used to split parameters. It matches an arbitrary amount of whitespaces except newlines.
5661
*/
57-
Pattern PARAMETER_SEPARATOR_PATTERN = Pattern.compile("[\\s&&[^\\n]]++");
62+
Pattern PARAMETER_SEPARATOR_PATTERN = Pattern.compile(PARAMETER_SEPARATOR_CHARACTER + "++");
5863

5964
/**
6065
* Returns the aliases for this command.

src/main/java/net/kautler/command/api/CommandHandler.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import static java.util.stream.Collectors.joining;
5151
import static java.util.stream.Collectors.toList;
5252
import static java.util.stream.Collectors.toMap;
53+
import static net.kautler.command.api.Command.PARAMETER_SEPARATOR_CHARACTER;
5354
import static net.kautler.command.api.Command.getParameters;
5455

5556
/**
@@ -209,7 +210,9 @@ protected void doSetCommands(Instance<Command<? super M>> commands) {
209210
commandPattern = Pattern.compile(
210211
commandByAlias.keySet().stream()
211212
.map(Pattern::quote)
212-
.collect(joining("|", "(?s)^(?<alias>", ")(?=\\s|$)[\\s&&[^\\n]]*+(?<parameterString>.*+)$")));
213+
.collect(joining("|", "(?s)^(?<alias>", ")(?=\\s|$)"
214+
+ "\\s?+" + PARAMETER_SEPARATOR_CHARACTER + "*+"
215+
+ "(?<parameterString>.*+)$")));
213216
}
214217

215218
/**

src/main/java/net/kautler/command/usage/UsageParserRuleContext.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
import net.kautler.command.usage.UsageParser.PlaceholderContext;
2525
import net.kautler.command.usage.UsageParser.PlaceholderWithWhitespaceContext;
2626
import org.antlr.v4.runtime.ParserRuleContext;
27+
import org.antlr.v4.runtime.tree.ParseTree;
2728

2829
import java.util.List;
30+
import java.util.Optional;
2931

3032
import static java.util.Collections.emptyList;
3133

@@ -36,8 +38,8 @@ public abstract class UsageParserRuleContext extends ParserRuleContext {
3638
/**
3739
* Constructs a new usage parser rule context.
3840
*/
39-
UsageParserRuleContext(ParserRuleContext parent, int invokingState) {
40-
super(parent, invokingState);
41+
UsageParserRuleContext(ParserRuleContext parent, int invokingStateNumber) {
42+
super(parent, invokingStateNumber);
4143
}
4244

4345
/**
@@ -109,4 +111,22 @@ public AlternativesContext alternatives() {
109111
public LiteralContext literal() {
110112
return null;
111113
}
114+
115+
/**
116+
* Returns the single child of this usage parser rule context or
117+
* an empty optional if there are no or multiple children.
118+
*
119+
* @return the single child if present
120+
*/
121+
public Optional<ParseTree> getSingleChild() {
122+
if ((placeholder() == null)
123+
&& (placeholderWithWhitespace() == null)
124+
&& (literal() == null)
125+
&& (optional() == null)
126+
&& (alternatives() == null)) {
127+
return Optional.empty();
128+
} else {
129+
return Optional.of(getChild(0));
130+
}
131+
}
112132
}

src/main/java/net/kautler/command/usage/UsagePatternBuilder.java

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@
2424
import net.kautler.command.usage.UsageParser.PlaceholderWithWhitespaceContext;
2525
import net.kautler.command.usage.UsageParser.UsageContext;
2626
import org.antlr.v4.runtime.RuleContext;
27+
import org.antlr.v4.runtime.tree.ParseTree;
2728

2829
import javax.enterprise.context.ApplicationScoped;
2930
import java.util.List;
3031
import java.util.Map;
32+
import java.util.Optional;
3133
import java.util.StringJoiner;
3234
import java.util.concurrent.ConcurrentHashMap;
3335
import java.util.concurrent.CopyOnWriteArrayList;
3436
import java.util.regex.Pattern;
35-
import java.util.stream.Collector;
3637

3738
import static java.lang.String.format;
38-
import static java.util.regex.Matcher.quoteReplacement;
3939
import static java.util.stream.Collectors.joining;
40-
import static net.kautler.command.api.Command.PARAMETER_SEPARATOR_PATTERN;
40+
import static net.kautler.command.api.Command.PARAMETER_SEPARATOR_CHARACTER;
4141

4242
/**
4343
* A usage visitor that constructs a regular expression pattern to parse command arguments according to the defined
@@ -46,10 +46,22 @@
4646
@ApplicationScoped
4747
public class UsagePatternBuilder extends UsageBaseVisitor<String> {
4848
/**
49-
* A regular expression part that matches a parameter separator or the end of string.
49+
* A regular expression part that matches a parameter separator that is not preceded by start or end of input.
5050
*/
5151
private static final String PARAMETER_BOUNDARY_PATTERN_PART =
52-
format("(?:%s|$)", PARAMETER_SEPARATOR_PATTERN.pattern());
52+
format("(?:(?<!^)%s++(?!$))?", PARAMETER_SEPARATOR_CHARACTER);
53+
54+
/**
55+
* A regular expression part that matches a parameter separator character as lookbehind.
56+
*/
57+
private static final String PRECEDED_BY_PARAMETER_SEPARATOR_CHARACTER =
58+
format("(?<=^|(?<!^)%s)", PARAMETER_SEPARATOR_CHARACTER);
59+
60+
/**
61+
* A regular expression part that matches a parameter separator character as lookahead.
62+
*/
63+
private static final String FOLLOWED_BY_PARAMETER_SEPARATOR_CHARACTER =
64+
format("(?=%s(?!$)|$)", PARAMETER_SEPARATOR_CHARACTER);
5365

5466
/**
5567
* A cache for regular expression patterns for usage specifications, so that the usage string does not need to be
@@ -136,49 +148,17 @@ public String visitOptional(OptionalContext ctx) {
136148
* @return the visitor result
137149
*/
138150
private String visitParentParserRuleContext(UsageParserRuleContext ctx) {
139-
boolean hasSingleChild = (ctx.placeholder() != null)
140-
|| (ctx.placeholderWithWhitespace() != null)
141-
|| (ctx.literal() != null)
142-
|| (ctx.optional() != null)
143-
|| (ctx.alternatives() != null);
144-
if (hasSingleChild) {
145-
return ctx.getChild(0).accept(this);
151+
Optional<ParseTree> singleChild = ctx.getSingleChild();
152+
if (singleChild.isPresent()) {
153+
return singleChild.get().accept(this);
146154
} else {
147155
List<ExpressionContext> expressions = ctx.expression();
148156
if (expressions.isEmpty()) {
149157
throw new AssertionError("Unhandled case");
150158
} else {
151-
boolean[] first = { true };
152-
boolean[] leftOptional = { false };
153-
boolean[] leftHasWhitespace = { false };
154159
return expressions.stream()
155160
.map(this::visitParentParserRuleContext)
156-
.collect(Collector.of(
157-
StringBuilder::new,
158-
(left, right) -> {
159-
boolean rightOptional = right.endsWith(")?");
160-
if (first[0]) {
161-
left.append(right);
162-
first[0] = false;
163-
leftHasWhitespace[0] = false;
164-
} else if (leftOptional[0] && !leftHasWhitespace[0]) {
165-
left.insert(left.length() - 2, PARAMETER_BOUNDARY_PATTERN_PART);
166-
left.append(right);
167-
leftHasWhitespace[0] = false;
168-
} else if (rightOptional) {
169-
left.append(right.replaceFirst(
170-
"^\\(\\?(?::|<[^>]++>)",
171-
"$0" + quoteReplacement(PARAMETER_BOUNDARY_PATTERN_PART)));
172-
leftHasWhitespace[0] = true;
173-
} else {
174-
left.append(PARAMETER_BOUNDARY_PATTERN_PART);
175-
left.append(right);
176-
leftHasWhitespace[0] = false;
177-
}
178-
leftOptional[0] = rightOptional;
179-
},
180-
(left, right) -> { throw new UnsupportedOperationException(); },
181-
Object::toString));
161+
.collect(joining(PARAMETER_BOUNDARY_PATTERN_PART));
182162
}
183163
}
184164
}
@@ -187,21 +167,33 @@ private String visitParentParserRuleContext(UsageParserRuleContext ctx) {
187167
public String visitPlaceholder(PlaceholderContext ctx) {
188168
String tokenText = ctx.getText();
189169
String tokenName = tokenText.substring(1, tokenText.length() - 1);
190-
return format("(?<%s>\\S+)", getGroupName(ctx, tokenName));
170+
return format(
171+
"%s(?<%s>\\S+)%s",
172+
PRECEDED_BY_PARAMETER_SEPARATOR_CHARACTER,
173+
getGroupName(ctx, tokenName),
174+
FOLLOWED_BY_PARAMETER_SEPARATOR_CHARACTER);
191175
}
192176

193177
@Override
194178
public String visitPlaceholderWithWhitespace(PlaceholderWithWhitespaceContext ctx) {
195179
String tokenText = ctx.getText();
196180
String tokenName = tokenText.substring(1, tokenText.length() - 4);
197-
return format("(?<%s>.+)", getGroupName(ctx, tokenName));
181+
return format(
182+
"%s(?<%s>.+)$",
183+
PRECEDED_BY_PARAMETER_SEPARATOR_CHARACTER,
184+
getGroupName(ctx, tokenName));
198185
}
199186

200187
@Override
201188
public String visitLiteral(LiteralContext ctx) {
202189
String tokenText = ctx.getText();
203190
String tokenName = tokenText.substring(1, tokenText.length() - 1);
204-
return format("(?<%s>%s)", getGroupName(ctx, tokenName, "Literal"), Pattern.quote(tokenName));
191+
return format(
192+
"%s(?<%s>%s)%s",
193+
PRECEDED_BY_PARAMETER_SEPARATOR_CHARACTER,
194+
getGroupName(ctx, tokenName, "Literal"),
195+
Pattern.quote(tokenName),
196+
FOLLOWED_BY_PARAMETER_SEPARATOR_CHARACTER);
205197
}
206198

207199
/**
@@ -264,7 +256,7 @@ private String getGroupName(UsageParserRuleContext ctx, String tokenName, String
264256
*/
265257
private UsageContext getUsageContext(UsageParserRuleContext ctx) {
266258
RuleContext current = ctx;
267-
while (current.getParent() != null) {
259+
while (!(current instanceof UsageContext)) {
268260
current = current.getParent();
269261
}
270262
return (UsageContext) current;

0 commit comments

Comments
 (0)