Skip to content

Commit e6d9d35

Browse files
committed
fix broken classpath scanning for manifest classpaths
Tools often have an option to specify the classpath as manifest entry, i.e. to run tests the tool will create an empty JAR only containing a manifest with an `Class-Path` attribute containing the actual classpath. This is useful if the classpath is long and the platform (e.g. Windows) does not support a sufficiently long command line, thus limiting the length of the classpath supplied on the command line. Unfortunately the recent fix for Android, switching from loading the classpath through the respective Classloader to resolving the classpath manually, did not take this case into account anymore and it slipped through the tests, since no environment would specify the classpath via manifest. Now we also resolve those manifest attributes when manually resolving the classpath. Signed-off-by: Peter Gafert <[email protected]>
1 parent 15e5df6 commit e6d9d35

File tree

4 files changed

+400
-21
lines changed

4 files changed

+400
-21
lines changed

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

Lines changed: 110 additions & 4 deletions
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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,38 @@
66
import java.lang.reflect.Field;
77
import java.util.List;
88

9+
import com.google.common.base.Joiner;
10+
import com.google.common.base.Splitter;
911
import com.google.common.collect.ImmutableList;
1012
import com.google.common.collect.ImmutableSet;
1113
import com.tngtech.archunit.Slow;
1214
import com.tngtech.archunit.core.domain.JavaClass;
1315
import com.tngtech.archunit.core.domain.JavaClasses;
1416
import com.tngtech.archunit.core.domain.JavaPackage;
17+
import com.tngtech.archunit.testutil.SystemPropertiesRule;
1518
import com.tngtech.archunit.testutil.TransientCopyRule;
19+
import org.junit.Assert;
1620
import org.junit.Rule;
1721
import org.junit.Test;
1822
import org.junit.experimental.categories.Category;
23+
import org.junit.rules.TemporaryFolder;
1924

2025
import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
2126
import static com.tngtech.archunit.core.importer.ClassFileImporterTest.jarFileOf;
2227
import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS;
28+
import static com.tngtech.archunit.core.importer.UrlSourceTest.JAVA_CLASS_PATH_PROP;
2329
import static com.tngtech.archunit.testutil.Assertions.assertThat;
2430
import static com.tngtech.archunit.testutil.Assertions.assertThatClasses;
31+
import static java.util.jar.Attributes.Name.CLASS_PATH;
2532

2633
@Category(Slow.class)
2734
public class ClassFileImporterSlowTest {
2835
@Rule
2936
public final TransientCopyRule copyRule = new TransientCopyRule();
37+
@Rule
38+
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
39+
@Rule
40+
public final SystemPropertiesRule systemPropertiesRule = new SystemPropertiesRule();
3041

3142
@Test
3243
public void imports_the_classpath() {
@@ -102,6 +113,31 @@ public void imports_duplicate_classes() throws IOException {
102113
assertThat(classes.get(JavaClass.class)).isNotNull();
103114
}
104115

116+
@Test
117+
public void imports_classes_from_classpath_specified_in_manifest_file() {
118+
String manifestClasspath = Joiner.on(" ").join(Splitter.on(File.pathSeparator).omitEmptyStrings().split(System.getProperty(JAVA_CLASS_PATH_PROP)));
119+
String jarPath = new TestJarFile()
120+
.withManifestAttribute(CLASS_PATH, manifestClasspath)
121+
.create()
122+
.getName();
123+
124+
System.clearProperty(JAVA_CLASS_PATH_PROP);
125+
verifyCantLoadWithCurrentClasspath(getClass());
126+
System.setProperty(JAVA_CLASS_PATH_PROP, jarPath);
127+
128+
JavaClasses javaClasses = new ClassFileImporter().importPackages(getClass().getPackage().getName());
129+
130+
assertThatClasses(javaClasses).contain(getClass());
131+
}
132+
133+
private void verifyCantLoadWithCurrentClasspath(Class<?> clazz) {
134+
try {
135+
new ClassFileImporter().importClass(clazz);
136+
Assert.fail(String.format("Should not have been able to load class %s with the current classpath", clazz.getName()));
137+
} catch (RuntimeException ignored) {
138+
}
139+
}
140+
105141
@Test
106142
public void creates_JavaPackages() {
107143
JavaClasses javaClasses = importJavaBase();

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,50 @@
55
import java.io.IOException;
66
import java.util.HashSet;
77
import java.util.Set;
8+
import java.util.jar.Attributes;
89
import java.util.jar.JarFile;
910
import java.util.jar.JarOutputStream;
11+
import java.util.jar.Manifest;
1012
import java.util.zip.ZipEntry;
1113

1214
import com.tngtech.archunit.testutil.TestUtils;
1315

1416
import static com.google.common.io.ByteStreams.toByteArray;
17+
import static java.util.jar.Attributes.Name.MANIFEST_VERSION;
1518

1619
class TestJarFile {
20+
private final Manifest manifest;
1721
private final Set<String> entries = new HashSet<>();
1822

19-
TestJarFile withEntry(String entry) {
20-
entries.add(entry);
23+
TestJarFile() {
24+
manifest = new Manifest();
25+
manifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
26+
}
27+
28+
TestJarFile withManifestAttribute(Attributes.Name name, String value) {
29+
manifest.getMainAttributes().put(name, value);
2130
return this;
2231
}
2332

24-
TestJarFile withEntries(Iterable<String> entries) {
25-
for (String entry : entries) {
26-
withEntry(entry);
27-
}
33+
TestJarFile withEntry(String entry) {
34+
entries.add(entry);
2835
return this;
2936
}
3037

3138
JarFile create() {
3239
File folder = TestUtils.newTemporaryFolder();
33-
File file = new File(folder, "test.jar");
40+
return create(new File(folder, "test.jar"));
41+
}
3442

35-
try (JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(file))) {
43+
JarFile create(File jarFile) {
44+
try (JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(jarFile), manifest)) {
3645
for (String entry : entries) {
3746
write(jarOut, entry);
3847
}
3948
} catch (IOException e) {
4049
throw new RuntimeException(e);
4150
}
42-
return newJarFile(file);
51+
return newJarFile(jarFile);
4352
}
4453

4554
private void write(JarOutputStream jarOut, String entry) throws IOException {

0 commit comments

Comments
 (0)