Skip to content
This repository

Extend url protocols #361

Merged
merged 7 commits into from almost 2 years ago

2 participants

Logan McGrath Aslak Hellesøy
Logan McGrath

Solution to #360: Unable to extend URL protocols handled by ClasspathIterable

I replaced the hard-coded dependencies on FileResourceIterator and ZipResourceIterator in the ClasspathIterable with a factory for loading resource iterators. Resource iterators are now created using ResourceIteratorFactory implementations and making these implementations available to the ClasspathIterable only requires a META-INF/services/cucumber.io.ResourceIteratorFactory file with the names of the implementing classes on each line.

The factory used by ClasspathIterable will try to find an appropriate factory to create resource iterators for each URL. If no appropriate factory is found, it will fall back to the ZipResourceIteratorFactory and then the FileResourceIteratorFactory. This means that existing behavior is not impacted by the changes.

There is a proof-of-concept test, cucumber.io.DelegatingResourceIteratorFactoryTest, which shows the META-INF/services/cucumber.io.ResourceIteratorFactory file in action. Other tests for fall-back capability with ZIP/File resource iterator factories would have been inconclusive because any test resources could exist either as a file or within a JAR depending on the test runtime environment.

Other changes:

  • I added URL-decoding to the ResourceLoaderTest class's dir field because I'm on Windows and spaces in my path were causing the test to fail
  • I added explicit generic argument to line 70 of cucumber.table.DataTable because I couldn't get the code to compile even with modifications to the compiler
Logan McGrath and others added some commits
Logan McGrath Because I don't want to have to tweak the crap outta my compiler to make
this work...
dcc204c
Logan McGrath * URL-decoding for test dir because this test will fail if you're on
Windows and your source code sits in a directory with spaces
d22a649
Logan McGrath style violation! 0155856
Logan McGrath Beginning work to make resource iterators extendable
* Raised visibility of ClasspathIterable.getPath(URL)
* Created ResourceIteratorFactory interface
* Factored FileResourceIterator creation into ResourceIteratorFactory
implementation
* Added services file for ResourceIteratorFactory
d886710
Logan McGrath Extensible at last!
* Factored resource iterators into separate factories
* There seems to be a classpath issue with the ZipResourceIterator,
because when it's created by a factory loaded using the ServiceLoader
class, it's unable to find some ZIP files (this was a problem in the
cucumber-jython tests) the solution was to create the
ZipThenFileResourceIteratorFallback class
a13365c
Logan McGrath Test for proof of concept
* Cosmetic tweaks
* Removed hard-coded dependency on ZipResourceIteratorFactory in
ClasspathIterable
271be9a
lmcgrath Merge branch 'master' into extend-url-protocols 4d2b84a
Aslak Hellesøy
Owner

I didn't know about the ServiceLoader class!! Maybe that can be used to load the various Backend implementations as well. I see only empty constructors are allowed, but I can live with some setters...

Anyway - nice patch. I'll take a closer look when I have more time.

Logan McGrath

I know right?? I didn't know about the ServiceLoader class until I started working with Arquillian several months ago. It loads all its extensions using it.

You can probably follow a similar pattern for the Backend implementations like what I did in the patch and just load factories for creating the back-ends themselves, that way you can get around the no-args constructor problem.

Aslak Hellesøy aslakhellesoy merged commit 4d2b84a into from
Aslak Hellesøy aslakhellesoy referenced this pull request
Merged

cucumber-android #525

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 7 unique commits by 2 authors.

