diff --git a/.gitignore b/.gitignore index 6b851028..149242e9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ target/ /*.ipr /*.iws out +.gradletasknamecache diff --git a/gradle.properties b/gradle.properties index b3f7c4ba..f437f5a3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,11 @@ +version=1.2.0.BUILD-SNAPSHOT + slf4jVersion=1.7.7 guavaVersion=17.0 -jlineVersion=2.11 +jlineVersion=2.12 junitVersion=4.11 springVersion=4.0.6.RELEASE commonsioVersion=2.4 hamcrestVersion=1.3 -version=1.2.0.BUILD-SNAPSHOT mockitoVersion=1.9.5 hamcrestDateVersion=0.9.3 \ No newline at end of file diff --git a/src/main/java/org/springframework/shell/converters/ArrayConverter.java b/src/main/java/org/springframework/shell/converters/ArrayConverter.java new file mode 100644 index 00000000..b3398bfe --- /dev/null +++ b/src/main/java/org/springframework/shell/converters/ArrayConverter.java @@ -0,0 +1,121 @@ +package org.springframework.shell.converters; + +import java.io.File; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.shell.core.Completion; +import org.springframework.shell.core.Converter; +import org.springframework.shell.core.MethodTarget; +import org.springframework.stereotype.Component; + +/** + * A converter that knows how to use other converters to create arrays of supported types. + * + * @author Eric Bottard + */ +@Component +public class ArrayConverter implements Converter{ + + private Set> converters; + + @Autowired + public void setConverters(Set> converters) { + this.converters = converters; + } + + @Override + public boolean supports(Class type, String optionContext) { + return findComponentConverter(type, optionContext) != null && !optionContext.contains("disable-array-converter"); + + } + + private Converter findComponentConverter(Class targetType, String optionContext) { + if (!targetType.isArray()) { + return null; + } + Class componentType = targetType.getComponentType(); + for (Converter converter : converters) { + if (converter.supports(componentType, optionContext)) { + return converter; + } + } + return null; + } + + @Override + public Object[] convertFromText(String value, Class targetType, String optionContext) { + Class componentType = targetType.getComponentType(); + + String splittingRegex = inferSplittingRegex(targetType, optionContext); + String[] splits = value.split(splittingRegex); + Object[] result = (Object[]) Array.newInstance(componentType, splits.length); + Converter converter = findComponentConverter(targetType, optionContext); + + for (int i = 0; i < splits.length; i++) { + result[i] = converter.convertFromText(splits[i], componentType, optionContext); + } + return result; + } + + /** + * Return a regex used to split the string representation of items. + *

The default delimiter is a comma, unless we're dealing with Files, in which case + * {@link java.io.File.pathSeparator} is used.

+ *

Delimiters can be protected by an escape character, which is '\' by default.

+ *

Command methods may override bot the delimiter and the escape through the {@code splittingRegex} option context + * string.

+ */ + private String inferSplittingRegex(Class targetType, String optionContext) { + String regex = extract(optionContext, "splittingRegex"); + if (regex == null) { + // Default for files is to use system separator with no way to escape + if (File[].class.isAssignableFrom(targetType)) { + regex = File.pathSeparator; + } else { + String delimiter = ","; + String escape = "\\"; + regex = String.format("(? completions, Class targetType, String existingData, String optionContext, MethodTarget target) { + Class componentType = targetType.getComponentType(); + + String splittingRegex = inferSplittingRegex(targetType, optionContext); + String[] splits = existingData.split(splittingRegex); + Converter converter = findComponentConverter(targetType, optionContext); + + // Search for completions with the last part only, prefixing the results by everything that was + // before the delimiter + String last = splits[splits.length - 1]; + int end = existingData.lastIndexOf(last); + String prefix = existingData.substring(0, end); + List ours = new ArrayList(); + + // Passing our method target below, as we can't do better. Obviously, method sig will be wrong + boolean result = converter.getAllPossibleValues(ours, componentType, last, optionContext, target); + for (Completion completion : ours) { + completions.add(new Completion(prefix + completion.getValue(), completion.getValue(), null, 0)); + } + + return result; + } + + private String extract(String optionContext, String key) { + String[] splits = optionContext.split(" "); + String prefix = key + "="; + for (String split : splits) { + if (split.startsWith(prefix)) { + return split.substring(prefix.length()); + } + } + return null; + } +} diff --git a/src/test/java/org/springframework/shell/BootstrapTest.java b/src/test/java/org/springframework/shell/BootstrapTest.java index 38e58a8c..8d96a634 100644 --- a/src/test/java/org/springframework/shell/BootstrapTest.java +++ b/src/test/java/org/springframework/shell/BootstrapTest.java @@ -3,7 +3,7 @@ package org.springframework.shell; import java.io.IOException; import java.util.logging.Logger; -import junit.framework.Assert; +import org.junit.Assert; import org.junit.Test; import org.springframework.shell.core.JLineShellComponent; @@ -19,7 +19,7 @@ public class BootstrapTest { //This is a brittle assertion - as additiona 'test' commands are added to the suite, this number will increase. Assert.assertEquals("Number of CommandMarkers is incorrect", 10, shell.getSimpleParser().getCommandMarkers().size()); - Assert.assertEquals("Number of Converters is incorrect", 16, shell.getSimpleParser().getConverters().size()); + Assert.assertEquals("Number of Converters is incorrect", 17, shell.getSimpleParser().getConverters().size()); } catch (RuntimeException t) { throw t; } finally { diff --git a/src/test/java/org/springframework/shell/converters/ArrayConverterTest.java b/src/test/java/org/springframework/shell/converters/ArrayConverterTest.java new file mode 100644 index 00000000..26f64847 --- /dev/null +++ b/src/test/java/org/springframework/shell/converters/ArrayConverterTest.java @@ -0,0 +1,119 @@ +package org.springframework.shell.converters; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hamcrest.Description; +import org.hamcrest.DiagnosingMatcher; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.shell.core.Completion; +import org.springframework.shell.core.Converter; + +/** + * Tests for ArrayConverter. + * + * @author Eric Bottard + */ +public class ArrayConverterTest { + + private ArrayConverter arrayConverter; + + @Before + public void setUp() throws Exception { + FileConverter fileConverter = new FileConverter() { + + @Override + protected File getWorkingDirectory() { + return new File("."); + } + }; + arrayConverter = new ArrayConverter(); + Set> allConverters = new HashSet>(); + allConverters.add(fileConverter); + allConverters.add(arrayConverter); + allConverters.add(new IntegerConverter()); + arrayConverter.setConverters(allConverters); + } + + @Test + public void testInferredFileSeparator() throws IOException { + + String raw = "src/main" + File.pathSeparator + "src/main/java"; + File main = new File("src/main").getCanonicalFile(); + File src = new File("src/main/java").getCanonicalFile(); + + + assertThat(arrayConverter.supports(File[].class, ""), equalTo(true)); + File[] result = (File[]) arrayConverter.convertFromText(raw, File[].class, ""); + assertThat(result, arrayContaining(main, src)); + + } + + @Test + public void testDefaultDelimiter() { + assertThat(arrayConverter.supports(Integer[].class, ""), equalTo(true)); + Integer[] result = (Integer[]) arrayConverter.convertFromText("1,2,3,4,5", Integer[].class, ""); + assertThat(result, arrayContaining(1, 2, 3, 4, 5)); + } + + @Test + public void testOverriddenDelimiter() { + assertThat(arrayConverter.supports(Integer[].class, "splittingRegex=;"), equalTo(true)); + Integer[] result = (Integer[]) arrayConverter.convertFromText("1;2;3;4;5", Integer[].class, "splittingRegex=;"); + assertThat(result, arrayContaining(1, 2, 3, 4, 5)); + } + + @Test + public void testUnsupportedType() { + assertThat(arrayConverter.supports(Integer.class, ""), equalTo(false)); + assertThat(arrayConverter.supports(Float[].class, ""), equalTo(false)); + } + + @Test + public void testOptOut() { + assertThat(arrayConverter.supports(Integer[].class, "disable-array-converter"), equalTo(false)); + } + + @Test + @SuppressWarnings("unchecked") + public void testCompletions() throws IOException { + String raw = "src/test/java/" + File.pathSeparator + "src/main/"; + + + List completions = new ArrayList(); + arrayConverter.getAllPossibleValues(completions, File[].class, raw, "", null); + assertThat(completions, containsInAnyOrder( + completionWhoseValue(equalTo("src/test/java/" + File.pathSeparator + "src/main/java/")), + completionWhoseValue(equalTo("src/test/java/" + File.pathSeparator +" src/main/resources/")))); + + } + + private Matcher completionWhoseValue(final Matcher matcher) { + return new DiagnosingMatcher() { + + @Override + public void describeTo(Description description) { + description.appendText("a completion that ").appendDescriptionOf(matcher); + } + + @Override + protected boolean matches(Object item, Description mismatchDescription) { + Completion completion = (Completion) item; + boolean match = matcher.matches(completion.getValue()); + matcher.describeMismatch(completion.getValue(), mismatchDescription); + return match; + } + }; + } + +} \ No newline at end of file