Skip to content

Commit 6a2de78

Browse files
authored
Fix classpath scanning TNG#363
Unfortunately TNG#347 broke the possibility to use a manifest `Class-Path` attribute to specify the classpath. Since this is a feature that many tools use to avoid "command line too long" problems (e.g. on Windows) when the classpath grows in size, I have added support for this to the new way of scanning the classpath. I have also added back the old way to resolve files through the context classloader, since I could not measure a real performance impact and using both sources seems safer in case there is some other classpath quirk I have overlooked. The set will remove duplicates anyway, before starting to import the classes.
2 parents 15e5df6 + 0c3eb53 commit 6a2de78

File tree

9 files changed

+456
-30
lines changed

9 files changed

+456
-30
lines changed

.github/workflows/build.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
name: CI
22

3-
on: [push, pull_request]
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
48

59
jobs:
610
build:

.github/workflows/gradle-wrapper-validation.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
name: "Validate Gradle Wrapper"
2-
on: [push, pull_request]
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
38

49
jobs:
510
validation:

archunit/src/main/java/com/tngtech/archunit/core/importer/Location.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ private String encodeIllegalCharacters(String relativeURI) {
140140
}
141141

142142
void checkScheme(String scheme, NormalizedUri uri) {
143-
checkArgument(scheme.equals(uri.getScheme()),
144-
"URI %s of %s must have scheme %s, but has %s",
145-
uri, getClass().getSimpleName(), scheme, uri.getScheme());
143+
String actualScheme = uri.getScheme();
144+
checkArgument(scheme.equals(actualScheme),
145+
"URI %s of Location must have scheme %s, but has %s", uri, scheme, actualScheme);
146146
}
147147

148148
/**

archunit/src/main/java/com/tngtech/archunit/core/importer/Locations.java

+17-4
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@
1515
*/
1616
package com.tngtech.archunit.core.importer;
1717

18+
import java.io.IOException;
1819
import java.net.URL;
1920
import java.util.Collection;
20-
import java.util.HashSet;
21+
import java.util.List;
2122
import java.util.Set;
2223

2324
import com.google.common.collect.ImmutableSet;
2425
import com.tngtech.archunit.PublicAPI;
26+
import com.tngtech.archunit.base.ArchUnitException.LocationException;
2527
import com.tngtech.archunit.core.InitialConfiguration;
2628

29+
import static com.google.common.collect.Sets.newHashSet;
2730
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
31+
import static com.tngtech.archunit.base.ClassLoaders.getCurrentClassLoader;
32+
import static java.util.Collections.list;
2833

