diff --git a/src/main/java/org/springframework/shell/core/ParserUtils.java b/src/main/java/org/springframework/shell/core/ParserUtils.java
deleted file mode 100644
index 434f7939..00000000
--- a/src/main/java/org/springframework/shell/core/ParserUtils.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright 2011-2012 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.shell.core;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-import org.springframework.util.Assert;
-
-/**
- * Utilities for parsing.
- *
- * @author Ben Alex
- * @since 1.0
- */
-public class ParserUtils {
-
- private ParserUtils() {}
-
- /**
- * Converts a particular buffer into a tokenized structure.
- *
- *
- * Properly treats double quotes (") as option delimiters.
- *
- *
- * Expects option names to be preceded by a single or double dash. We call this an "option marker".
- *
- *
- * Treats spaces as the default option tokenizer.
- *
- *
- * Any token without an option marker is considered the default. The default is returned in the Map as an element with an empty string key (""). There can only be a single default.
- *
- * @param remainingBuffer to tokenize
- * @return a Map where keys are the option names (minus any dashes) and values are the option values (any double-quotes are removed)
- */
- public static Map tokenize(final String remainingBuffer) {
- Assert.notNull(remainingBuffer, "Remaining buffer cannot be null, although it can be empty");
- Map result = new LinkedHashMap();
- StringBuilder currentOption = new StringBuilder();
- StringBuilder currentValue = new StringBuilder();
- boolean inQuotes = false;
-
- // Verify correct number of double quotes are present
- int count = 0;
- for (char c : remainingBuffer.toCharArray()) {
- if ('"' == c) {
- count++;
- }
- }
- Assert.isTrue(count % 2 == 0, "Cannot have an unbalanced number of quotation marks");
-
- if ("".equals(remainingBuffer.trim())) {
- // They've not specified anything, so exit now
- return result;
- }
-
- String[] split = remainingBuffer.split(" ");
- for (int i = 0; i < split.length; i++) {
- String currentToken = split[i];
-
- if (currentToken.startsWith("\"") && currentToken.endsWith("\"") && currentToken.length() > 1) {
- String tokenLessDelimiters = currentToken.substring(1, currentToken.length() - 1);
- currentValue.append(tokenLessDelimiters);
-
- // Store this token
- store(result, currentOption, currentValue);
- currentOption = new StringBuilder();
- currentValue = new StringBuilder();
- continue;
- }
-
- if (inQuotes) {
- // We're only interested in this token series ending
- if (currentToken.endsWith("\"")) {
- String tokenLessDelimiters = currentToken.substring(0, currentToken.length() - 1);
- currentValue.append(" ").append(tokenLessDelimiters);
- inQuotes = false;
-
- // Store this now-ended token series
- store(result, currentOption, currentValue);
- currentOption = new StringBuilder();
- currentValue = new StringBuilder();
- } else {
- // The current token series has not ended
- currentValue.append(" ").append(currentToken);
- }
- continue;
- }
-
- if (currentToken.startsWith("\"")) {
- // We're about to start a new delimited token
- String tokenLessDelimiters = currentToken.substring(1);
- currentValue.append(tokenLessDelimiters);
- inQuotes = true;
- continue;
- }
-
- if (currentToken.trim().equals("")) {
- // It's simply empty, so ignore it (ROO-23)
- continue;
- }
-
- if (currentToken.startsWith("--")) {
- // We're about to start a new option marker
- // First strip all of the - or -- or however many there are
- int lastIndex = currentToken.lastIndexOf("-");
- String tokenLessDelimiters = currentToken.substring(lastIndex + 1);
- currentOption.append(tokenLessDelimiters);
-
- // Store this token if it's the last one, or the next token starts with a "-"
- if (i + 1 == split.length) {
- // We're at the end of the tokens, so store this one and stop processing
- store(result, currentOption, currentValue);
- break;
- }
-
- if (split[i + 1].startsWith("-")) {
- // A new token is being started next iteration, so store this one now
- store(result, currentOption, currentValue);
- currentOption = new StringBuilder();
- currentValue = new StringBuilder();
- }
-
- continue;
- }
-
- // We must be in a standard token
-
- // If the standard token has no option name, we allow it to contain unquoted spaces
- if (currentOption.length() == 0) {
- if (currentValue.length() > 0) {
- // Existing content, so add a space first
- currentValue.append(" ");
- }
- currentValue.append(currentToken);
-
- // Store this token if it's the last one, or the next token starts with a "-"
- if (i + 1 == split.length) {
- // We're at the end of the tokens, so store this one and stop processing
- store(result, currentOption, currentValue);
- break;
- }
-
- if (split[i + 1].startsWith("--")) {
- // A new token is being started next iteration, so store this one now
- store(result, currentOption, currentValue);
- currentOption = new StringBuilder();
- currentValue = new StringBuilder();
- }
-
- continue;
- }
-
- // This is an ordinary token, so store it now
- currentValue.append(currentToken);
- store(result, currentOption, currentValue);
- currentOption = new StringBuilder();
- currentValue = new StringBuilder();
- }
-
- // Strip out an empty default option, if it was returned (ROO-379)
- if (result.containsKey("") && result.get("").trim().equals("")) {
- result.remove("");
- }
-
- return result;
- }
-
- private static void store(final Map results, final StringBuilder currentOption, final StringBuilder currentValue) {
- if (currentOption.length() > 0) {
- // There is an option marker
- String option = currentOption.toString();
- Assert.isTrue(!results.containsKey(option), "You cannot specify option '" + option + "' more than once in a single command");
- results.put(option, currentValue.toString());
- } else {
- // There was no option marker, so verify this isn't the first
- Assert.isTrue(!results.containsKey(""), "You cannot add more than one default option ('" + currentValue.toString() + "') in a single command");
- results.put("", currentValue.toString());
- }
- }
-}
diff --git a/src/main/java/org/springframework/shell/core/SimpleParser.java b/src/main/java/org/springframework/shell/core/SimpleParser.java
index cfd22910..313022df 100644
--- a/src/main/java/org/springframework/shell/core/SimpleParser.java
+++ b/src/main/java/org/springframework/shell/core/SimpleParser.java
@@ -45,7 +45,7 @@ import org.springframework.util.StringUtils;
/**
* Default implementation of {@link Parser}.
- *
+ *
* @author Ben Alex
* @since 1.0
*/
@@ -53,12 +53,16 @@ public class SimpleParser implements Parser {
// Constants
private static final Logger LOGGER = HandlerUtils.getLogger(SimpleParser.class);
+
private static final Comparator COMPARATOR = new NaturalOrderComparator();
// Fields
private final Object mutex = new Object();
+
private final Set> converters = new HashSet>();
+
private final Set commands = new HashSet();
+
private final Map availabilityIndicators = new HashMap();
private MethodTarget getAvailabilityIndicator(final String command) {
@@ -66,9 +70,8 @@ public class SimpleParser implements Parser {
}
/**
- * get all mandatory options keys. For the options with multiple keys, the
- * keys will be in one row.
- *
+ * get all mandatory options keys. For the options with multiple keys, the keys will be in one row.
+ *
* @param cliOptions options
* @return mandatory options keys
*/
@@ -78,7 +81,7 @@ public class SimpleParser implements Parser {
/**
* get all options key.
- *
+ *
* @param cliOptions
* @param includeOptionalOptions
* @return options keys
@@ -100,7 +103,6 @@ public class SimpleParser implements Parser {
return optionsKeys;
}
-
public ParseResult parse(final String rawInput) {
synchronized (mutex) {
Assert.notNull(rawInput, "Raw input required");
@@ -142,8 +144,9 @@ public class SimpleParser implements Parser {
// Attempt to parse
Map options = null;
try {
- options = ParserUtils.tokenize(methodTarget.getRemainingBuffer());
- } catch (IllegalArgumentException e) {
+ options = new Tokenizer(methodTarget.getRemainingBuffer()).getTokens();
+ }
+ catch (IllegalArgumentException e) {
LOGGER.warning(ExceptionUtils.extractRootCause(e).getMessage());
return null;
}
@@ -185,9 +188,9 @@ public class SimpleParser implements Parser {
boolean mandatory = !StringUtils.hasText(value) && cliOption.mandatory();
boolean specifiedKey = !StringUtils.hasText(value) && options.containsKey(sourcedFrom);
boolean specifiedKeyWithoutValue = false;
- if(specifiedKey){
+ if (specifiedKey) {
value = cliOption.specifiedDefaultValue();
- if("__NULL__".equals(value)){
+ if ("__NULL__".equals(value)) {
specifiedKeyWithoutValue = true;
}
}
@@ -255,7 +258,8 @@ public class SimpleParser implements Parser {
throw new IllegalStateException();
}
arguments.add(result);
- } catch (RuntimeException e) {
+ }
+ catch (RuntimeException e) {
LOGGER.warning(e.getClass().getName() + ": Failed to convert '" + value + "' to type "
+ requiredType.getSimpleName() + " for option '"
+ StringUtils.arrayToCommaDelimitedString(cliOption.key()) + "'");
@@ -263,7 +267,8 @@ public class SimpleParser implements Parser {
LOGGER.warning(e.getMessage());
}
return null;
- } finally {
+ }
+ finally {
CliOptionContext.resetOptionContext();
CliSimpleParserContext.resetSimpleParserContext();
}
@@ -274,13 +279,13 @@ public class SimpleParser implements Parser {
if (!unavailableOptions.isEmpty()) {
StringBuilder message = new StringBuilder();
if (unavailableOptions.size() == 1) {
- message.append("Option '").append(unavailableOptions.iterator().next()).append(
- "' is not available for this command. ");
+ message.append("Option '").append(unavailableOptions.iterator().next())
+ .append("' is not available for this command. ");
}
else {
- message.append("Options ").append(
- StringUtils.collectionToDelimitedString(unavailableOptions, ", ", "'", "'")).append(
- " are not available for this command. ");
+ message.append("Options ")
+ .append(StringUtils.collectionToDelimitedString(unavailableOptions, ", ", "'", "'"))
+ .append(" are not available for this command. ");
}
message.append("Use tab assist or the \"help\" command to see the legal options");
LOGGER.warning(message.toString());
@@ -304,7 +309,7 @@ public class SimpleParser implements Parser {
StringBuilder valueBuilder = new StringBuilder();
valueBuilder.append("You should specify value for option '");
- List> optionsKeys = getOptionsKeys(cliOptions,true);
+ List> optionsKeys = getOptionsKeys(cliOptions, true);
for (List keys : optionsKeys) {
boolean found = false;
for (String key : keys) {
@@ -324,7 +329,7 @@ public class SimpleParser implements Parser {
optionBuilder.append(", ");
}
}
- //remove the ", " in the end.
+ // remove the ", " in the end.
String hintForOption = optionBuilder.toString();
hintForOption = hintForOption.substring(0, hintForOption.length() - 2);
if (hintForOptions) {
@@ -338,7 +343,7 @@ public class SimpleParser implements Parser {
/**
* Normalises the given raw user input string ready for parsing
- *
+ *
* @param rawInput the string to normalise; can't be null
* @return a non-null string
*/
@@ -347,7 +352,8 @@ public class SimpleParser implements Parser {
return rawInput.replaceAll(" +", " ").trim();
}
- private Set getSpecifiedUnavailableOptions(final Set cliOptions, final Map options) {
+ private Set getSpecifiedUnavailableOptions(final Set cliOptions,
+ final Map options) {
Set cliOptionKeySet = new LinkedHashSet();
for (CliOption cliOption : cliOptions) {
for (String key : cliOption.key()) {
@@ -380,7 +386,8 @@ public class SimpleParser implements Parser {
logger.warning("Command '" + buffer + "' not found (for assistance press " + AbstractShell.completionKeys + ")");
}
- private Collection locateTargets(final String buffer, final boolean strictMatching, final boolean checkAvailabilityIndicators) {
+ private Collection locateTargets(final String buffer, final boolean strictMatching,
+ final boolean checkAvailabilityIndicators) {
Assert.notNull(buffer, "Buffer required");
final Collection result = new HashSet();
@@ -401,8 +408,10 @@ public class SimpleParser implements Parser {
+ method.toGenericString() + "'");
try {
available = (Boolean) mt.getMethod().invoke(mt.getTarget());
- // We should "break" here, but we loop over all to ensure no conflicting availability indicators are defined
- } catch (Exception e) {
+ // We should "break" here, but we loop over all to ensure no conflicting
+ // availability indicators are defined
+ }
+ catch (Exception e) {
available = false;
}
}
@@ -455,7 +464,8 @@ public class SimpleParser implements Parser {
for (int candidate = lastCommandWordUsed; candidate < commandWords.length; candidate++) {
if (lastWord != null && lastWord.length() > 0 && commandWords[candidate].startsWith(lastWord)) {
if (bufferToReturn == null) {
- // This is the first match, so ensure the intended match really represents the start of a command and not a later word within it
+ // This is the first match, so ensure the intended match really represents the start of a
+ // command and not a later word within it
if (lastCommandWordUsed == 0 && candidate > 0) {
// This is not a valid match
break next_buffer_loop;
@@ -565,8 +575,9 @@ public class SimpleParser implements Parser {
// Make a reasonable attempt at parsing the remainingBuffer
Map options;
try {
- options = ParserUtils.tokenize(methodTarget.getRemainingBuffer());
- } catch (IllegalArgumentException ex) {
+ options = new Tokenizer(methodTarget.getRemainingBuffer()).getTokens();
+ }
+ catch (IllegalArgumentException ex) {
// Assume any IllegalArgumentException is due to a quotation mark mismatch
candidates.add(new Completion(translated + "\""));
return 0;
@@ -579,7 +590,8 @@ public class SimpleParser implements Parser {
if (parameterAnnotations.length == 0) {
for (String value : cmd.value()) {
if (buffer.startsWith(value) || value.startsWith(buffer)) {
- results.add(new Completion(value)); // no space at the end, as there's no need to continue the command further
+ results.add(new Completion(value)); // no space at the end, as there's no need to continue the
+ // command further
}
}
candidates.addAll(results);
@@ -606,7 +618,8 @@ public class SimpleParser implements Parser {
}
}
- // To get this far, we know there are arguments required for this CliCommand, and they specified a valid command name
+ // To get this far, we know there are arguments required for this CliCommand, and they specified a valid
+ // command name
// Record all the CliOptions applicable to this command
List cliOptions = new ArrayList();
@@ -675,16 +688,19 @@ public class SimpleParser implements Parser {
return 0;
}
- // Handle suggesting an option key if they haven't got one presently specified (or they've completed a full option key/value pair)
+ // Handle suggesting an option key if they haven't got one presently specified (or they've completed a full
+ // option key/value pair)
if (lastOptionKey == null
|| (!"".equals(lastOptionKey) && !"".equals(lastOptionValue) && translated.endsWith(" "))) {
// We have either NEVER specified an option key/value pair
// OR we have specified a full option key/value pair
- // Let's list some other options the user might want to try (naturally skip the "" option, as that's the default)
+ // Let's list some other options the user might want to try (naturally skip the "" option, as that's the
+ // default)
for (CliOption include : unspecified) {
for (String value : include.key()) {
- // Manually determine if this non-mandatory but unspecifiedDefaultValue=* requiring option is able to be bound
+ // Manually determine if this non-mandatory but unspecifiedDefaultValue=* requiring option is
+ // able to be bound
if (!include.mandatory() && "*".equals(include.unspecifiedDefaultValue()) && !"".equals(value)) {
try {
for (Converter> candidate : converters) {
@@ -706,11 +722,13 @@ public class SimpleParser implements Parser {
if (paramType != null && candidate.supports(paramType, include.optionContext())) {
// Try to invoke this usable converter
candidate.convertFromText("*", paramType, include.optionContext());
- // If we got this far, the converter is happy with "*" so we need not bother the user with entering the data in themselves
+ // If we got this far, the converter is happy with "*" so we need not bother the
+ // user with entering the data in themselves
break;
}
}
- } catch (RuntimeException notYetReady) {
+ }
+ catch (RuntimeException notYetReady) {
if (translated.endsWith(" ")) {
results.add(new Completion(translated + "--" + value + " "));
}
@@ -728,7 +746,8 @@ public class SimpleParser implements Parser {
}
}
- // Only abort at this point if we have some suggestions; otherwise we might want to try to complete the "" option
+ // Only abort at this point if we have some suggestions; otherwise we might want to try to complete the
+ // "" option
if (results.size() > 0) {
candidates.addAll(results);
return 0;
@@ -737,8 +756,9 @@ public class SimpleParser implements Parser {
// Handle completing the option key they're presently typing
if ((lastOptionValue == null || "".equals(lastOptionValue)) && !translated.endsWith(" ")) {
- // Given we haven't got an option value of any form, and there's no space at the buffer end, we must still be typing an option key
- //System.out.println("completing an option");
+ // Given we haven't got an option value of any form, and there's no space at the buffer end, we must
+ // still be typing an option key
+ // System.out.println("completing an option");
for (CliOption option : cliOptions) {
for (String value : option.key()) {
if (value != null && lastOptionKey != null
@@ -782,7 +802,8 @@ public class SimpleParser implements Parser {
}
if (allValues.isEmpty()) {
- // Doesn't appear to be a custom Converter, so let's go and provide defaults for simple types
+ // Doesn't appear to be a custom Converter, so let's go and provide defaults for simple
+ // types
// Provide some simple options for common types
if (Boolean.class.isAssignableFrom(parameterType)
@@ -815,12 +836,12 @@ public class SimpleParser implements Parser {
// We only provide a suggestion if the lastOptionValue == ""
if (!StringUtils.hasText(lastOptionValue)) {
// We should add the result, as they haven't typed anything yet
- results.add(new Completion(prefix + currentValue.getValue() + suffix,
- currentValue.getFormattedValue(), currentValue.getHeading(),
- currentValue.getOrder()));
+ results.add(new Completion(prefix + currentValue.getValue() + suffix, currentValue
+ .getFormattedValue(), currentValue.getHeading(), currentValue.getOrder()));
}
else {
- // Only add the result **if** what they've typed is compatible *AND* they haven't already typed it in full
+ // Only add the result **if** what they've typed is compatible *AND* they haven't
+ // already typed it in full
if (currentValue.getValue().toLowerCase().startsWith(lastOptionValue.toLowerCase())
&& !lastOptionValue.equalsIgnoreCase(currentValue.getValue())
&& lastOptionValue.length() < currentValue.getValue().length()) {
@@ -831,7 +852,8 @@ public class SimpleParser implements Parser {
}
}
- // ROO-389: give inline options given there's multiple choices available and we want to help the user
+ // ROO-389: give inline options given there's multiple choices available and we want to help
+ // the user
StringBuilder help = new StringBuilder();
help.append(OsUtils.LINE_SEPARATOR);
help.append(option.mandatory() ? "required --" : "optional --");
@@ -852,13 +874,13 @@ public class SimpleParser implements Parser {
else {
if (!"".equals(option.specifiedDefaultValue())
&& !"__NULL__".equals(option.specifiedDefaultValue())) {
- help.append("; default if option present: '").append(option.specifiedDefaultValue()).append(
- "'");
+ help.append("; default if option present: '")
+ .append(option.specifiedDefaultValue()).append("'");
}
if (!"".equals(option.unspecifiedDefaultValue())
&& !"__NULL__".equals(option.unspecifiedDefaultValue())) {
- help.append("; default if option not present: '").append(
- option.unspecifiedDefaultValue()).append("'");
+ help.append("; default if option not present: '")
+ .append(option.unspecifiedDefaultValue()).append("'");
}
}
LOGGER.info(help.toString());
@@ -866,7 +888,8 @@ public class SimpleParser implements Parser {
if (results.size() == 1) {
String suggestion = results.iterator().next().getValue().trim();
if (suggestion.equals(lastOptionValue)) {
- // They have pressed TAB in the default value, and the default value has already been provided as an explicit option
+ // They have pressed TAB in the default value, and the default value has already
+ // been provided as an explicit option
return 0;
}
}
@@ -891,34 +914,32 @@ public class SimpleParser implements Parser {
/**
* populate completion for mandatory options
- *
+ *
* @param translated user's input
* @param unspecified unspecified options
* @param value the option key
* @param results completion list
*/
- private void handleMandatoryCompletion(String translated, List unspecified, String value, SortedSet results) {
+ private void handleMandatoryCompletion(String translated, List unspecified, String value,
+ SortedSet results) {
StringBuilder strBuilder = new StringBuilder(translated);
if (!translated.endsWith(" ")) {
strBuilder.append(" ");
}
// Plan change for SHL-20. But usability is bad.
/*
- List> mandatoryOptions = getMandatoryOptions(unspecified);
- for (List option : mandatoryOptions) {
- strBuilder.append("--");
- strBuilder.append(option.get(0));
- strBuilder.append(" ");
- }
- */
+ * List> mandatoryOptions = getMandatoryOptions(unspecified); for (List option :
+ * mandatoryOptions) { strBuilder.append("--"); strBuilder.append(option.get(0)); strBuilder.append(" "); }
+ */
strBuilder.append("--");
strBuilder.append(value);
strBuilder.append(" ");
results.add(new Completion(strBuilder.toString()));
}
-
- public void obtainHelp(@CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for") String buffer) {
+ public void obtainHelp(
+ @CliOption(key = { "", "command" }, optionContext = "availableCommands", help = "Command name to provide help for")
+ String buffer) {
synchronized (mutex) {
if (buffer == null) {
buffer = "";
@@ -955,18 +976,17 @@ public class SimpleParser implements Parser {
if ("".equals(key)) {
key = "** default **";
}
- sb.append(" Keyword: ").append(key).append(
- OsUtils.LINE_SEPARATOR);
+ sb.append(" Keyword: ").append(key).append(OsUtils.LINE_SEPARATOR);
}
- sb.append(" Help: ").append(cliOption.help()).append(
- OsUtils.LINE_SEPARATOR);
- sb.append(" Mandatory: ").append(cliOption.mandatory()).append(
- OsUtils.LINE_SEPARATOR);
- sb.append(" Default if specified: '").append(cliOption.specifiedDefaultValue()).append(
- "'").append(OsUtils.LINE_SEPARATOR);
- sb.append(" Default if unspecified: '").append(cliOption.unspecifiedDefaultValue()).append(
- "'").append(OsUtils.LINE_SEPARATOR);
+ sb.append(" Help: ").append(cliOption.help())
+ .append(OsUtils.LINE_SEPARATOR);
+ sb.append(" Mandatory: ").append(cliOption.mandatory())
+ .append(OsUtils.LINE_SEPARATOR);
+ sb.append(" Default if specified: '").append(cliOption.specifiedDefaultValue())
+ .append("'").append(OsUtils.LINE_SEPARATOR);
+ sb.append(" Default if unspecified: '").append(cliOption.unspecifiedDefaultValue())
+ .append("'").append(OsUtils.LINE_SEPARATOR);
sb.append(OsUtils.LINE_SEPARATOR);
}
@@ -998,8 +1018,8 @@ public class SimpleParser implements Parser {
}
LOGGER.info(sb.toString());
-// LOGGER.warning("** Type 'hint' (without the quotes) and hit ENTER for step-by-step guidance **"
-// + StringUtils.LINE_SEPARATOR);
+ // LOGGER.warning("** Type 'hint' (without the quotes) and hit ENTER for step-by-step guidance **"
+ // + StringUtils.LINE_SEPARATOR);
}
}
@@ -1042,7 +1062,7 @@ public class SimpleParser implements Parser {
}
}
}
-
+
public final Set getCommandMarkers() {
synchronized (mutex) {
return Collections.unmodifiableSet(commands);
@@ -1074,7 +1094,7 @@ public class SimpleParser implements Parser {
converters.remove(converter);
}
}
-
+
public final Set> getConverters() {
synchronized (mutex) {
return Collections.unmodifiableSet(converters);
diff --git a/src/main/java/org/springframework/shell/core/Tokenizer.java b/src/main/java/org/springframework/shell/core/Tokenizer.java
new file mode 100644
index 00000000..a7e0ffd8
--- /dev/null
+++ b/src/main/java/org/springframework/shell/core/Tokenizer.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.core;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Converts a particular buffer into a tokenized structure.
+ *
+ *
+ * Properly treats double quotes (") as option delimiters.
+ *
+ *
+ * Expects option names to be preceded by a double dash. We call this an "option marker".
+ *
+ *
+ * Treats spaces as the default option tokenizer.
+ *
+ *
+ * Any token without an option marker is considered the default. The default is returned in the Map as an element with
+ * an empty string key (""). There can only be a single default.
+ *
+ */
+public class Tokenizer {
+
+ private static final char ESCAPE_CHAR = '\\';
+
+ private final char[] buffer;
+
+ private int pos = 0;
+
+ private final Map result = new LinkedHashMap();
+
+ public Tokenizer(String text) {
+ this.buffer = text.toCharArray();
+ tokenize();
+ }
+
+ private void eatWhiteSpace() {
+ while (pos < buffer.length && buffer[pos] == ' ') {
+ pos++;
+ }
+ }
+
+ public void tokenize() {
+ while (pos < buffer.length) {
+ eatWhiteSpace();
+ if (pos < buffer.length) {
+ eatKeyValuePair();
+ }
+ }
+ }
+
+ public Map getTokens() {
+ return result;
+ }
+
+ /**
+ * Consume either {@code --key=value} or just {@code value}, eating extra spaces.
+ */
+ private void eatKeyValuePair() {
+ if (buffer[pos] == '-' && pos + 1 < buffer.length && buffer[pos + 1] == '-') {
+ pos += 2;
+ eatKeyEqualsValue();
+ }
+ else {
+ String value = eatValue();
+ store("", value);
+ }
+
+ }
+
+ private void store(String key, String value) {
+ if (result.put(key, value) != null) {
+ throw new IllegalArgumentException("You cannot specify option '" + key
+ + "' more than once in a single command");
+ }
+ }
+
+ private String eatValue() {
+ StringBuilder sb = new StringBuilder();
+ char endDelimiter = ' ';
+ if (buffer[pos] == '"') {
+ endDelimiter = '"';
+ pos++;
+ }
+ while (pos < buffer.length && buffer[pos] != endDelimiter) {
+ if (buffer[pos] == ESCAPE_CHAR && pos + 1 < buffer.length && buffer[pos + 1] == endDelimiter) {
+ sb.append(endDelimiter);
+ pos += 2;
+ continue;
+ }
+ sb.append(buffer[pos]);
+ pos++;
+ }
+ // When here, we either ran out of input, or encountered our delim, or both
+ if (endDelimiter == '"' && pos == buffer.length && buffer[pos - 1] != '"') {
+ throw new IllegalArgumentException("Cannot have an unbalanced number of quotation marks");
+ }
+ // Eat our delim
+ pos++;
+ return sb.toString();
+ }
+
+ private void eatKeyEqualsValue() {
+ String key = eatKey();
+ eatWhiteSpace();
+ String value = eatValue();
+ store(key, value);
+ }
+
+ private String eatKey() {
+ int start = pos;
+ while (pos < buffer.length && buffer[pos] != ' ') {
+ pos++;
+ }
+ return new String(buffer, start, pos - start);
+ }
+}
diff --git a/src/test/java/org/springframework/shell/core/TokenizerTests.java b/src/test/java/org/springframework/shell/core/TokenizerTests.java
new file mode 100644
index 00000000..2878d429
--- /dev/null
+++ b/src/test/java/org/springframework/shell/core/TokenizerTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.shell.core;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.assertEquals;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+/**
+ * Tests for Tokenizer.
+ *
+ * @author Eric Bottard
+ */
+public class TokenizerTests {
+
+ @Test
+ public void testEmpty() {
+ Map result = tokenize("");
+ assertEquals(emptyMap(), result);
+ }
+
+ @Test
+ public void testBlank() {
+ Map result = tokenize(" ");
+ assertEquals(emptyMap(), result);
+ }
+
+ @Test
+ public void testDefaultKey() {
+ Map result = tokenize("foo");
+ assertEquals(singletonMap("", "foo"), result);
+ }
+
+ @Test
+ public void testOneOption() {
+ Map result = tokenize("--foo bar");
+ assertEquals(singletonMap("foo", "bar"), result);
+ }
+
+ @Test
+ public void testTwoOptions() {
+ Map result = tokenize("--foo bar --fizz buzz");
+ Map expected = new HashMap();
+ expected.put("foo", "bar");
+ expected.put("fizz", "buzz");
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testTwoOptionsOneWithDefault() {
+ Map result = tokenize("bar --fizz buzz");
+ Map expected = new HashMap();
+ expected.put("", "bar");
+ expected.put("fizz", "buzz");
+ assertEquals(expected, result);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testTwoOptionsSameKey() {
+ tokenize("--foo bar --foo buzz");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testTwoOptionsSameEmptyKey() {
+ tokenize("bar buzz");
+ }
+
+ @Test
+ public void testValueQuotation() {
+ Map result = tokenize("--foo \"bar fizz\"");
+ assertEquals(singletonMap("foo", "bar fizz"), result);
+ }
+
+ @Test
+ public void testExtraSpaces() {
+ Map result = tokenize(" --foo \"bar fizz\" --bozz bizzz \"the default\"");
+ Map expected = new HashMap();
+ expected.put("", "the default");
+ expected.put("foo", "bar fizz");
+ expected.put("bozz", "bizzz");
+ assertEquals(expected, result);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testValueQuotationUnbalanced() {
+ Map result = tokenize("--foo \"bar fizz");
+ assertEquals(singletonMap("foo", "bar fizz"), result);
+ }
+
+ @Test
+ public void testValueQuotationEscaped() {
+ Map result = tokenize("--foo \"bar \\\"fizz\"");
+ assertEquals(singletonMap("foo", "bar \"fizz"), result);
+ }
+
+ private Map tokenize(String what) {
+ return new Tokenizer(what).getTokens();
+ }
+}