JSIEVE-103 introduce ArgumentParser utility
diff --git a/core/pom.xml b/core/pom.xml
index d5c5329..b2114a8 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -80,6 +80,17 @@
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>${assertj.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/core/src/main/java/org/apache/jsieve/utils/ArgumentParser.java b/core/src/main/java/org/apache/jsieve/utils/ArgumentParser.java
new file mode 100644
index 0000000..5d47369
--- /dev/null
+++ b/core/src/main/java/org/apache/jsieve/utils/ArgumentParser.java
@@ -0,0 +1,193 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you 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.apache.jsieve.utils;
+
+import org.apache.jsieve.Argument;
+import org.apache.jsieve.NumberArgument;
+import org.apache.jsieve.StringListArgument;
+import org.apache.jsieve.TagArgument;
+import org.apache.jsieve.exception.SyntaxException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ArgumentParser {
+
+    private List<Argument> remainingArguments;
+    private Set<String> singleTags;
+    private Map<String, Argument> tagsWithValues;
+
+    public ArgumentParser(List<Argument> arguments) {
+        this.remainingArguments = new ArrayList<Argument>();
+        this.singleTags = new HashSet<String>();
+        this.tagsWithValues = new HashMap<String, Argument>();
+        initialize(arguments);
+    }
+
+    public void initialize(List<Argument> arguments) {
+        TagArgument previousSeenTagArgument = null;
+        for (Argument argument : arguments) {
+            if (argument instanceof TagArgument) {
+                TagArgument tagArgument = (TagArgument) argument;
+                handlePreviousTagArgument(previousSeenTagArgument);
+                previousSeenTagArgument = tagArgument;
+            } else {
+                handleOtherArguments(previousSeenTagArgument, argument);
+                previousSeenTagArgument = null;
+            }
+        }
+        if (previousSeenTagArgument != null) {
+            singleTags.add(previousSeenTagArgument.getTag());
+        }
+    }
+
+    private void handlePreviousTagArgument(TagArgument previousSeenTagArgument) {
+        if (previousSeenTagArgument != null) {
+            singleTags.add(previousSeenTagArgument.getTag());
+        }
+    }
+
+    private void handleOtherArguments(TagArgument lastSeenTagArgument, Argument argument) {
+        if (lastSeenTagArgument == null) {
+            remainingArguments.add(argument);
+        } else {
+            tagsWithValues.put(lastSeenTagArgument.getTag(), argument);
+        }
+    }
+
+    public String getStringValueForTag(String tag, String exceptionMessage) throws SyntaxException {
+        Argument argument = retrieveArgumentIfExists(tag, exceptionMessage);
+        if (argument == null) {
+            return null;
+        }
+        return retrieveSingleStringValue(argument, exceptionMessage);
+    }
+
+    public Integer getNumericValueForTag(String tag, String exceptionMessage) throws SyntaxException {
+        Argument argument = retrieveArgumentIfExists(tag, exceptionMessage);
+        if (argument == null) {
+            return null;
+        }
+        return retrieveNumericValue(argument, exceptionMessage);
+    }
+
+    public List<String> getStringListForTag(String tag, String exceptionMessage) throws SyntaxException {
+        Argument argument = retrieveArgumentIfExists(tag, exceptionMessage);
+        if (argument == null) {
+            return new ArrayList<String>();
+        }
+        return retrieveStringValues(argument, exceptionMessage);
+    }
+
+    public Set<String> getSingleTags() {
+        return new HashSet<String>(singleTags);
+    }
+
+    public String getRemainingStringValue(String exceptionMessage) throws SyntaxException {
+        if (remainingArguments.size() > 1) {
+            throw new SyntaxException(exceptionMessage);
+        }
+        if (remainingArguments.size() == 0) {
+            return null;
+        }
+        return retrieveSingleStringValue(remainingArguments.get(0), exceptionMessage);
+    }
+
+    public void throwOnUnvalidSeenSingleTag(String... validTags) throws SyntaxException {
+        validateTagCollectionWithExpectations(singleTags, validTags);
+    }
+
+    public void throwOnUnvalidSeenTagWithValue(String... validTags) throws SyntaxException {
+        validateTagCollectionWithExpectations(tagsWithValues.keySet(), validTags);
+    }
+
+    private void validateTagCollectionWithExpectations(Set<String> seenTags, String[] expectations) throws SyntaxException {
+        Set<String> validTagList = getSetFromStringArray(expectations);
+        if (!validTagList.containsAll(seenTags)) {
+            throw new SyntaxException(buildUnwantedTagsErrorMessage(retrieveUnwantedTags(seenTags, validTagList)));
+        }
+    }
+
+    private Set<String> getSetFromStringArray(String[] validTags) {
+        Set<String> validTagList = new HashSet<String>();
+        for(String validTag : validTags) {
+            validTagList.add(validTag);
+        }
+        return validTagList;
+    }
+
+    private Set<String> retrieveUnwantedTags(Set<String> seenTags, Set<String> validTagList) {
+        Set<String> unwantedTags = new HashSet<String>(seenTags);
+        unwantedTags.removeAll(validTagList);
+        return unwantedTags;
+    }
+
+    private String buildUnwantedTagsErrorMessage(Set<String> unwantedTags) {
+        String errorMessage;
+        StringBuilder errorMessageBuilder = new StringBuilder().append("Unexpected tags : [");
+        for (String unwantedTag : unwantedTags) {
+            errorMessageBuilder.append("\"").append(unwantedTag).append("\"");
+        }
+        errorMessageBuilder.append("]");
+        errorMessage = errorMessageBuilder.toString();
+        return errorMessage;
+    }
+
+    private Argument retrieveArgumentIfExists(String tag, String exceptionMessage) throws SyntaxException {
+        Argument argument = tagsWithValues.get(tag);
+        if (argument == null) {
+            if (singleTags.contains(tag)) {
+                throw new SyntaxException(exceptionMessage);
+            } else {
+                return null;
+            }
+        }
+        return argument;
+    }
+
+    private List<String> retrieveStringValues(Argument argument, String exceptionMessage) throws SyntaxException {
+        if (! (argument instanceof StringListArgument)) {
+            throw new SyntaxException(exceptionMessage);
+        }
+        StringListArgument stringListArgument = (StringListArgument) argument;
+        return stringListArgument.getList();
+    }
+
+    private String retrieveSingleStringValue(Argument argument, String exceptionMessage) throws SyntaxException {
+        List<String> stringsValue = retrieveStringValues(argument, exceptionMessage);
+        if (stringsValue.size() != 1) {
+            throw new SyntaxException(exceptionMessage);
+        }
+        return stringsValue.get(0);
+    }
+
+    private Integer retrieveNumericValue(Argument argument, String exceptionMessage) throws SyntaxException {
+        if (! (argument instanceof NumberArgument)) {
+            throw new SyntaxException(exceptionMessage);
+        }
+        NumberArgument numberArgument = (NumberArgument) argument;
+        return numberArgument.getInteger();
+    }
+
+}
diff --git a/core/src/test/java/org/apache/jsieve/utils/ArgumentParserTest.java b/core/src/test/java/org/apache/jsieve/utils/ArgumentParserTest.java
new file mode 100644
index 0000000..59fc69d
--- /dev/null
+++ b/core/src/test/java/org/apache/jsieve/utils/ArgumentParserTest.java
@@ -0,0 +1,248 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you 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.apache.jsieve.utils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.collect.Lists;
+import org.apache.jsieve.Argument;
+import org.apache.jsieve.NumberArgument;
+import org.apache.jsieve.StringListArgument;
+import org.apache.jsieve.TagArgument;
+import org.apache.jsieve.exception.SyntaxException;
+import org.apache.jsieve.parser.generated.Token;
+import org.junit.Test;
+
+public class ArgumentParserTest {
+    public static final int KIND = 18;
+    public static final int NUMERIC_VALUE = 42;
+    public static final String ANY_TAG = ":any";
+    public static final String OTHER_TAG = ":other";
+    public static final String EXCEPTION_MESSAGE = "ExceptionMessage";
+    public static final String STRING_VALUE = "string";
+    public static final String STRING_VALUE_2 = "string2";
+    public static final TagArgument ANY_TAG_ARGUMENT = new TagArgument(new Token(KIND, ANY_TAG));
+    public static final TagArgument OTHER_TAG_ARGUMENT = new TagArgument(new Token(KIND, OTHER_TAG));
+    public static final NumberArgument NUMBER_ARGUMENT = new NumberArgument(new Token(KIND, Integer.toString(NUMERIC_VALUE)));
+    public static final StringListArgument SINGLE_STRING_ARGUMENT = new StringListArgument(Lists.newArrayList(STRING_VALUE));
+    public static final StringListArgument STRING_LIST_ARGUMENT = new StringListArgument(Lists.newArrayList(STRING_VALUE, STRING_VALUE_2));
+
+    @Test
+    public void getRemainingStringValueShouldReturnNullByDefault() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        assertThat(argumentParser.getRemainingStringValue(EXCEPTION_MESSAGE)).isNull();
+    }
+
+    @Test
+    public void getNumericValueForTagShouldReturnNullByDefault() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        assertThat(argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isNull();
+    }
+
+    @Test
+    public void getStringListForTagShouldReturnEmptyByDefault() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        assertThat(argumentParser.getStringListForTag(ANY_TAG, EXCEPTION_MESSAGE)).isEmpty();
+    }
+
+
+    @Test
+    public void getStringValueForTagShouldReturnNullByDefault() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        assertThat(argumentParser.getStringValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isNull();
+    }
+
+    @Test
+    public void validateSingleTagsShouldNotThrowByDefault() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        argumentParser.throwOnUnvalidSeenSingleTag();
+    }
+
+    @Test
+    public void validateTagsWithValueShouldNotThrowByDefault() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        argumentParser.throwOnUnvalidSeenTagWithValue();
+    }
+
+    @Test
+    public void validateSingleTagsShouldNotThrowWhenAllowedValuesAreNotPresent() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        argumentParser.throwOnUnvalidSeenSingleTag(ANY_TAG);
+    }
+
+    @Test
+    public void validateTagsWithValueShouldNotThrowWhenAllowedValuesAreNotPresent() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList());
+        argumentParser.throwOnUnvalidSeenTagWithValue(ANY_TAG);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void validateSingleTagsShouldThrowOnUnexpectedSingleTag() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(ANY_TAG_ARGUMENT));
+        argumentParser.throwOnUnvalidSeenSingleTag();
+    }
+
+    @Test
+    public void validateTagsWithValueShouldNotThrowOnSingleTag() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(ANY_TAG_ARGUMENT));
+        argumentParser.throwOnUnvalidSeenTagWithValue();
+    }
+
+    @Test
+    public void validateSingleTagsShouldNotThrowOnExpectedSingleTag() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(ANY_TAG_ARGUMENT));
+        argumentParser.throwOnUnvalidSeenSingleTag(ANY_TAG);
+    }
+
+    @Test
+    public void validateSingleTagsShouldNotThrowOnTagWithValue() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(OTHER_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        argumentParser.throwOnUnvalidSeenSingleTag(ANY_TAG);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void validateTagsWithValueShouldThrowOnUnexpectedTagWithValue() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(OTHER_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        argumentParser.throwOnUnvalidSeenTagWithValue(ANY_TAG);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void validateSingleTagsShouldThrowOnUnexpectedValue() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(OTHER_TAG_ARGUMENT));
+        argumentParser.throwOnUnvalidSeenSingleTag(ANY_TAG);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getRemainingStringValueShouldThrowOnWrongArgumentType() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(NUMBER_ARGUMENT));
+        argumentParser.getRemainingStringValue(EXCEPTION_MESSAGE);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getRemainingStringValueShouldThrowOnMultipleElementsInStringList() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(STRING_LIST_ARGUMENT));
+        argumentParser.getRemainingStringValue(EXCEPTION_MESSAGE);
+    }
+
+    @Test
+    public void getRemainingStringValueShouldWork() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(SINGLE_STRING_ARGUMENT));
+        assertThat(argumentParser.getRemainingStringValue(EXCEPTION_MESSAGE)).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void getRemainingStringValueShouldReturnNullWhenCalledWithOnlyTagWithValue() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, STRING_LIST_ARGUMENT));
+        assertThat(argumentParser.getRemainingStringValue(EXCEPTION_MESSAGE)).isNull();
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getNumericValueForTagShouldThrowOnSingleStringArgument() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, SINGLE_STRING_ARGUMENT));
+        argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE);
+    }
+
+    @Test
+    public void getStringListForTagShouldWorkWithSingleValue() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, SINGLE_STRING_ARGUMENT));
+        assertThat(argumentParser.getStringListForTag(ANY_TAG, EXCEPTION_MESSAGE)).containsExactly(STRING_VALUE);
+    }
+
+    @Test
+    public void getStringValueForTagShouldWork() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, SINGLE_STRING_ARGUMENT));
+        assertThat(argumentParser.getStringValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void getNumericValueForTagShouldWork() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        assertThat(argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isEqualTo(NUMERIC_VALUE);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getStringListForTagShouldThrowWhenUsedWithNumberArgument() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        argumentParser.getStringListForTag(ANY_TAG, EXCEPTION_MESSAGE);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getStringValueForTagShouldThrowWhenUsedWithNumberArgument() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        argumentParser.getStringValueForTag(ANY_TAG, EXCEPTION_MESSAGE);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getNumericValueForTagShouldThrowWhenUsedWithStringListArgument() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, STRING_LIST_ARGUMENT));
+        argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE);
+    }
+
+    @Test
+    public void getStringListForTagShouldWork() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, STRING_LIST_ARGUMENT));
+        assertThat(argumentParser.getStringListForTag(ANY_TAG, EXCEPTION_MESSAGE)).containsOnly(STRING_VALUE, STRING_VALUE_2);
+    }
+
+    @Test(expected = SyntaxException.class)
+    public void getStringValueForTagShouldThrowWhenUsedWithMultipleValues() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, STRING_LIST_ARGUMENT));
+        argumentParser.getStringValueForTag(ANY_TAG, EXCEPTION_MESSAGE);
+    }
+
+    @Test
+    public void getNumericValueForTagShouldReturnNullWhenUsedOnAnOtherTag() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(OTHER_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        assertThat(argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isNull();
+    }
+
+    @Test
+    public void getStringListForTagShouldReturnNullWhenUsedOnAnOtherTag() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(OTHER_TAG_ARGUMENT, STRING_LIST_ARGUMENT));
+        assertThat(argumentParser.getStringListForTag(ANY_TAG, EXCEPTION_MESSAGE)).isEmpty();
+    }
+
+    @Test
+    public void getStringValueForTagShouldReturnNullWhenUsedOnAnOtherTag() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(OTHER_TAG_ARGUMENT, SINGLE_STRING_ARGUMENT));
+        assertThat(argumentParser.getStringValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isNull();
+    }
+
+    @Test
+    public void argumentParserShouldHandleMultipleTagsWithValue() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(OTHER_TAG_ARGUMENT, SINGLE_STRING_ARGUMENT, ANY_TAG_ARGUMENT, NUMBER_ARGUMENT));
+        assertThat(argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isEqualTo(NUMERIC_VALUE);
+        assertThat(argumentParser.getStringValueForTag(OTHER_TAG, EXCEPTION_MESSAGE));
+    }
+
+    @Test
+    public void argumentParserShouldHandleTagWithValueAndRemainingString() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.newArrayList(ANY_TAG_ARGUMENT, NUMBER_ARGUMENT, SINGLE_STRING_ARGUMENT));
+        assertThat(argumentParser.getNumericValueForTag(ANY_TAG, EXCEPTION_MESSAGE)).isEqualTo(NUMERIC_VALUE);
+        assertThat(argumentParser.getRemainingStringValue(EXCEPTION_MESSAGE)).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void argumentParserShouldHandleMultipleSingleTagArguments() throws Exception {
+        ArgumentParser argumentParser = new ArgumentParser(Lists.<Argument>newArrayList(ANY_TAG_ARGUMENT, OTHER_TAG_ARGUMENT));
+        assertThat(argumentParser.getSingleTags()).containsExactly(ANY_TAG, OTHER_TAG);
+    }
+
+}