001//
002// MIT License
003//
004// Copyright (c) 2021 Alexander Söderberg & Contributors
005//
006// Permission is hereby granted, free of charge, to any person obtaining a copy
007// of this software and associated documentation files (the "Software"), to deal
008// in the Software without restriction, including without limitation the rights
009// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010// copies of the Software, and to permit persons to whom the Software is
011// furnished to do so, subject to the following conditions:
012//
013// The above copyright notice and this permission notice shall be included in all
014// copies or substantial portions of the Software.
015//
016// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022// SOFTWARE.
023//
024package cloud.commandframework.arguments.standard;
025
026import cloud.commandframework.ArgumentDescription;
027import cloud.commandframework.arguments.CommandArgument;
028import cloud.commandframework.arguments.parser.ArgumentParseResult;
029import cloud.commandframework.arguments.parser.ArgumentParser;
030import cloud.commandframework.captions.CaptionVariable;
031import cloud.commandframework.captions.StandardCaptionKeys;
032import cloud.commandframework.context.CommandContext;
033import cloud.commandframework.exceptions.parsing.NoInputProvidedException;
034import cloud.commandframework.exceptions.parsing.ParserException;
035import cloud.commandframework.util.StringUtils;
036import org.checkerframework.checker.nullness.qual.NonNull;
037
038import java.util.Collections;
039import java.util.List;
040import java.util.Queue;
041import java.util.StringJoiner;
042import java.util.function.BiFunction;
043import java.util.regex.Matcher;
044import java.util.regex.Pattern;
045
046@SuppressWarnings("unused")
047public final class StringArgument<C> extends CommandArgument<C, String> {
048
049    private static final Pattern QUOTED_DOUBLE = Pattern.compile("\"(?<inner>(?:[^\"\\\\]|\\\\.)*)\"");
050    private static final Pattern QUOTED_SINGLE = Pattern.compile("'(?<inner>(?:[^'\\\\]|\\\\.)*)'");
051
052    private final StringMode stringMode;
053
054    private StringArgument(
055            final boolean required,
056            final @NonNull String name,
057            final @NonNull StringMode stringMode,
058            final @NonNull String defaultValue,
059            final @NonNull BiFunction<@NonNull CommandContext<C>, @NonNull String,
060                    @NonNull List<@NonNull String>> suggestionsProvider,
061            final @NonNull ArgumentDescription defaultDescription
062    ) {
063        super(required, name, new StringParser<>(stringMode, suggestionsProvider),
064                defaultValue, String.class, suggestionsProvider, defaultDescription
065        );
066        this.stringMode = stringMode;
067    }
068
069    /**
070     * Create a new builder
071     *
072     * @param name Name of the argument
073     * @param <C>  Command sender type
074     * @return Created builder
075     */
076    public static <C> StringArgument.@NonNull Builder<C> newBuilder(final @NonNull String name) {
077        return new StringArgument.Builder<>(name);
078    }
079
080    /**
081     * Create a new required single string command argument
082     *
083     * @param name Argument name
084     * @param <C>  Command sender type
085     * @return Created argument
086     */
087    public static <C> @NonNull CommandArgument<C, String> of(final @NonNull String name) {
088        return StringArgument.<C>newBuilder(name).single().asRequired().build();
089    }
090
091    /**
092     * Create a new required command argument
093     *
094     * @param name       Argument name
095     * @param stringMode String mode
096     * @param <C>        Command sender type
097     * @return Created argument
098     */
099    public static <C> @NonNull CommandArgument<C, String> of(
100            final @NonNull String name,
101            final @NonNull StringMode stringMode
102    ) {
103        return StringArgument.<C>newBuilder(name).withMode(stringMode).asRequired().build();
104    }
105
106    /**
107     * Create a new optional single string command argument
108     *
109     * @param name Argument name
110     * @param <C>  Command sender type
111     * @return Created argument
112     */
113    public static <C> @NonNull CommandArgument<C, String> optional(final @NonNull String name) {
114        return StringArgument.<C>newBuilder(name).single().asOptional().build();
115    }
116
117    /**
118     * Create a new optional command argument
119     *
120     * @param name       Argument name
121     * @param stringMode String mode
122     * @param <C>        Command sender type
123     * @return Created argument
124     */
125    public static <C> @NonNull CommandArgument<C, String> optional(
126            final @NonNull String name,
127            final @NonNull StringMode stringMode
128    ) {
129        return StringArgument.<C>newBuilder(name).withMode(stringMode).asOptional().build();
130    }
131
132    /**
133     * Create a new required command argument with a default value
134     *
135     * @param name          Argument name
136     * @param defaultString Default string
137     * @param <C>           Command sender type
138     * @return Created argument
139     */
140    public static <C> @NonNull CommandArgument<C, String> optional(
141            final @NonNull String name,
142            final @NonNull String defaultString
143    ) {
144        return StringArgument.<C>newBuilder(name).asOptionalWithDefault(defaultString).build();
145    }
146
147    /**
148     * Create a new required command argument with the 'single' parsing mode
149     *
150     * @param name Argument name
151     * @param <C>  Command sender type
152     * @return Created argument
153     */
154    public static <C> @NonNull CommandArgument<C, String> single(final @NonNull String name) {
155        return of(name, StringMode.SINGLE);
156    }
157
158    /**
159     * Create a new required command argument with the 'greedy' parsing mode
160     *
161     * @param name Argument name
162     * @param <C>  Command sender type
163     * @return Created argument
164     */
165    public static <C> @NonNull CommandArgument<C, String> greedy(final @NonNull String name) {
166        return of(name, StringMode.GREEDY);
167    }
168
169    /**
170     * Create a new required command argument with the 'quoted' parsing mode
171     *
172     * @param name Argument name
173     * @param <C>  Command sender type
174     * @return Created argument
175     */
176    public static <C> @NonNull CommandArgument<C, String> quoted(final @NonNull String name) {
177        return of(name, StringMode.QUOTED);
178    }
179
180    /**
181     * Get the string mode
182     *
183     * @return String mode
184     */
185    public @NonNull StringMode getStringMode() {
186        return this.stringMode;
187    }
188
189
190    public enum StringMode {
191        SINGLE,
192        GREEDY,
193        QUOTED
194    }
195
196
197    public static final class Builder<C> extends CommandArgument.Builder<C, String> {
198
199        private StringMode stringMode = StringMode.SINGLE;
200        private BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider = (v1, v2) -> Collections.emptyList();
201
202        private Builder(final @NonNull String name) {
203            super(String.class, name);
204        }
205
206        /**
207         * Set the String mode
208         *
209         * @param stringMode String mode to parse with
210         * @return Builder instance
211         */
212        private @NonNull Builder<C> withMode(final @NonNull StringMode stringMode) {
213            this.stringMode = stringMode;
214            return this;
215        }
216
217        /**
218         * Set the string mode to greedy
219         *
220         * @return Builder instance
221         */
222        public @NonNull Builder<C> greedy() {
223            this.stringMode = StringMode.GREEDY;
224            return this;
225        }
226
227        /**
228         * Set the string mode to single
229         *
230         * @return Builder instance
231         */
232        public @NonNull Builder<C> single() {
233            this.stringMode = StringMode.SINGLE;
234            return this;
235        }
236
237        /**
238         * Set the string mode to greedy
239         *
240         * @return Builder instance
241         */
242        public @NonNull Builder<C> quoted() {
243            this.stringMode = StringMode.QUOTED;
244            return this;
245        }
246
247        /**
248         * Set the suggestions provider
249         *
250         * @param suggestionsProvider Suggestions provider
251         * @return Builder instance
252         */
253        @Override
254        public @NonNull Builder<C> withSuggestionsProvider(
255                final @NonNull BiFunction<@NonNull CommandContext<C>,
256                        @NonNull String, @NonNull List<@NonNull String>> suggestionsProvider
257        ) {
258            this.suggestionsProvider = suggestionsProvider;
259            return this;
260        }
261
262        /**
263         * Builder a new string argument
264         *
265         * @return Constructed argument
266         */
267        @Override
268        public @NonNull StringArgument<C> build() {
269            return new StringArgument<>(this.isRequired(), this.getName(), this.stringMode,
270                    this.getDefaultValue(), this.suggestionsProvider, this.getDefaultDescription()
271            );
272        }
273
274    }
275
276
277    public static final class StringParser<C> implements ArgumentParser<C, String> {
278
279        private final StringMode stringMode;
280        private final BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider;
281
282        /**
283         * Construct a new string parser
284         *
285         * @param stringMode          String parsing mode
286         * @param suggestionsProvider Suggestions provider
287         */
288        public StringParser(
289                final @NonNull StringMode stringMode,
290                final @NonNull BiFunction<@NonNull CommandContext<C>, @NonNull String,
291                        @NonNull List<@NonNull String>> suggestionsProvider
292        ) {
293            this.stringMode = stringMode;
294            this.suggestionsProvider = suggestionsProvider;
295        }
296
297        @Override
298        public @NonNull ArgumentParseResult<String> parse(
299                final @NonNull CommandContext<C> commandContext,
300                final @NonNull Queue<@NonNull String> inputQueue
301        ) {
302            final String input = inputQueue.peek();
303            if (input == null) {
304                return ArgumentParseResult.failure(new NoInputProvidedException(
305                        StringParser.class,
306                        commandContext
307                ));
308            }
309
310            if (this.stringMode == StringMode.SINGLE) {
311                inputQueue.remove();
312                return ArgumentParseResult.success(input);
313            } else if (this.stringMode == StringMode.QUOTED) {
314                final StringJoiner sj = new StringJoiner(" ");
315                for (final String string : inputQueue) {
316                    sj.add(string);
317                }
318                final String string = sj.toString();
319
320                final Matcher doubleMatcher = QUOTED_DOUBLE.matcher(string);
321                String doubleMatch = null;
322                if (doubleMatcher.find()) {
323                    doubleMatch = doubleMatcher.group("inner");
324                }
325                final Matcher singleMatcher = QUOTED_SINGLE.matcher(string);
326                String singleMatch = null;
327                if (singleMatcher.find()) {
328                    singleMatch = singleMatcher.group("inner");
329                }
330
331                String inner = null;
332                if (singleMatch != null && doubleMatch != null) {
333                    final int singleIndex = string.indexOf(singleMatch);
334                    final int doubleIndex = string.indexOf(doubleMatch);
335                    inner = doubleIndex < singleIndex ? doubleMatch : singleMatch;
336                } else if (singleMatch == null && doubleMatch != null) {
337                    inner = doubleMatch;
338                } else if (singleMatch != null) {
339                    inner = singleMatch;
340                }
341
342                if (inner != null) {
343                    final int numSpaces = StringUtils.countCharOccurrences(inner, ' ');
344                    for (int i = 0; i <= numSpaces; i++) {
345                        inputQueue.remove();
346                    }
347                } else {
348                    inner = inputQueue.remove();
349                    if (inner.startsWith("\"") || inner.startsWith("'")) {
350                        return ArgumentParseResult.failure(new StringParseException(sj.toString(),
351                                StringMode.QUOTED, commandContext
352                        ));
353                    }
354                }
355
356                inner = inner.replace("\\\"", "\"").replace("\\'", "'");
357
358                return ArgumentParseResult.success(inner);
359            }
360
361            final StringJoiner sj = new StringJoiner(" ");
362            final int size = inputQueue.size();
363
364            boolean started = false;
365            boolean finished = false;
366
367            char start = ' ';
368            for (int i = 0; i < size; i++) {
369                String string = inputQueue.peek();
370
371                if (string == null) {
372                    break;
373                }
374
375                sj.add(string);
376                inputQueue.remove();
377            }
378
379            return ArgumentParseResult.success(sj.toString());
380        }
381
382        @Override
383        public @NonNull List<@NonNull String> suggestions(
384                final @NonNull CommandContext<C> commandContext,
385                final @NonNull String input
386        ) {
387            return this.suggestionsProvider.apply(commandContext, input);
388        }
389
390        @Override
391        public boolean isContextFree() {
392            return true;
393        }
394
395        /**
396         * Get the string mode
397         *
398         * @return String mode
399         */
400        public @NonNull StringMode getStringMode() {
401            return this.stringMode;
402        }
403
404    }
405
406
407    public static final class StringParseException extends ParserException {
408
409        private static final long serialVersionUID = -8903115465005472945L;
410        private final String input;
411        private final StringMode stringMode;
412
413        /**
414         * Construct a new string parse exception
415         *
416         * @param input      Input
417         * @param stringMode String mode
418         * @param context    Command context
419         */
420        public StringParseException(
421                final @NonNull String input,
422                final @NonNull StringMode stringMode,
423                final @NonNull CommandContext<?> context
424        ) {
425            super(
426                    StringParser.class,
427                    context,
428                    StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_STRING,
429                    CaptionVariable.of("input", input),
430                    CaptionVariable.of("stringMode", stringMode.name())
431            );
432            this.input = input;
433            this.stringMode = stringMode;
434        }
435
436
437        /**
438         * Get the input provided by the sender
439         *
440         * @return Input
441         */
442        public @NonNull String getInput() {
443            return this.input;
444        }
445
446        /**
447         * Get the string mode
448         *
449         * @return String mode
450         */
451        public @NonNull StringMode getStringMode() {
452            return this.stringMode;
453        }
454
455    }
456
457}