Repository: tng/archunit
Issue: Can't handle spaces in dependencies

We use ArchUnit 0.21.0 on an OSGi system. To load our java classes we just point to a rootdir, pretty much like this:

```java
private static JavaClasses getJavaClasses() {
	return new ClassFileImporter()
			.withImportOption(DO_NOT_INCLUDE_ARCHIVES)
			.importPath("rootPath");
}
```

OSGi being OSGi, the code above will only find the class files for our product. The dependencies reside in different directories such as `~/.m2/` or `$workspace/.metadata.plugins/`. When loading a dependency class the OSGi classloader will know to check such directories.

The stacktrace we're encountering is the following:

```
ClassResolverFromClasspath.tryGetUriOf(String) line: 62
ClassResolverFromClasspath.tryResolve(String) line: 49
ImportedClasses.getOrResolve(String) line: 57
ImportedClasses.ensurePresent(String) line: 69
ClassGraphCreator.ensureMemberTypesArePresent() line: 117
ClassGraphCreator.complete() line: 106
ClassFileProcessor.process(ClassFileSource) line: 63
ClassFileImporter.importLocations(Collection<Location>) line: 336
ClassFileImporter.importPaths(Collection<Path>) line: 158
ClassFileImporter.importPaths(String...) line: 133
ClassFileImporter.importPath(String) line: 112
BaseArchUnitTest.getJavaClasses() line: 56
```

Line 56 in `BaseArchUnitTest` being the `.importPath("rootPath");` in the code above.

For most of our `typeName`s in `ImportedClasses.getOrResolve(String)`, the javaClass != null and thus we have no need to resolve it. However, for dependencies such as OSGi's `Version.class` (org.osgi.framework.Version.class) we need to go look into the directories mentioned above.

This results in `resource` in `ClassResolverFromClasspath.tryGetUriOf(String)` being of the form `C:/Users/David Name/sigasi-development-project-master/ws/.metadata/.plugins/org.eclipse.pde.core/.bundle_pool/plugins/org.eclipse.osgi_3.16.300.v20210525-1715.jar!/org/osgi/framework/Version.class`. Note the space in the user name.

When we then try to do `resource.toURI()`, we get the error below.

Conclusion: The URL received from the classloader should first be encoded, and only then transformed into a URI


```
java.net.URISyntaxException: Illegal character in opaque part at index 24: jar:file:/C:/Users/David Name/sigasi-development-project-master/ws/.metadata/.plugins/org.eclipse.pde.core/.bundle_pool/plugins/org.eclipse.osgi_3.16.300.v20210525-1715.jar!/org/osgi/framework/Version.class
```

Changed files:
archunit/src/main/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClasspath.java
archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java
archunit/src/test/java/com/tngtech/archunit/ArchConfigurationTest.java
archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTestUtils.java
archunit/src/test/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClassPathTest.java
archunit/src/test/java/com/tngtech/archunit/lang/extension/TestServicesFile.java
archunit/src/test/java/com/tngtech/archunit/core/importer/LocationTest.java
archunit/src/test/java/com/tngtech/archunit/testutil/OutsideOfClassPathRule.java
archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterSlowTest.java
archunit/src/test/java/com/tngtech/archunit/testutil/TestUtils.java
archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java
archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java
archunit/src/test/java/com/tngtech/archunit/core/domain/SourceTest.java

diff --git a/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java b/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java
index a8e402bc..1b2f6d86 100644
--- a/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java
+++ b/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java
@@ -5,7 +5,6 @@ import java.io.FileReader;
 import java.lang.module.ModuleFinder;
 import java.lang.module.ModuleReference;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.stream.Stream;
@@ -13,15 +12,15 @@ import java.util.stream.StreamSupport;
 
 import org.junit.Test;
 
-import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
 import static com.tngtech.archunit.core.importer.ClassFileSourceTest.MODULE_INFO_FILE_NAME;
