SHL-173: Add converter for array of convertible

This commit is contained in:
Eric Bottard
2015-03-03 18:23:35 +01:00
committed by Mark Pollack
parent 511a7e230b
commit 4c18cfb8f4
5 changed files with 246 additions and 4 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ target/
/*.ipr
/*.iws
out
.gradletasknamecache

View File

@@ -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

View File

@@ -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<Object[]>{
private Set<Converter<?>> converters;
@Autowired
public void setConverters(Set<Converter<?>> 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.
* <p>The default delimiter is a comma, unless we're dealing with Files, in which case
* {@link java.io.File.pathSeparator} is used.</p>
* <p>Delimiters can be protected by an escape character, which is '\' by default.</p>
* <p>Command methods may override bot the delimiter and the escape through the {@code splittingRegex} option context
* string.</p>
*/
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("(?<!\\Q%s\\E)\\Q%s\\E", escape, delimiter);
}
}
return regex;
}
@Override
public boolean getAllPossibleValues(List<Completion> 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<Completion> ours = new ArrayList<Completion>();
// 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;
}
}

View File

@@ -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 {

View File

@@ -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<Converter<?>> allConverters = new HashSet<Converter<?>>();
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<Completion> completions = new ArrayList<Completion>();
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<Completion> completionWhoseValue(final Matcher<String> matcher) {
return new DiagnosingMatcher<Completion>() {
@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;
}
};
}
}