Jul 17, 2012
Logan McGrath Because I don't want to have to tweak the crap outta my compiler to make
this work...
dcc204c
Logan McGrath * URL-decoding for test dir because this test will fail if you're on
Windows and your source code sits in a directory with spaces
d22a649
Logan McGrath style violation! 0155856
Logan McGrath Beginning work to make resource iterators extendable
* Raised visibility of ClasspathIterable.getPath(URL)
* Created ResourceIteratorFactory interface
* Factored FileResourceIterator creation into ResourceIteratorFactory
implementation
* Added services file for ResourceIteratorFactory
d886710
Logan McGrath Extensible at last!
* Factored resource iterators into separate factories
* There seems to be a classpath issue with the ZipResourceIterator,
because when it's created by a factory loaded using the ServiceLoader
class, it's unable to find some ZIP files (this was a problem in the
cucumber-jython tests) the solution was to create the
ZipThenFileResourceIteratorFallback class
a13365c
Logan McGrath Test for proof of concept
* Cosmetic tweaks
* Removed hard-coded dependency on ZipResourceIteratorFactory in
ClasspathIterable
271be9a
lmcgrath Merge branch 'master' into extend-url-protocols 4d2b84a
This page is out of date. Refresh to see the latest.
13 core/src/main/java/cucumber/io/ClasspathIterable.java
@@ -13,11 +13,13 @@
13 13
14 14 class ClasspathIterable implements Iterable<Resource> {
15 15 private final ClassLoader cl;
  16 + private final ResourceIteratorFactory resourceIteratorFactory;
16 17 private final String path;
17 18 private final String suffix;
18 19
19 20 public ClasspathIterable(ClassLoader cl, String path, String suffix) {
20 21 this.cl = cl;
  22 + this.resourceIteratorFactory = new DelegatingResourceIteratorFactory();
21 23 this.path = path;
22 24 this.suffix = suffix;
23 25 }
@@ -29,14 +31,7 @@ public ClasspathIterable(ClassLoader cl, String path, String suffix) {
29 31 Enumeration<URL> resources = cl.getResources(path);
30 32 while (resources.hasMoreElements()) {
31 33 URL url = resources.nextElement();
32   - if (url.getProtocol().equals("jar")) {
33   - String jarPath = filePath(url);
34   - iterator.push(new ZipResourceIterator(jarPath, path, suffix));
35   - } else {
36   - File file = new File(getPath(url));
37   - File rootDir = new File(file.getAbsolutePath().substring(0, file.getAbsolutePath().length() - path.length()));
38   - iterator.push(new FileResourceIterator(rootDir, file, suffix));
39   - }
  34 + iterator.push(this.resourceIteratorFactory.createIterator(url, path, suffix));
40 35 }
41 36 return iterator;
42 37 } catch (IOException e) {
@@ -54,7 +49,7 @@ static boolean hasSuffix(String suffix, String name) {
54 49 return suffix == null || name.endsWith(suffix);
55 50 }
56 51
57   - private static String getPath(URL url) {
  52 + static String getPath(URL url) {
58 53 try {
59 54 return URLDecoder.decode(url.getPath(), "UTF-8");
60 55 } catch (UnsupportedEncodingException e) {
69 core/src/main/java/cucumber/io/DelegatingResourceIteratorFactory.java
... ... @@ -0,0 +1,69 @@
  1 +package cucumber.io;
  2 +
  3 +import java.net.URL;
  4 +import java.util.Iterator;
  5 +import java.util.ServiceLoader;
  6 +
  7 +import cucumber.runtime.CucumberException;
  8 +
  9 +
  10 +/**
  11 + * A {@link ResourceIteratorFactory} implementation which delegates to
  12 + * factories found by the ServiceLoader class.
  13 + */
  14 +public class DelegatingResourceIteratorFactory implements ResourceIteratorFactory {
  15 +
  16 + /**
  17 + * The delegates.
  18 + */
  19 + private final Iterable<ResourceIteratorFactory> delegates;
  20 +
  21 + /**
  22 + * The fallback resource iterator factory.
  23 + */
  24 + private final ResourceIteratorFactory fallback;
  25 +
  26 + /**
  27 + * Initializes a new instance of the DelegatingResourceIteratorFactory
  28 + * class.
  29 + */
  30 + public DelegatingResourceIteratorFactory() {
  31 + this(new ZipThenFileResourceIteratorFallback());
  32 + }
  33 +
  34 + /**
  35 + * Initializes a new instance of the DelegatingResourceIteratorFactory
  36 + * class with a fallback factory.
  37 + *
  38 + * @param fallback The fallback resource iterator factory to use when an
  39 + * appropriate one couldn't be found otherwise.
  40 + */
  41 + public DelegatingResourceIteratorFactory(ResourceIteratorFactory fallback) {
  42 + delegates = ServiceLoader.load(ResourceIteratorFactory.class);
  43 + this.fallback = fallback;
  44 + }
  45 +
  46 + @Override
  47 + public boolean isFactoryFor(URL url) {
  48 + for (ResourceIteratorFactory delegate : delegates) {
  49 + if (delegate.isFactoryFor(url)) {
  50 + return true;
  51 + }
  52 + }
  53 + return fallback.isFactoryFor(url);
  54 + }
  55 +
  56 + @Override
  57 + public Iterator<Resource> createIterator(URL url, String path, String suffix) {
  58 + for (ResourceIteratorFactory delegate : delegates) {
  59 + if (delegate.isFactoryFor(url)) {
  60 + return delegate.createIterator(url, path, suffix);
  61 + }
  62 + }
  63 + if (fallback.isFactoryFor(url)) {
  64 + return fallback.createIterator(url, path, suffix);
  65 + } else {
  66 + throw new CucumberException("Fallback factory cannot handle URL: " + url);
  67 + }
  68 + }
  69 +}
39 core/src/main/java/cucumber/io/FileResourceIteratorFactory.java
... ... @@ -0,0 +1,39 @@
  1 +package cucumber.io;
  2 +
  3 +import static cucumber.io.ClasspathIterable.getPath;
  4 +
  5 +import java.io.File;
  6 +import java.net.URL;
  7 +import java.util.Iterator;
  8 +
  9 +/**
  10 + * Factory which creates {@link FileResourceIterator}s.
  11 + *
  12 + * <p>{@link FileResourceIterator}s should be created for any cases where a
  13 + * URL's protocol isn't otherwise handled. Thus, {@link #isFactoryFor(URL)}
  14 + * will always return <code>true</code>. Because of this behavior, the
  15 + * <code>FileResourceIteratorFactory</code> should never be registered as a
  16 + * service implementation for {@link ResourceIteratorFactory} as it could
  17 + * easily hide other service implementations.</p>
  18 + */
  19 +public class FileResourceIteratorFactory implements ResourceIteratorFactory {
  20 +
  21 + /**
  22 + * Initializes a new instance of the FileResourceIteratorFactory class.
  23 + */
  24 + public FileResourceIteratorFactory() {
  25 + // intentionally empty
  26 + }
  27 +
  28 + @Override
  29 + public boolean isFactoryFor(URL url) {
  30 + return true;
  31 + }
  32 +
  33 + @Override
  34 + public Iterator<Resource> createIterator(URL url, String path, String suffix) {
  35 + File file = new File(getPath(url));
  36 + File rootDir = new File(file.getAbsolutePath().substring(0, file.getAbsolutePath().length() - path.length()));
  37 + return new FileResourceIterator(rootDir, file, suffix);
  38 + }
  39 +}
30 core/src/main/java/cucumber/io/ResourceIteratorFactory.java
... ... @@ -0,0 +1,30 @@
  1 +package cucumber.io;
  2 +
  3 +import java.net.URL;
  4 +import java.util.Iterator;
  5 +
  6 +/**
  7 + * Factory contract for creating resource iterators.
  8 + */
  9 +public interface ResourceIteratorFactory {
  10 +
  11 + /**
  12 + * Gets a value indicating whether the factory can create iterators for the
  13 + * resource specified by the given URL.
  14 + *
  15 + * @param url The URL to check.
  16 + * @return True if the factory can create an iterator for the given URL.
  17 + */
  18 + boolean isFactoryFor(URL url);
  19 +
  20 + /**
  21 + * Creates an iterator for the given URL with the path and suffix.
  22 + *
  23 + * @param url The URL.
  24 + * @param path The path.
  25 + * @param suffix The suffix.
  26 + * @return The iterator over the resources designated by the URL, path, and
  27 + * suffix.
  28 + */
  29 + Iterator<Resource> createIterator(URL url, String path, String suffix);
  30 +}
38 core/src/main/java/cucumber/io/ZipResourceIteratorFactory.java
... ... @@ -0,0 +1,38 @@
  1 +package cucumber.io;
  2 +
  3 +import static cucumber.io.ClasspathIterable.filePath;
  4 +
  5 +import java.io.IOException;
  6 +import java.net.URL;
  7 +import java.util.Iterator;
  8 +
  9 +import cucumber.runtime.CucumberException;
  10 +
  11 +/**
  12 + * Factory which creates {@link ZipResourceIterator}s for URL's with the "jar"
  13 + * protocol.
  14 + */
  15 +public class ZipResourceIteratorFactory implements ResourceIteratorFactory {
  16 +
  17 + /**
  18 + * Initializes a new instance of the ZipResourceIteratorFactory class.
  19 + */
  20 + public ZipResourceIteratorFactory() {
  21 + // intentionally empty
  22 + }
  23 +
  24 + @Override
  25 + public boolean isFactoryFor(URL url) {
  26 + return "jar".equals(url.getProtocol());
  27 + }
  28 +
  29 + @Override
  30 + public Iterator<Resource> createIterator(URL url, String path, String suffix) {
  31 + try {
  32 + String jarPath = filePath(url);
  33 + return new ZipResourceIterator(jarPath, path, suffix);
  34 + } catch (IOException e) {
  35 + throw new CucumberException(e);
  36 + }
  37 + }
  38 +}
44 core/src/main/java/cucumber/io/ZipThenFileResourceIteratorFallback.java
... ... @@ -0,0 +1,44 @@
  1 +package cucumber.io;
  2 +
  3 +import java.net.URL;
  4 +import java.util.Iterator;
  5 +
  6 +/**
  7 + * Resource iterator factory implementation which acts as a fallback when no
  8 + * other factories are found.
  9 + */
  10 +public class ZipThenFileResourceIteratorFallback implements ResourceIteratorFactory {
  11 + /**
  12 + * The file resource iterator factory.
  13 + */
  14 + private final FileResourceIteratorFactory fileResourceIteratorFactory;
  15 +
  16 + /**
  17 + * The ZIP resource iterator factory.
  18 + */
  19 + private final ZipResourceIteratorFactory zipResourceIteratorFactory;
  20 +
  21 +
  22 + /**
  23 + * Initializes a new instance of the ZipThenFileResourceIteratorFallback
  24 + * class.
  25 + */
  26 + public ZipThenFileResourceIteratorFallback() {
  27 + fileResourceIteratorFactory = new FileResourceIteratorFactory();
  28 + zipResourceIteratorFactory = new ZipResourceIteratorFactory();
  29 + }
  30 +
  31 + @Override
  32 + public boolean isFactoryFor(URL url) {
  33 + return zipResourceIteratorFactory.isFactoryFor(url) || fileResourceIteratorFactory.isFactoryFor(url);
  34 + }
  35 +
  36 + @Override
  37 + public Iterator<Resource> createIterator(URL url, String path, String suffix) {
  38 + if (zipResourceIteratorFactory.isFactoryFor(url)) {
  39 + return zipResourceIteratorFactory.createIterator(url, path, suffix);
  40 + } else {
  41 + return fileResourceIteratorFactory.createIterator(url, path, suffix);
  42 + }
  43 + }
  44 +}
2  core/src/main/java/cucumber/table/DataTable.java
@@ -67,7 +67,7 @@ public DataTable(List<DataTableRow> gherkinRows, TableConverter tableConverter)
67 67 }
68 68
69 69 public <T> T convert(Type type) {
70   - return tableConverter.convert(type, this);
  70 + return tableConverter.<T>convert(type, this);
71 71 }
72 72
73 73 /**
27 core/src/test/java/cucumber/io/DelegatingResourceIteratorFactoryTest.java
... ... @@ -0,0 +1,27 @@
  1 +package cucumber.io;
  2 +
  3 +import static org.junit.Assert.assertTrue;
  4 +
  5 +import java.net.MalformedURLException;
  6 +import java.net.URL;
  7 +import java.util.Iterator;
  8 +
  9 +import org.junit.Test;
  10 +
  11 +import cucumber.io.TestResourceIteratorFactory.TestResourceIterator;
  12 +
  13 +
  14 +public class DelegatingResourceIteratorFactoryTest {
  15 +
  16 + @Test
  17 + public void should_load_test_resource_iterator() throws MalformedURLException {
  18 + ResourceIteratorFactory factory = new DelegatingResourceIteratorFactory();
  19 + URL url = new URL("file:///this/is/only/a/test");
  20 +
  21 + assertTrue(factory.isFactoryFor(url));
  22 +
  23 + Iterator<Resource> iterator = factory.createIterator(url, "test", "test");
  24 +
  25 + assertTrue(iterator instanceof TestResourceIterator);
  26 + }
  27 +}
8 core/src/test/java/cucumber/io/ResourceLoaderTest.java
@@ -4,13 +4,19 @@
4 4
5 5 import java.io.File;
6 6 import java.io.IOException;
  7 +import java.io.UnsupportedEncodingException;
  8 +import java.net.URLDecoder;
7 9 import java.util.ArrayList;
8 10 import java.util.List;
9 11
10 12 import static org.junit.Assert.assertEquals;
11 13
12 14 public class ResourceLoaderTest {
13   - private final File dir = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getFile());
  15 + private final File dir;
  16 +
  17 + public ResourceLoaderTest() throws UnsupportedEncodingException {
  18 + dir = new File(URLDecoder.decode(getClass().getProtectionDomain().getCodeSource().getLocation().getFile(), "UTF-8"));
  19 + }
14 20
15 21 @Test
16 22 public void loads_resources_from_filesystem_dir() {
43 core/src/test/java/cucumber/io/TestResourceIteratorFactory.java
... ... @@ -0,0 +1,43 @@
  1 +package cucumber.io;
  2 +
  3 +import java.net.URL;
  4 +import java.util.Iterator;
  5 +import java.util.NoSuchElementException;
  6 +
  7 +public class TestResourceIteratorFactory implements ResourceIteratorFactory
  8 +{
  9 + /**
  10 + * Initializes a new instance of the TestResourceIteratorFactory class.
  11 + */
  12 + public TestResourceIteratorFactory() {
  13 + // intentionally empty
  14 + }
  15 +
  16 + @Override
  17 + public boolean isFactoryFor(URL url) {
  18 + return "file".equals(url.getProtocol()) && url.getPath().endsWith("test");
  19 + }
  20 +
  21 + @Override
  22 + public Iterator<Resource> createIterator(URL url, String path, String suffix) {
  23 + return new TestResourceIterator();
  24 + }
  25 +
  26 + public static final class TestResourceIterator implements Iterator<Resource> {
  27 +
  28 + @Override
  29 + public boolean hasNext() {
  30 + return false;
  31 + }
  32 +
  33 + @Override
  34 + public Resource next() {
  35 + throw new NoSuchElementException();
  36 + }
  37 +
  38 + @Override
  39 + public void remove() {
  40 + throw new UnsupportedOperationException();
  41 + }
  42 + }
  43 +}
1  core/src/test/resources/META-INF/services/cucumber.io.ResourceIteratorFactory
... ... @@ -0,0 +1 @@
  1 +cucumber.io.TestResourceIteratorFactory

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.