+import static com.tngtech.archunit.testutil.TestUtils.uriOf;
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class ModuleLocationFactoryTest {
     private ModuleLocationFactory locationFactory = new ModuleLocationFactory();
 
     @Test
-    public void reads_single_entry_of_jrt() throws URISyntaxException {
+    public void reads_single_entry_of_jrt() {
         URI jrtJavaIoFile = uriOf(File.class);
         Location jrtJavaIo = locationFactory.create(jrtJavaIoFile);
 
@@ -30,7 +29,7 @@ public class ModuleLocationFactoryTest {
     }
 
     @Test
-    public void iterates_package_of_jrt() throws URISyntaxException {
+    public void iterates_package_of_jrt() {
         URI jrtJavaIoFile = uriOf(File.class);
         Location jrtJavaIo = locationFactory.create(parentOf(jrtJavaIoFile));
 
@@ -39,7 +38,7 @@ public class ModuleLocationFactoryTest {
     }
 
     @Test
-    public void iterates_entire_jrt() throws URISyntaxException {
+    public void iterates_entire_jrt() {
         Location jrtContainingFile = locationFactory.create(createModuleUriContaining(File.class));
 
         assertThat(jrtContainingFile.iterateEntries())
@@ -47,7 +46,7 @@ public class ModuleLocationFactoryTest {
     }
 
     @Test
-    public void respects_import_options() throws URISyntaxException {
+    public void respects_import_options() {
         Location jrtContainingFile = locationFactory.create(createModuleUriContaining(File.class));
 
         ClassFileSource fileSource = jrtContainingFile.asClassFileSource(new ImportOptions()
@@ -76,7 +75,7 @@ public class ModuleLocationFactoryTest {
     }
 
     @SuppressWarnings("SameParameterValue")
-    private URI createModuleUriContaining(Class<?> clazz) throws URISyntaxException {
+    private URI createModuleUriContaining(Class<?> clazz) {
         URI someJrt = uriOf(clazz);
         String moduleUri = someJrt.toString().replaceAll("(jrt:/[^/]+).*", "$1");
         return URI.create(moduleUri);
@@ -85,8 +84,4 @@ public class ModuleLocationFactoryTest {
     private URI parentOf(URI uri) {
         return URI.create(uri.toString().replaceAll("/[^/]+$", ""));
     }
-
-    private URI uriOf(Class<?> clazz) throws URISyntaxException {
-        return urlOf(clazz).toURI();
-    }
 }
diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClasspath.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClasspath.java
index a2f60ed1..3cf36df6 100644
--- a/archunit/src/main/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClasspath.java
+++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClasspath.java
@@ -57,9 +57,19 @@ public final class ClassResolverFromClasspath implements ClassResolver {
             return Optional.empty();
         }
         try {
-            return Optional.of(resource.toURI());
+            return Optional.of(toUri(resource));
         } catch (URISyntaxException e) {
             throw new ArchUnitException.LocationException(e);
         }
     }
+
+    private URI toUri(URL resource) throws URISyntaxException {
+        try {
+            return resource.toURI();
+        } catch (URISyntaxException e) {
+            // In case the ClassLoader returns a URL with unencoded characters (e.g. spaces), this will correctly encode them.
+            // This was added to solve https://github.com/TNG/ArchUnit/issues/683 where some OSGI ClassLoader would not encode spaces correctly.
+            return new URI(resource.getProtocol(), resource.getHost(), resource.getPath(), null);
+        }
+    }
 }
diff --git a/archunit/src/test/java/com/tngtech/archunit/ArchConfigurationTest.java b/archunit/src/test/java/com/tngtech/archunit/ArchConfigurationTest.java
index 3ace2f8c..81d785ca 100644
--- a/archunit/src/test/java/com/tngtech/archunit/ArchConfigurationTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/ArchConfigurationTest.java
@@ -17,13 +17,12 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static com.tngtech.archunit.testutil.Assertions.assertThat;
 import static com.tngtech.archunit.testutil.ReflectionTestUtils.constructor;
-import static com.tngtech.archunit.testutil.TestUtils.properties;
-import static com.tngtech.archunit.testutil.TestUtils.singleProperty;
+import static com.tngtech.archunit.testutil.TestUtils.*;
 import static org.assertj.core.api.Assertions.entry;
 
 public class ArchConfigurationTest {
     private static final String PROPERTIES_FILE_NAME = "archconfigtest.properties";
-    private final File testPropsFile = new File(getClass().getResource("/").getFile(), PROPERTIES_FILE_NAME);
+    private final File testPropsFile = new File(new File(toUri(getClass().getResource("/"))), PROPERTIES_FILE_NAME);
 
     @Rule
     public final ExpectedException thrown = ExpectedException.none();
diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/SourceTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/SourceTest.java
index aca2e267..cadc9c93 100644
--- a/archunit/src/test/java/com/tngtech/archunit/core/domain/SourceTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/SourceTest.java
@@ -24,8 +24,9 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.tngtech.archunit.testutil.Assertions.assertThat;
+import static com.tngtech.archunit.testutil.TestUtils.uriOf;
+import static com.tngtech.archunit.testutil.TestUtils.urlOf;
 import static com.tngtech.java.junit.dataprovider.DataProviders.$;
 import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -38,11 +39,11 @@ public class SourceTest {
     }
 
     @Test
-    public void source_file_name() throws URISyntaxException {
-        Source source = new Source(urlOf(Object.class).toURI(), Optional.of("SomeClass.java"), false);
+    public void source_file_name() {
+        Source source = new Source(uriOf(Object.class), Optional.of("SomeClass.java"), false);
         assertThat(source.getFileName()).as("source file name").contains("SomeClass.java");
 
-        source = new Source(urlOf(Object.class).toURI(), Optional.<String>empty(), false);
+        source = new Source(uriOf(Object.class), Optional.<String>empty(), false);
         assertThat(source.getFileName()).as("source file name").isAbsent();
     }
 
@@ -150,10 +151,10 @@ public class SourceTest {
 
     @Test
     public void disables_md5_calculation_via_parameter() throws Exception {
-        Source source = new Source(urlOf(getClass()).toURI(), Optional.of("any.java"), false);
+        Source source = new Source(uriOf(getClass()), Optional.of("any.java"), false);
         assertThat(source.getMd5sum()).isEqualTo(Md5sum.DISABLED);
 
-        source = new Source(urlOf(getClass()).toURI(), Optional.of("any.java"), true);
+        source = new Source(uriOf(getClass()), Optional.of("any.java"), true);
         assertThat(source.getMd5sum().asBytes()).isEqualTo(expectedMd5BytesAt(source.getUri().toURL()));
     }
 
@@ -185,9 +186,4 @@ public class SourceTest {
     private static Object jarUrl() {
         return urlOf(Rule.class);
     }
-
-    public static URL urlOf(Class<?> clazz) {
-        return checkNotNull(clazz.getResource("/" + clazz.getName().replace('.', '/') + ".class"),
-                "Can't determine url of %s", clazz.getName());
-    }
 }
diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterSlowTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterSlowTest.java
index 9685e8e8..f22fcd3a 100644
--- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterSlowTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterSlowTest.java
@@ -1,7 +1,6 @@
 package com.tngtech.archunit.core.importer;
 
 import java.io.File;
-import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.util.List;
@@ -21,13 +20,13 @@ import org.junit.Test;
 import org.junit.experimental.categories.Category;
 import org.junit.rules.TemporaryFolder;
 
-import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
 import static com.tngtech.archunit.core.importer.ClassFileImporterTestUtils.jarFileOf;
 import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS;
 import static com.tngtech.archunit.core.importer.UrlSourceTest.JAVA_CLASS_PATH_PROP;
 import static com.tngtech.archunit.testutil.Assertions.assertThat;
 import static com.tngtech.archunit.testutil.Assertions.assertThatType;
 import static com.tngtech.archunit.testutil.Assertions.assertThatTypes;
+import static com.tngtech.archunit.testutil.TestUtils.urlOf;
 import static java.util.jar.Attributes.Name.CLASS_PATH;
 
 @Category(Slow.class)
@@ -99,7 +98,7 @@ public class ClassFileImporterSlowTest {
     }
 
     @Test
-    public void imports_duplicate_classes() throws IOException {
+    public void imports_duplicate_classes() {
         String existingClass = urlOf(JavaClass.class).getFile();
         copyRule.copy(
                 new File(existingClass),
diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java
index 71a9cec6..7c23c171 100644
--- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTest.java
@@ -6,7 +6,6 @@ import java.io.PrintStream;
 import java.io.Serializable;
 import java.lang.annotation.Annotation;
 import java.net.MalformedURLException;
-import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -87,7 +86,6 @@ import static com.tngtech.archunit.core.domain.JavaModifier.BRIDGE;
 import static com.tngtech.archunit.core.domain.JavaModifier.STATIC;
 import static com.tngtech.archunit.core.domain.JavaModifier.SYNTHETIC;
 import static com.tngtech.archunit.core.domain.SourceTest.bytesAt;
-import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
 import static com.tngtech.archunit.core.domain.TestUtils.MD5_SUM_DISABLED;
 import static com.tngtech.archunit.core.domain.TestUtils.md5sumOf;
 import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf;
@@ -100,6 +98,8 @@ import static com.tngtech.archunit.testutil.Assertions.assertThatTypes;
 import static com.tngtech.archunit.testutil.ReflectionTestUtils.constructor;
 import static com.tngtech.archunit.testutil.ReflectionTestUtils.field;
 import static com.tngtech.archunit.testutil.ReflectionTestUtils.method;
+import static com.tngtech.archunit.testutil.TestUtils.uriOf;
+import static com.tngtech.archunit.testutil.TestUtils.urlOf;
 import static com.tngtech.java.junit.dataprovider.DataProviders.$;
 import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
 import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach;
@@ -654,7 +654,7 @@ public class ClassFileImporterTest {
 
     @Test
     public void imports_urls_of_folders() throws Exception {
-        File testexamplesFolder = new File(new File(urlOf(getClass()).toURI()).getParentFile(), "testexamples");
+        File testexamplesFolder = new File(new File(uriOf(getClass())).getParentFile(), "testexamples");
 
         JavaClasses javaClasses = new ClassFileImporter().importUrl(testexamplesFolder.toURI().toURL());
 
@@ -797,19 +797,19 @@ public class ClassFileImporterTest {
 
         JavaClass clazzFromFile = new ClassFileImporter().importClass(ClassToImportOne.class);
         Source source = clazzFromFile.getSource().get();
-        assertThat(source.getUri()).isEqualTo(urlOf(ClassToImportOne.class).toURI());
+        assertThat(source.getUri()).isEqualTo(uriOf(ClassToImportOne.class));
         assertThat(source.getFileName()).contains(ClassToImportOne.class.getSimpleName() + ".java");
         assertThat(source.getMd5sum()).isEqualTo(md5sumOf(bytesAt(urlOf(ClassToImportOne.class))));
 
         clazzFromFile = new ClassFileImporter().importClass(ClassWithInnerClass.Inner.class);
         source = clazzFromFile.getSource().get();
-        assertThat(source.getUri()).isEqualTo(urlOf(ClassWithInnerClass.Inner.class).toURI());
+        assertThat(source.getUri()).isEqualTo(uriOf(ClassWithInnerClass.Inner.class));
         assertThat(source.getFileName()).contains(ClassWithInnerClass.class.getSimpleName() + ".java");
         assertThat(source.getMd5sum()).isEqualTo(md5sumOf(bytesAt(urlOf(ClassWithInnerClass.Inner.class))));
 
         JavaClass clazzFromJar = new ClassFileImporter().importClass(Rule.class);
         source = clazzFromJar.getSource().get();
-        assertThat(source.getUri()).isEqualTo(urlOf(Rule.class).toURI());
+        assertThat(source.getUri()).isEqualTo(uriOf(Rule.class));
         assertThat(source.getFileName()).contains(Rule.class.getSimpleName() + ".java");
         assertThat(source.getMd5sum()).isEqualTo(md5sumOf(bytesAt(urlOf(Rule.class))));
 
@@ -850,8 +850,8 @@ public class ClassFileImporterTest {
     }
 
     @Test
-    public void imports_paths() throws Exception {
-        File exampleFolder = new File(new File(urlOf(getClass()).toURI()).getParentFile(), "testexamples");
+    public void imports_paths() {
+        File exampleFolder = new File(new File(uriOf(getClass())).getParentFile(), "testexamples");
         File folderOne = new File(exampleFolder, "pathone");
         File folderTwo = new File(exampleFolder, "pathtwo");
 
@@ -876,7 +876,7 @@ public class ClassFileImporterTest {
     public void ImportOptions_are_respected() throws Exception {
         ClassFileImporter importer = new ClassFileImporter().withImportOption(importOnly(getClass(), Rule.class));
 
-        assertThatTypes(importer.importPath(Paths.get(urlOf(getClass()).toURI()))).matchExactly(getClass());
+        assertThatTypes(importer.importPath(Paths.get(uriOf(getClass())))).matchExactly(getClass());
         assertThatTypes(importer.importUrl(urlOf(getClass()))).matchExactly(getClass());
         assertThatTypes(importer.importJar(jarFileOf(Rule.class))).matchExactly(Rule.class);
     }
@@ -898,8 +898,8 @@ public class ClassFileImporterTest {
         assertThat(classes.get(clazz.getName())).hasSimpleName(clazz.getSimpleName());
     }
 
-    private void copyClassFile(Class<?> clazz, File targetFolder) throws IOException, URISyntaxException {
-        Files.copy(Paths.get(urlOf(clazz).toURI()), new File(targetFolder, clazz.getSimpleName() + ".class").toPath());
+    private void copyClassFile(Class<?> clazz, File targetFolder) throws IOException {
+        Files.copy(Paths.get(uriOf(clazz)), new File(targetFolder, clazz.getSimpleName() + ".class").toPath());
     }
 
     private ImportOption importOnly(final Class<?>... classes) {
diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTestUtils.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTestUtils.java
index 2e57347f..9f2b1e7b 100644
--- a/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTestUtils.java
+++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterTestUtils.java
@@ -16,7 +16,7 @@ import com.tngtech.archunit.core.domain.properties.HasName;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Iterables.getFirst;
-import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
+import static com.tngtech.archunit.testutil.TestUtils.urlOf;
 
 class ClassFileImporterTestUtils {
 
diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/LocationTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/LocationTest.java
index 8f901042..095fcff4 100644
--- a/archunit/src/test/java/com/tngtech/archunit/core/importer/LocationTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/LocationTest.java
@@ -1,5 +1,18 @@
 package com.tngtech.archunit.core.importer;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.tngtech.archunit.ArchConfiguration;
+import com.tngtech.archunit.base.DescribedPredicate;
+import com.tngtech.archunit.core.InitialConfigurationTest;
+import com.tngtech.archunit.core.importer.resolvers.ClassResolverFactoryTest;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.assertj.core.api.Condition;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -15,19 +28,6 @@ import java.util.Set;
 import java.util.jar.JarFile;
 import java.util.regex.Pattern;
 
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.tngtech.archunit.ArchConfiguration;
-import com.tngtech.archunit.base.DescribedPredicate;
-import com.tngtech.archunit.core.InitialConfigurationTest;
-import com.tngtech.archunit.core.importer.resolvers.ClassResolverFactoryTest;
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import org.assertj.core.api.Condition;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 import static com.google.common.base.Functions.toStringFunction;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.Iterables.transform;
@@ -283,7 +283,7 @@ public class LocationTest {
     @Test
     public void location_of_path() throws URISyntaxException {
         URL url = getClass().getResource(".");
-        assertThat(Location.of(Paths.get(url.toURI())).asURI().getPath()).isEqualTo(url.getFile());
+        assertThat(Location.of(Paths.get(url.toURI())).asURI()).isEqualTo(url.toURI());
     }
 
     @DataProvider
diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClassPathTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClassPathTest.java
index 681eda80..89e41636 100644
--- a/archunit/src/test/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClassPathTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/resolvers/ClassResolverFromClassPathTest.java
@@ -1,22 +1,37 @@
 package com.tngtech.archunit.core.importer.resolvers;
 
+import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URL;
 
+import com.tngtech.archunit.base.Function;
 import com.tngtech.archunit.base.Optional;
 import com.tngtech.archunit.core.domain.JavaClass;
 import com.tngtech.archunit.core.importer.resolvers.ClassResolver.ClassUriImporter;
+import com.tngtech.archunit.testutil.TestUtils;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
 import static com.tngtech.archunit.core.domain.TestUtils.importClassWithContext;
 import static com.tngtech.archunit.testutil.Assertions.assertThat;
+import static com.tngtech.java.junit.dataprovider.DataProviders.$;
+import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+@RunWith(DataProviderRunner.class)
 public class ClassResolverFromClassPathTest {
     @Rule
     public final MockitoRule mockitoRule = MockitoJUnit.rule();
@@ -24,14 +39,17 @@ public class ClassResolverFromClassPathTest {
     @Mock
     private ClassUriImporter uriImporter;
 
-    private ClassResolverFromClasspath resolver = new ClassResolverFromClasspath();
+    private final ClassResolverFromClasspath resolver = new ClassResolverFromClasspath();
+
+    @Before
+    public void setUp() {
+        resolver.setClassUriImporter(uriImporter);
+    }
 
     @Test
-    public void finds_uri_of_class_on_classpath() throws URISyntaxException {
+    public void finds_uri_of_class_on_classpath() {
         JavaClass expectedJavaClass = importClassWithContext(Object.class);
-        when(uriImporter.tryImport(uriOf(Object.class))).thenReturn(Optional.of(expectedJavaClass));
-
-        resolver.setClassUriImporter(uriImporter);
+        when(uriImporter.tryImport(TestUtils.uriOf(Object.class))).thenReturn(Optional.of(expectedJavaClass));
 
         Optional<JavaClass> result = resolver.tryResolve(Object.class.getName());
 
@@ -39,16 +57,62 @@ public class ClassResolverFromClassPathTest {
     }
 
     @Test
-    public void is_resilient_if_URI_cant_be_located() throws URISyntaxException {
-        resolver.setClassUriImporter(uriImporter);
-
+    public void is_resilient_if_URI_cant_be_located() {
         Optional<JavaClass> result = resolver.tryResolve("sooo.Wrong");
 
         assertThat(result).isAbsent();
         verifyNoMoreInteractions(uriImporter);
     }
 
-    private URI uriOf(Class<?> clazz) throws URISyntaxException {
-        return getClass().getResource("/" + clazz.getName().replace('.', '/') + ".class").toURI();
+    @DataProvider
+    public static Object[][] urls_with_spaces() throws MalformedURLException, URISyntaxException {
+        return $$(
+                $(new URL("file:/C:/Some Windows/URL with spaces 123/any.jar"), new URI("file:/C:/Some%20Windows/URL%20with%20spaces%20123/any.jar")),
+                $(new URL("file:/Some Unix/URL with spaces 123/any.jar"), new URI("file:/Some%20Unix/URL%20with%20spaces%20123/any.jar"))
+        );
+    }
+
+    @Test
+    @UseDataProvider("urls_with_spaces")
+    public void is_resilient_against_wrongly_encoded_ClassLoader_resource_URLs(final URL urlReturnedByClassLoader, URI expectedUriDerivedFromUrl) {
+        // it seems like some OSGI ClassLoaders incorrectly return URLs with unencoded spaces.
+        // This lead to `url.toURI()` throwing an exception -> https://github.com/TNG/ArchUnit/issues/683
+        verifyUrlCannotBeConvertedToUriInTheCurrentForm(urlReturnedByClassLoader);
+
+        final JavaClass expectedJavaClass = importClassWithContext(Object.class);
+        when(uriImporter.tryImport(expectedUriDerivedFromUrl)).thenReturn(Optional.of(expectedJavaClass));
+
+        Optional<JavaClass> resolvedClass = withMockedContextClassLoader(new Function<ClassLoader, Optional<JavaClass>>() {
+            @Override
+            public Optional<JavaClass> apply(ClassLoader classLoaderMock) {
+                String typeNameFromUrlWithSpaces = "some.TypeFromUrlWithSpaces";
+                String typeResourceFromUrlWithSpaces = typeNameFromUrlWithSpaces.replace(".", "/") + ".class";
+                when(classLoaderMock.getResource(typeResourceFromUrlWithSpaces)).thenReturn(urlReturnedByClassLoader);
+
+                return resolver.tryResolve(typeNameFromUrlWithSpaces);
+            }
+        });
+
+        assertThat(resolvedClass).contains(expectedJavaClass);
+    }
+
+    private <T> T withMockedContextClassLoader(Function<ClassLoader, T> doWithClassLoader) {
+        ClassLoader classLoaderMock = mock(ClassLoader.class);
+        ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
+        try {
+            Thread.currentThread().setContextClassLoader(classLoaderMock);
+            return doWithClassLoader.apply(classLoaderMock);
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalContextClassLoader);
+        }
+    }
+
+    private void verifyUrlCannotBeConvertedToUriInTheCurrentForm(final URL url) {
+        assertThatThrownBy(new ThrowingCallable() {
+            @Override
+            public void call() throws Throwable {
+                url.toURI();
+            }
+        }).isInstanceOf(URISyntaxException.class);
     }
 }
\\ No newline at end of file
diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java b/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java
index 410bdd3f..b2ef9506 100644
--- a/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java
+++ b/archunit/src/test/java/com/tngtech/archunit/lang/ArchRuleTest.java
@@ -1,12 +1,5 @@
 package com.tngtech.archunit.lang;
 
-import java.io.File;
-import java.io.IOException;
-import java.util.Iterator;
-import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.io.Files;
@@ -23,6 +16,13 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
 import static com.google.common.collect.Lists.newArrayList;
 import static com.tngtech.archunit.core.domain.TestUtils.importClasses;
 import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext;
@@ -30,6 +30,7 @@ import static com.tngtech.archunit.lang.ArchRule.Assertions.ARCHUNIT_IGNORE_PATT
 import static com.tngtech.archunit.lang.Priority.HIGH;
 import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.all;
 import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
+import static com.tngtech.archunit.testutil.TestUtils.toUri;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -178,7 +179,7 @@ public class ArchRuleTest {
     }
 
     private File ignoreFile() {
-        return new File(new File(getClass().getResource("/").getFile()), ARCHUNIT_IGNORE_PATTERNS_FILE_NAME);
+        return new File(new File(toUri(getClass().getResource("/"))), ARCHUNIT_IGNORE_PATTERNS_FILE_NAME);
     }
 
     private void expectAssertionErrorWithMessages(final String... messages) {
diff --git a/archunit/src/test/java/com/tngtech/archunit/lang/extension/TestServicesFile.java b/archunit/src/test/java/com/tngtech/archunit/lang/extension/TestServicesFile.java
index 3bac1548..df6fec66 100644
--- a/archunit/src/test/java/com/tngtech/archunit/lang/extension/TestServicesFile.java
+++ b/archunit/src/test/java/com/tngtech/archunit/lang/extension/TestServicesFile.java
@@ -1,12 +1,13 @@
 package com.tngtech.archunit.lang.extension;
 
-import java.io.File;
-
 import com.tngtech.archunit.testutil.ReplaceFileRule;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
+import java.io.File;
+
+import static com.tngtech.archunit.testutil.TestUtils.toUri;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 public class TestServicesFile implements TestRule {
@@ -18,7 +19,7 @@ public class TestServicesFile implements TestRule {
     }
 
     public void addService(Class<? extends ArchUnitExtension> extensionClass) {
-        File root = new File(getClass().getResource("/").getFile());
+        File root = new File(toUri(getClass().getResource("/")));
         File metaInfServices = new File(new File(root, "META-INF"), "services");
         File extensionSPI = new File(metaInfServices, ArchUnitExtension.class.getName());
         replaceFile.appendLine(extensionSPI, extensionClass.getName(), UTF_8);
diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/OutsideOfClassPathRule.java b/archunit/src/test/java/com/tngtech/archunit/testutil/OutsideOfClassPathRule.java
index a8e76406..0946aa24 100644
--- a/archunit/src/test/java/com/tngtech/archunit/testutil/OutsideOfClassPathRule.java
+++ b/archunit/src/test/java/com/tngtech/archunit/testutil/OutsideOfClassPathRule.java
@@ -1,14 +1,5 @@
 package com.tngtech.archunit.testutil;
 
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
@@ -18,8 +9,18 @@ import org.junit.rules.TemporaryFolder;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
 import static com.google.common.base.Preconditions.checkState;
 import static com.tngtech.archunit.testutil.TestUtils.newTemporaryFolder;
+import static com.tngtech.archunit.testutil.TestUtils.toUri;
 
 public class OutsideOfClassPathRule extends ExternalResource {
     private final TemporaryFolder temporaryFolder = new TemporaryFolder(newTemporaryFolder());
@@ -39,7 +40,7 @@ public class OutsideOfClassPathRule extends ExternalResource {
     }
 
     public Path setUp(URL folder) throws IOException {
-        this.originFolder = new File(folder.getFile()).toPath();
+        this.originFolder = new File(toUri(folder)).toPath();
         return moveOutOfClassPath();
     }
 
diff --git a/archunit/src/test/java/com/tngtech/archunit/testutil/TestUtils.java b/archunit/src/test/java/com/tngtech/archunit/testutil/TestUtils.java
index 6cfbf470..0636c734 100644
--- a/archunit/src/test/java/com/tngtech/archunit/testutil/TestUtils.java
+++ b/archunit/src/test/java/com/tngtech/archunit/testutil/TestUtils.java
@@ -2,6 +2,9 @@ package com.tngtech.archunit.testutil;
 
 import java.io.File;
 import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Properties;
@@ -11,6 +14,7 @@ import com.tngtech.archunit.core.domain.properties.HasName;
 import org.assertj.core.util.Files;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
 import static org.assertj.core.util.Files.temporaryFolderPath;
 import static org.assertj.core.util.Strings.concat;
 
@@ -63,4 +67,21 @@ public class TestUtils {
             }
         });
     }
+
+    public static URI toUri(URL url) {
+        try {
+            return url.toURI();
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static URL urlOf(Class<?> clazz) {
+        return requireNonNull(clazz.getResource("/" + clazz.getName().replace('.', '/') + ".class"),
+                String.format("Can't determine url of %s", clazz.getName()));
+    }
+
+    public static URI uriOf(Class<?> clazz) {
+        return toUri(urlOf(clazz));
+    }
 }