diff --git a/core/src/main/java/cucumber/runtime/Runtime.java b/core/src/main/java/cucumber/runtime/Runtime.java index 1e6399c3ba..ed9f5dea4b 100644 --- a/core/src/main/java/cucumber/runtime/Runtime.java +++ b/core/src/main/java/cucumber/runtime/Runtime.java @@ -98,7 +98,7 @@ public Runtime(ResourceLoader resourceLoader, ClassLoader classLoader, Collectio this.resourceLoader = resourceLoader; this.classLoader = classLoader; this.runtimeOptions = runtimeOptions; - Glue glue = optionalGlue != null ? optionalGlue : new RuntimeGlue(undefinedStepsTracker, new LocalizedXStreams(classLoader)); + Glue glue = optionalGlue != null ? optionalGlue : new RuntimeGlue(undefinedStepsTracker, new LocalizedXStreams(classLoader, runtimeOptions.getConverters())); this.stats = new Stats(runtimeOptions.isMonochrome()); this.bus = new EventBus(stopWatch); this.runner = new Runner(glue, bus, backends, runtimeOptions); diff --git a/core/src/main/java/cucumber/runtime/RuntimeOptions.java b/core/src/main/java/cucumber/runtime/RuntimeOptions.java index 44fa240f22..073759a793 100644 --- a/core/src/main/java/cucumber/runtime/RuntimeOptions.java +++ b/core/src/main/java/cucumber/runtime/RuntimeOptions.java @@ -7,6 +7,7 @@ import cucumber.api.formatter.Formatter; import cucumber.api.formatter.StrictAware; import cucumber.runner.EventBus; +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; import cucumber.runtime.formatter.PluginFactory; import cucumber.runtime.io.ResourceLoader; import cucumber.runtime.model.CucumberFeature; @@ -34,6 +35,7 @@ import static cucumber.util.FixJava.join; import static cucumber.util.FixJava.map; import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; // IMPORTANT! Make sure USAGE.txt is always uptodate if this class changes. public class RuntimeOptions { @@ -66,6 +68,7 @@ public String map(String keyword) { private final List junitOptions = new ArrayList(); private final PluginFactory pluginFactory; private final List plugins = new ArrayList(); + private final List converters = new ArrayList(); private boolean dryRun; private boolean strict = false; private boolean monochrome = false; @@ -211,6 +214,11 @@ private void parse(List args) { parsedPluginData.updatePluginSummaryPrinterNames(pluginSummaryPrinterNames); } + RuntimeOptions withConverters(List converters) { + this.converters.addAll(converters); + return this; + } + private void addLineFilters(Map> parsedLineFilters, String key, List lines) { if (parsedLineFilters.containsKey(key)) { parsedLineFilters.get(key).addAll(lines); @@ -328,6 +336,10 @@ public List getPlugins() { return plugins; } + List getConverters() { + return unmodifiableList(converters); + } + public Formatter formatter(ClassLoader classLoader) { return pluginProxy(classLoader, Formatter.class); } diff --git a/core/src/main/java/cucumber/runtime/RuntimeOptionsFactory.java b/core/src/main/java/cucumber/runtime/RuntimeOptionsFactory.java index e208fa14ba..3ad54ff268 100644 --- a/core/src/main/java/cucumber/runtime/RuntimeOptionsFactory.java +++ b/core/src/main/java/cucumber/runtime/RuntimeOptionsFactory.java @@ -1,6 +1,8 @@ package cucumber.runtime; import cucumber.api.CucumberOptions; +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverters; import cucumber.runtime.formatter.PluginFactory; import cucumber.runtime.io.MultiLoader; @@ -22,7 +24,9 @@ public RuntimeOptionsFactory(Class clazz) { public RuntimeOptions create() { List args = buildArgsFromOptions(); - return new RuntimeOptions(args); + List converters = buildConverters(); + return new RuntimeOptions(args) + .withConverters(converters); } private List buildArgsFromOptions() { @@ -141,6 +145,27 @@ private void addJunitOptions(CucumberOptions options, List args) { } } + private List buildConverters() { + List converters = new ArrayList(); + + for (Class classWithConverters = clazz; + hasSuperClass(classWithConverters); + classWithConverters = classWithConverters.getSuperclass() + ) { + XStreamConverters xstreamConverters = classWithConverters.getAnnotation(XStreamConverters.class); + if (xstreamConverters != null) { + Collections.addAll(converters, xstreamConverters.value()); + } + + XStreamConverter xstreamConverter = classWithConverters.getAnnotation(XStreamConverter.class); + if (xstreamConverter != null) { + converters.add(xstreamConverter); + } + } + + return converters; + } + static String packagePath(Class clazz) { return packagePath(packageName(clazz.getName())); } diff --git a/core/src/main/java/cucumber/runtime/xstream/LocalizedXStreams.java b/core/src/main/java/cucumber/runtime/xstream/LocalizedXStreams.java index e9d17dd87c..88e39301fd 100644 --- a/core/src/main/java/cucumber/runtime/xstream/LocalizedXStreams.java +++ b/core/src/main/java/cucumber/runtime/xstream/LocalizedXStreams.java @@ -1,15 +1,19 @@ package cucumber.runtime.xstream; import cucumber.deps.com.thoughtworks.xstream.XStream; +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; import cucumber.deps.com.thoughtworks.xstream.converters.Converter; import cucumber.deps.com.thoughtworks.xstream.converters.ConverterLookup; +import cucumber.deps.com.thoughtworks.xstream.converters.ConverterMatcher; import cucumber.deps.com.thoughtworks.xstream.converters.ConverterRegistry; import cucumber.deps.com.thoughtworks.xstream.converters.SingleValueConverter; import cucumber.deps.com.thoughtworks.xstream.core.DefaultConverterLookup; +import cucumber.runtime.CucumberException; import cucumber.runtime.ParameterInfo; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -17,16 +21,23 @@ public class LocalizedXStreams { private final Map xStreamsByLocale = new HashMap(); + private final List extraConverters; private final ClassLoader classLoader; - public LocalizedXStreams(ClassLoader classLoader) { + public LocalizedXStreams(ClassLoader classLoader, List extraConverters) { this.classLoader = classLoader; + this.extraConverters = extraConverters; + } + + public LocalizedXStreams(ClassLoader classLoader) { + this(classLoader, Collections.emptyList()); } public LocalizedXStream get(Locale locale) { LocalizedXStream xStream = xStreamsByLocale.get(locale); if (xStream == null) { xStream = newXStream(locale); + registerExtraConverters(xStream); xStreamsByLocale.put(locale, xStream); } return xStream; @@ -37,6 +48,21 @@ private LocalizedXStream newXStream(Locale locale) { return new LocalizedXStream(classLoader, lookup, lookup, locale); } + private void registerExtraConverters(LocalizedXStream xStream) { + for (XStreamConverter converter : extraConverters) { + try { + ConverterMatcher matcher = converter.value().newInstance(); + if (matcher instanceof Converter) { + xStream.registerConverter((Converter) matcher, converter.priority()); + } + } catch (InstantiationException e) { + throw new CucumberException(e.getMessage(), e); + } catch (IllegalAccessException e) { + throw new CucumberException(e.getMessage(), e); + } + } + } + public static class LocalizedXStream extends XStream { private final Locale locale; private final ThreadLocal> timeConverters = new ThreadLocal>() { diff --git a/core/src/test/java/cucumber/runtime/DummyConverter.java b/core/src/test/java/cucumber/runtime/DummyConverter.java new file mode 100644 index 0000000000..9a6a726525 --- /dev/null +++ b/core/src/test/java/cucumber/runtime/DummyConverter.java @@ -0,0 +1,26 @@ +package cucumber.runtime; + +import cucumber.deps.com.thoughtworks.xstream.converters.Converter; +import cucumber.deps.com.thoughtworks.xstream.converters.MarshallingContext; +import cucumber.deps.com.thoughtworks.xstream.converters.UnmarshallingContext; +import cucumber.deps.com.thoughtworks.xstream.io.HierarchicalStreamReader; +import cucumber.deps.com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +public class DummyConverter implements Converter { + + @Override + public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext ctx) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext ctx) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean canConvert(Class type) { + return false; + } + +} diff --git a/core/src/test/java/cucumber/runtime/RuntimeOptionsFactoryTest.java b/core/src/test/java/cucumber/runtime/RuntimeOptionsFactoryTest.java index e69353825c..b321025a20 100644 --- a/core/src/test/java/cucumber/runtime/RuntimeOptionsFactoryTest.java +++ b/core/src/test/java/cucumber/runtime/RuntimeOptionsFactoryTest.java @@ -3,6 +3,9 @@ import cucumber.api.CucumberOptions; import cucumber.api.SnippetType; import cucumber.runtime.io.ResourceLoader; +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverters; +import cucumber.deps.com.thoughtworks.xstream.converters.basic.LongConverter; import org.junit.Test; import java.util.Iterator; @@ -11,9 +14,11 @@ import static cucumber.runtime.RuntimeOptionsFactory.packageName; import static cucumber.runtime.RuntimeOptionsFactory.packagePath; +import cucumber.runtime.xstream.PatternConverter; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -134,6 +139,41 @@ public void create_with_junit_options() { assertEquals(asList("option1", "option2=value"), runtimeOptions.getJunitOptions()); } + @Test + public void create_with_xstream_converter() { + RuntimeOptionsFactory runtimeOptionsFactory = new RuntimeOptionsFactory(ClassWithConverter.class); + RuntimeOptions runtimeOptions = runtimeOptionsFactory.create(); + + List converters = runtimeOptions.getConverters(); + assertNotNull(converters); + assertEquals(1, converters.size()); + assertEquals(DummyConverter.class, converters.get(0).value()); + } + + @Test + public void create_with_xstream_converters() { + RuntimeOptionsFactory runtimeOptionsFactory = new RuntimeOptionsFactory(ClassWithConverters.class); + RuntimeOptions runtimeOptions = runtimeOptionsFactory.create(); + + List converters = runtimeOptions.getConverters(); + assertNotNull(converters); + assertEquals(2, converters.size()); + assertEquals(DummyConverter.class, converters.get(0).value()); + assertEquals(PatternConverter.class, converters.get(1).value()); + } + + @Test + public void create_with_xstream_converter_from_baseclass() { + RuntimeOptionsFactory runtimeOptionsFactory = new RuntimeOptionsFactory(SubclassWithConverter.class); + RuntimeOptions runtimeOptions = runtimeOptionsFactory.create(); + + List converters = runtimeOptions.getConverters(); + assertNotNull(converters); + assertEquals(2, converters.size()); + assertEquals(LongConverter.class, converters.get(0).value()); + assertEquals(DummyConverter.class, converters.get(1).value()); + } + private void assertPluginExists(List plugins, String pluginName) { boolean found = false; for (Object plugin : plugins) { @@ -211,4 +251,23 @@ static class ClassWithNoSummaryPrinterPlugin { static class ClassWithJunitOption { // empty } + + @XStreamConverter(DummyConverter.class) + static class ClassWithConverter { + // empty + } + + @XStreamConverters({ + @XStreamConverter(DummyConverter.class), + @XStreamConverter(PatternConverter.class) + }) + static class ClassWithConverters { + // empty + } + + @XStreamConverter(LongConverter.class) + static class SubclassWithConverter extends ClassWithConverter { + // empty + } + } diff --git a/core/src/test/java/cucumber/runtime/xstream/ExternalConverterTest.java b/core/src/test/java/cucumber/runtime/xstream/ExternalConverterTest.java new file mode 100644 index 0000000000..7c1fcbb232 --- /dev/null +++ b/core/src/test/java/cucumber/runtime/xstream/ExternalConverterTest.java @@ -0,0 +1,69 @@ +package cucumber.runtime.xstream; + +import cucumber.deps.com.thoughtworks.xstream.annotations.XStreamConverter; +import cucumber.deps.com.thoughtworks.xstream.converters.Converter; +import cucumber.deps.com.thoughtworks.xstream.converters.ConverterLookup; +import cucumber.deps.com.thoughtworks.xstream.converters.MarshallingContext; +import cucumber.deps.com.thoughtworks.xstream.converters.UnmarshallingContext; +import cucumber.deps.com.thoughtworks.xstream.io.HierarchicalStreamReader; +import cucumber.deps.com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +public class ExternalConverterTest { + + private List extraConverters; + private ClassLoader classLoader; + + @Before + public void setUp() throws Exception { + extraConverters = new ArrayList(); + classLoader = Thread.currentThread().getContextClassLoader(); + } + + @Test + public void shouldUseExtraConverter() { + extraConverters.add(Registration.class.getAnnotation(XStreamConverter.class)); + LocalizedXStreams transformers = new LocalizedXStreams(classLoader, extraConverters); + + ConverterLookup lookup = transformers.get(Locale.US).getConverterLookup(); + Converter c = lookup.lookupConverterForType(MyClass.class); + assertTrue(c instanceof AlwaysConverter); + } + + @XStreamConverter(AlwaysConverter.class) + public static class Registration { + } + + public static class AlwaysConverter implements Converter { + + @Override + public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext ctx) { + throw new UnsupportedOperationException("DUMMY MARSHAL"); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext ctx) { + throw new UnsupportedOperationException("DUMMY UNMARSHAL"); + } + + @Override + public boolean canConvert(Class type) { + return true; + } + + } + + public static class MyClass { + public final String s; + + public MyClass(String s) { + this.s = s; + } + } + +}