2934
/**
3035
* Represents a set of {@link Location locations} of Java class files. Also offers methods to derive concrete locations (i.e. URIs) from
@@ -106,7 +111,7 @@ private static String asResourceName(String qualifiedName) {
106111
private static Set<Location> getLocationsOf(String resourceName) {
107112
UrlSource classpath = locationResolver.get().resolveClassPath();
108113
NormalizedResourceName normalizedResourceName = NormalizedResourceName.from(resourceName);
109-
return ImmutableSet.copyOf(getResourceLocations(normalizedResourceName, classpath));
114+
return ImmutableSet.copyOf(getResourceLocations(getCurrentClassLoader(Locations.class), normalizedResourceName, classpath));
110115
}
111116

112117
/**
@@ -117,8 +122,8 @@ private static Set<Location> getLocationsOf(String resourceName) {
117122
* does not behave correctly for older Java versions,
118123
* because the folder entry {@code /java/io} is missing from {@code rt.jar}.
119124
*/
120-
private static Collection<Location> getResourceLocations(NormalizedResourceName resourceName, Iterable<URL> classpath) {
121-
Set<Location> result = new HashSet<>();
125+
private static Collection<Location> getResourceLocations(ClassLoader loader, NormalizedResourceName resourceName, Iterable<URL> classpath) {
126+
Set<Location> result = newHashSet(Locations.of(getResources(loader, resourceName)));
122127
for (Location location : Locations.of(classpath)) {
123128
if (containsEntryWithPrefix(location, resourceName)) {
124129
result.add(location.append(resourceName.toString()));
@@ -127,6 +132,14 @@ private static Collection<Location> getResourceLocations(NormalizedResourceName
127132
return result;
128133
}
129134

135+
private static List<URL> getResources(ClassLoader loader, NormalizedResourceName resourceName) {
136+
try {
137+
return list(loader.getResources(resourceName.toString()));
138+
} catch (IOException e) {
139+
throw new LocationException(e);
140+
}
141+
}
142+
130143
private static boolean containsEntryWithPrefix(Location location, NormalizedResourceName searchedJarEntryPrefix) {
131144
for (NormalizedResourceName name : location.iterateEntries()) {
132145
if (name.startsWith(searchedJarEntryPrefix)) {

archunit/src/main/java/com/tngtech/archunit/core/importer/UrlSource.java

+110-4
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
package com.tngtech.archunit.core.importer;
1717

1818
import java.io.File;
19+
import java.io.IOException;
20+
import java.net.JarURLConnection;
1921
import java.net.MalformedURLException;
2022
import java.net.URI;
2123
import java.net.URISyntaxException;
2224
import java.net.URL;
2325
import java.nio.file.InvalidPathException;
26+
import java.nio.file.Path;
2427
import java.nio.file.Paths;
2528
import java.util.ArrayList;
29+
import java.util.HashSet;
2630
import java.util.Iterator;
2731
import java.util.List;
2832
import java.util.Set;
@@ -31,13 +35,19 @@
3135
import com.google.common.base.Splitter;
3236
import com.google.common.collect.FluentIterable;
3337
import com.google.common.collect.ImmutableList;
38+
import com.google.common.collect.ImmutableSet;
39+
import com.google.common.collect.Sets;
3440
import com.tngtech.archunit.Internal;
3541
import com.tngtech.archunit.base.ArchUnitException.LocationException;
3642
import com.tngtech.archunit.base.Optional;
3743
import org.slf4j.Logger;
3844
import org.slf4j.LoggerFactory;
3945

46+
import static com.google.common.base.Strings.nullToEmpty;
47+
import static com.google.common.collect.Iterables.concat;
4048
import static com.tngtech.archunit.core.importer.Location.toURI;
49+
import static java.util.Collections.emptySet;
50+
import static java.util.jar.Attributes.Name.CLASS_PATH;
4151

4252
interface UrlSource extends Iterable<URL> {
4353
@Internal
@@ -68,10 +78,106 @@ private static Iterable<URL> unique(Iterable<URL> urls) {
6878
}
6979

7080
static UrlSource classPathSystemProperties() {
71-
return iterable(ImmutableList.<URL>builder()
81+
List<URL> directlySpecifiedAsProperties = ImmutableList.<URL>builder()
7282
.addAll(findUrlsForClassPathProperty(BOOT_CLASS_PATH_PROPERTY_NAME))
7383
.addAll(findUrlsForClassPathProperty(CLASS_PATH_PROPERTY_NAME))
74-
.build());
84+
.build();
85+
Iterable<URL> transitivelySpecifiedThroughManifest = readClasspathEntriesFromManifests(directlySpecifiedAsProperties);
86+
return iterable(concat(directlySpecifiedAsProperties, transitivelySpecifiedThroughManifest));
87+
}
88+
89+
private static Iterable<URL> readClasspathEntriesFromManifests(List<URL> urls) {
90+
Set<URI> result = new HashSet<>();
91+
readClasspathUriEntriesFromManifests(result, FluentIterable.from(urls).transform(URL_TO_URI));
92+
return FluentIterable.from(result).transform(URI_TO_URL);
93+
}
94+
95+
// Use URI because of better equals / hashcode
96+
private static void readClasspathUriEntriesFromManifests(Set<URI> result, Iterable<URI> urls) {
97+
for (URI url : urls) {
98+
if (url.getScheme().equals("jar")) {
99+
Set<URI> manifestUris = readClasspathEntriesFromManifest(url);
100+
Set<URI> unknownSoFar = ImmutableSet.copyOf(Sets.difference(manifestUris, result));
101+
result.addAll(unknownSoFar);
102+
readClasspathUriEntriesFromManifests(result, unknownSoFar);
103+
}
104+
}
105+
}
106+
107+
private static Set<URI> readClasspathEntriesFromManifest(URI url) {
108+
Optional<Path> jarPath = findParentPathOf(url);
109+
if (!jarPath.isPresent()) {
110+
return emptySet();
111+
}
112+
113+
Set<URI> result = new HashSet<>();
114+
for (String classpathEntry : Splitter.on(" ").omitEmptyStrings().split(readManifestClasspath(url))) {
115+
result.addAll(parseManifestClasspathEntry(jarPath.get(), classpathEntry).asSet());
116+
}
117+
return result;
118+
}
119+
120+
private static Optional<Path> findParentPathOf(URI uri) {
121+
try {
122+
return Optional.fromNullable(Paths.get(ensureFileUrl(uri).toURI()).getParent());
123+
} catch (Exception e) {
124+
LOG.warn("Could not find parent folder for " + uri, e);
125+
return Optional.absent();
126+
}
127+
}
128+
129+
private static URL ensureFileUrl(URI url) throws IOException {
130+
return ((JarURLConnection) url.toURL().openConnection()).getJarFileURL();
131+
}
132+
133+
private static String readManifestClasspath(URI uri) {
134+
try {
135+
String result = (String) ((JarURLConnection) uri.toURL().openConnection()).getMainAttributes().get(CLASS_PATH);
136+
return nullToEmpty(result);
137+
} catch (Exception e) {
138+
return "";
139+
}
140+
}
141+
142+
private static Optional<URI> parseManifestClasspathEntry(Path parent, String classpathEntry) {
143+
if (isUrl(classpathEntry)) {
144+
return parseUrl(parent, classpathEntry);
145+
} else {
146+
return parsePath(parent, classpathEntry);
147+
}
148+
}
149+
150+
private static boolean isUrl(String classpathEntry) {
151+
return classpathEntry.startsWith("file:") || classpathEntry.startsWith("jar:");
152+
}
153+
154+
private static Optional<URI> parseUrl(Path parent, String classpathUrlEntry) {
155+
try {
156+
return Optional.of(convertToJarUrlIfNecessary(parent.toUri().resolve(URI.create(classpathUrlEntry).getSchemeSpecificPart())));
157+
} catch (Exception e) {
158+
LOG.warn("Cannot parse URL classpath entry " + classpathUrlEntry, e);
159+
return Optional.absent();
160+
}
161+
}
162+
163+
private static Optional<URI> parsePath(Path parent, String classpathFilePathEntry) {
164+
try {
165+
Path path = Paths.get(classpathFilePathEntry);
166+
if (!path.isAbsolute()) {
167+
path = parent.resolve(path);
168+
}
169+
return Optional.of(convertToJarUrlIfNecessary(path.toUri()));
170+
} catch (Exception e) {
171+
LOG.warn("Cannot parse file path classpath entry " + classpathFilePathEntry, e);
172+
return Optional.absent();
173+
}
174+
}
175+
176+
private static URI convertToJarUrlIfNecessary(URI uri) {
177+
if (uri.toString().endsWith(".jar")) {
178+
return URI.create("jar:" + uri + "!/");
179+
}
180+
return uri;
75181
}
76182

77183
private static List<URL> findUrlsForClassPathProperty(String propertyName) {
@@ -85,7 +191,7 @@ private static List<URL> findUrlsForClassPathProperty(String propertyName) {
85191
}
86192

87193
private static Optional<URL> parseClassPathEntry(String path) {
88-
return path.endsWith(".jar") ? newJarUri(path) : newFileUri(path);
194+
return path.endsWith(".jar") ? newJarUrl(path) : newFileUri(path);
89195
}
90196

91197
private static Optional<URL> newFileUri(String path) {
@@ -118,7 +224,7 @@ private static Optional<URL> tryResolvePathFromUrl(String path) {
118224
}
119225
}
120226

121-
private static Optional<URL> newJarUri(String path) {
227+
private static Optional<URL> newJarUrl(String path) {
122228
Optional<URL> fileUri = newFileUri(path);
123229

124230
try {

archunit/src/test/java/com/tngtech/archunit/core/importer/ClassFileImporterSlowTest.java

+44
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,45 @@
44
import java.io.IOException;
55
import java.lang.annotation.Annotation;
66
import java.lang.reflect.Field;
7+
import java.net.URL;
8+
import java.net.URLClassLoader;
79
import java.util.List;
810

11+
import com.google.common.base.Joiner;
12+
import com.google.common.base.Splitter;
913
import com.google.common.collect.ImmutableList;
1014
import com.google.common.collect.ImmutableSet;
1115
import com.tngtech.archunit.Slow;
1216
import com.tngtech.archunit.core.domain.JavaClass;
1317
import com.tngtech.archunit.core.domain.JavaClasses;
1418
import com.tngtech.archunit.core.domain.JavaPackage;
19+
import com.tngtech.archunit.testutil.ContextClassLoaderRule;
20+
import com.tngtech.archunit.testutil.SystemPropertiesRule;
1521
import com.tngtech.archunit.testutil.TransientCopyRule;
22+
import org.junit.Assert;
1623
import org.junit.Rule;
1724
import org.junit.Test;
1825
import org.junit.experimental.categories.Category;
26+
import org.junit.rules.TemporaryFolder;
1927

2028
import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
2129
import static com.tngtech.archunit.core.importer.ClassFileImporterTest.jarFileOf;
2230
import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS;
31+
import static com.tngtech.archunit.core.importer.UrlSourceTest.JAVA_CLASS_PATH_PROP;
2332
import static com.tngtech.archunit.testutil.Assertions.assertThat;
2433
import static com.tngtech.archunit.testutil.Assertions.assertThatClasses;
34+
import static java.util.jar.Attributes.Name.CLASS_PATH;
2535

2636
@Category(Slow.class)
2737
public class ClassFileImporterSlowTest {
2838
@Rule
2939
public final TransientCopyRule copyRule = new TransientCopyRule();
40+
@Rule
41+
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
42+
@Rule
43+
public final SystemPropertiesRule systemPropertiesRule = new SystemPropertiesRule();
44+
@Rule
45+
public final ContextClassLoaderRule contextClassLoaderRule = new ContextClassLoaderRule();
3046

3147
@Test
3248
public void imports_the_classpath() {
@@ -102,6 +118,34 @@ public void imports_duplicate_classes() throws IOException {
102118
assertThat(classes.get(JavaClass.class)).isNotNull();
103119
}
104120

121+
@Test
122+
public void imports_classes_from_classpath_specified_in_manifest_file() {
123+
String manifestClasspath =
124+
Joiner.on(" ").join(Splitter.on(File.pathSeparator).omitEmptyStrings().split(System.getProperty(JAVA_CLASS_PATH_PROP)));
125+
String jarPath = new TestJarFile()
126+
.withManifestAttribute(CLASS_PATH, manifestClasspath)
127+
.create()
128+
.getName();
129+
130+
System.clearProperty(JAVA_CLASS_PATH_PROP);
131+
// Ensure we cannot load the class through the fallback via the Classloader
132+
Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[0], null));
133+
verifyCantLoadWithCurrentClasspath(getClass());
134+
System.setProperty(JAVA_CLASS_PATH_PROP, jarPath);
135+
136+
JavaClasses javaClasses = new ClassFileImporter().importPackages(getClass().getPackage().getName());
137+
138+
assertThatClasses(javaClasses).contain(getClass());
139+
}
140+
141+
private void verifyCantLoadWithCurrentClasspath(Class<?> clazz) {
142+
try {
143+
new ClassFileImporter().importClass(clazz);
144+
Assert.fail(String.format("Should not have been able to load class %s with the current classpath", clazz.getName()));
145+
} catch (RuntimeException ignored) {
146+
}
147+
}
148+
105149
@Test
106150
public void creates_JavaPackages() {
107151
JavaClasses javaClasses = importJavaBase();

0 commit comments

Comments
 (0)