Skip to content

Commit 9fe1143

Browse files
committed
GH-221 - Temporarily shadow ArchUnit's Location type.
It includes the fix submitted for TNG/ArchUnit#1131. To be removed once we can upgrade to a released version of the fix.
1 parent 32b689d commit 9fe1143

File tree

1 file changed

+395
-0
lines changed
  • spring-modulith-core/src/main/java/com/tngtech/archunit/core/importer

1 file changed

+395
-0
lines changed
Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
/*
2+
* Copyright 2014-2023 TNG Technology Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.tngtech.archunit.core.importer;
17+
18+
import static com.tngtech.archunit.PublicAPI.Usage.*;
19+
import static com.tngtech.archunit.thirdparty.com.google.common.base.Preconditions.*;
20+
import static java.util.Collections.*;
21+
22+
import java.io.File;
23+
import java.io.IOException;
24+
import java.net.URI;
25+
import java.net.URISyntaxException;
26+
import java.net.URL;
27+
import java.nio.file.FileVisitResult;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.nio.file.Paths;
31+
import java.nio.file.SimpleFileVisitor;
32+
import java.nio.file.attribute.BasicFileAttributes;
33+
import java.util.ArrayList;
34+
import java.util.Collection;
35+
import java.util.Enumeration;
36+
import java.util.List;
37+
import java.util.Objects;
38+
import java.util.Set;
39+
import java.util.concurrent.ExecutionException;
40+
import java.util.jar.JarEntry;
41+
import java.util.jar.JarFile;
42+
import java.util.regex.Pattern;
43+
import java.util.stream.Stream;
44+
45+
import com.tngtech.archunit.PublicAPI;
46+
import com.tngtech.archunit.base.ArchUnitException.LocationException;
47+
import com.tngtech.archunit.base.ArchUnitException.UnsupportedUriSchemeException;
48+
import com.tngtech.archunit.core.InitialConfiguration;
49+
import com.tngtech.archunit.thirdparty.com.google.common.cache.Cache;
50+
import com.tngtech.archunit.thirdparty.com.google.common.cache.CacheBuilder;
51+
import com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableList;
52+
53+
/**
54+
* Handles various forms of location from where classes can be imported in a consistent way. Any location will be
55+
* treated like a {@link URI}, thus there will not be any platform dependent file separator problems.<br>
56+
* <br>
57+
* Examples for locations could be
58+
* <ul>
59+
* <li><code>file:///home/someuser/workspace/myproject/target/classes/myproject/Foo.class</code></li>
60+
* <li><code>jar:file:///home/someuser/.m2/repository/myproject/foolib.jar!/myproject/Foo.class</code></li>
61+
* </ul>
62+
*/
63+
@PublicAPI(usage = ACCESS)
64+
public abstract class Location {
65+
private static final InitialConfiguration<Set<Factory>> factories = new InitialConfiguration<>();
66+
67+
static {
68+
ImportPlugin.Loader.loadForCurrentPlatform().plugInLocationFactories(factories);
69+
}
70+
71+
private static final Cache<NormalizedUri, Collection<NormalizedResourceName>> ENTRY_CACHE = CacheBuilder.newBuilder()
72+
.build();
73+
74+
final NormalizedUri uri;
75+
76+
Location(NormalizedUri uri) {
77+
this.uri = checkNotNull(uri);
78+
}
79+
80+
@PublicAPI(usage = ACCESS)
81+
public URI asURI() {
82+
return uri.toURI();
83+
}
84+
85+
abstract ClassFileSource asClassFileSource(ImportOptions importOptions);
86+
87+
/**
88+
* @param part A part to check the respective location {@link URI} for
89+
* @return {@code true}, if the respective {@link URI} contains the given part, {@code false} otherwise
90+
*/
91+
@PublicAPI(usage = ACCESS)
92+
public boolean contains(String part) {
93+
return uri.toString().contains(part);
94+
}
95+
96+
/**
97+
* @param pattern A pattern to compare the respective location {@link URI} against
98+
* @return {@code true}, if the respective {@link URI} matches the given pattern, {@code false} otherwise
99+
*/
100+
@PublicAPI(usage = ACCESS)
101+
public boolean matches(Pattern pattern) {
102+
return pattern.matcher(uri.toString()).matches();
103+
}
104+
105+
@PublicAPI(usage = ACCESS)
106+
public abstract boolean isJar();
107+
108+
/**
109+
* This is a generalization of {@link #isJar()}. Before JDK 9, the only archives were Jar files, starting with JDK 9,
110+
* we also have JRTs (the JDK modules).
111+
*
112+
* @return {@code true}, if this location represents an archive, like a JAR or JRT, {@code false} otherwise
113+
*/
114+
@PublicAPI(usage = ACCESS)
115+
public abstract boolean isArchive();
116+
117+
// NOTE: URI behaves strange, if it is a JAR Uri, i.e. jar:file://.../some.jar!/, resolve does not work like expected
118+
Location append(String relativeURI) {
119+
relativeURI = encodeIllegalCharacters(relativeURI);
120+
if (uri.toString().endsWith("/") && relativeURI.startsWith("/")) {
121+
relativeURI = relativeURI.substring(1);
122+
}
123+
if (!uri.toString().endsWith("/") && !relativeURI.startsWith("/")) {
124+
relativeURI = "/" + relativeURI;
125+
}
126+
return Location.of(URI.create(uri + relativeURI));
127+
}
128+
129+
// NOTE: new URI(..) with more than one argument does URL encoding of illegal characters. URLEncoder on the other
130+
// hand form-encodes all characters, even '/' which we do not want.
131+
private String encodeIllegalCharacters(String relativeURI) {
132+
try {
133+
return new URI(null, null, relativeURI, null).toString();
134+
} catch (URISyntaxException e) {
135+
throw new LocationException(e);
136+
}
137+
}
138+
139+
void checkScheme(String scheme, NormalizedUri uri) {
140+
String actualScheme = uri.getScheme();
141+
checkArgument(scheme.equals(actualScheme),
142+
"URI %s of Location must have scheme %s, but has %s", uri, scheme, actualScheme);
143+
}
144+
145+
/**
146+
* @return A Stream containing all class file names under this location, e.g. relative file names, Jar entry names,
147+
* ...
148+
*/
149+
final Stream<NormalizedResourceName> streamEntries() {
150+
try {
151+
return ENTRY_CACHE.get(uri, this::readResourceEntries).stream();
152+
} catch (ExecutionException e) {
153+
throw new LocationException(e);
154+
}
155+
}
156+
157+
abstract Collection<NormalizedResourceName> readResourceEntries();
158+
159+
@Override
160+
public int hashCode() {
161+
return Objects.hash(uri);
162+
}
163+
164+
@Override
165+
public boolean equals(Object obj) {
166+
if (this == obj) {
167+
return true;
168+
}
169+
if (obj == null || getClass() != obj.getClass()) {
170+
return false;
171+
}
172+
Location other = (Location) obj;
173+
return Objects.equals(this.uri, other.uri);
174+
}
175+
176+
@Override
177+
public String toString() {
178+
return "Location{uri=" + uri + '}';
179+
}
180+
181+
@PublicAPI(usage = ACCESS)
182+
public static Location of(URL url) {
183+
return of(toURI(url));
184+
}
185+
186+
@PublicAPI(usage = ACCESS)
187+
public static Location of(URI uri) {
188+
uri = JarFileLocation.ensureJarProtocol(uri);
189+
for (Factory factory : factories.get()) {
190+
if (factory.supports(uri.getScheme())) {
191+
return factory.create(uri);
192+
}
193+
}
194+
throw new UnsupportedUriSchemeException(uri);
195+
}
196+
197+
@PublicAPI(usage = ACCESS)
198+
public static Location of(JarFile jar) {
199+
return JarFileLocation.from(jar);
200+
}
201+
202+
@PublicAPI(usage = ACCESS)
203+
public static Location of(Path path) {
204+
return FilePathLocation.from(path.toUri());
205+
}
206+
207+
static URI toURI(URL url) {
208+
try {
209+
return url.toURI();
210+
} catch (URISyntaxException e) {
211+
throw new LocationException(e);
212+
}
213+
}
214+
215+
interface Factory {
216+
boolean supports(String scheme);
217+
218+
Location create(URI uri);
219+
}
220+
221+
static class JarFileLocationFactory implements Factory {
222+
@Override
223+
public boolean supports(String scheme) {
224+
return JarFileLocation.SCHEME.equals(scheme);
225+
}
226+
227+
@Override
228+
public Location create(URI uri) {
229+
return JarFileLocation.from(uri);
230+
}
231+
}
232+
233+
static class FilePathLocationFactory implements Factory {
234+
@Override
235+
public boolean supports(String scheme) {
236+
return FilePathLocation.SCHEME.equals(scheme);
237+
}
238+
239+
@Override
240+
public Location create(URI uri) {
241+
return FilePathLocation.from(uri);
242+
}
243+
}
244+
245+
private static class JarFileLocation extends Location {
246+
private static final String SCHEME = "jar";
247+
248+
private JarFileLocation(NormalizedUri uri) {
249+
super(uri);
250+
checkScheme(SCHEME, uri);
251+
}
252+
253+
static URI ensureJarProtocol(URI uri) {
254+
return !SCHEME.equals(uri.getScheme()) && uri.getPath().endsWith(".jar") ? newJarUri(uri) : uri;
255+
}
256+
257+
static JarFileLocation from(URI uri) {
258+
checkArgument(uri.toString().contains("!/"), "JAR URI must contain '!/'");
259+
return new JarFileLocation(NormalizedUri.from(uri));
260+
}
261+
262+
static JarFileLocation from(JarFile jar) {
263+
return from(newJarUri(FilePathLocation.newFileUri(jar.getName())));
264+
}
265+
266+
private static URI newJarUri(URI uri) {
267+
return URI.create(String.format("%s:%s!/", SCHEME, uri));
268+
}
269+
270+
@Override
271+
ClassFileSource asClassFileSource(ImportOptions importOptions) {
272+
try {
273+
String uriString = uri.toString();
274+
int index = uriString.lastIndexOf("!/");
275+
String[] parts = { uriString.substring(0, index), uriString.substring(index + 2) };
276+
return new ClassFileSource.FromJar(new URL(parts[0] + "!/"), parts[1], importOptions);
277+
} catch (IOException e) {
278+
throw new LocationException(e);
279+
}
280+
}
281+
282+
@Override
283+
public boolean isJar() {
284+
return true;
285+
}
286+
287+
@Override
288+
public boolean isArchive() {
289+
return true;
290+
}
291+
292+
@Override
293+
Collection<NormalizedResourceName> readResourceEntries() {
294+
File file = getFileOfJar();
295+
if (!file.exists()) {
296+
return emptySet();
297+
}
298+
299+
return readJarFileContent(file);
300+
}
301+
302+
private File getFileOfJar() {
303+
return new File(URI.create(uri.toString()
304+
.replaceAll("^" + SCHEME + ":", "")
305+
.replaceAll("!/.*", "")));
306+
}
307+
308+
private Collection<NormalizedResourceName> readJarFileContent(File fileOfJar) {
309+
ImmutableList.Builder<NormalizedResourceName> result = ImmutableList.builder();
310+
String prefix = uri.toString().replaceAll(".*!/", "");
311+
try (JarFile jarFile = new JarFile(fileOfJar)) {
312+
result.addAll(readEntries(prefix, jarFile));
313+
} catch (IOException e) {
314+
throw new LocationException(e);
315+
}
316+
return result.build();
317+
}
318+
319+
private List<NormalizedResourceName> readEntries(String prefix, JarFile jarFile) {
320+
List<NormalizedResourceName> result = new ArrayList<>();
321+
Enumeration<JarEntry> entries = jarFile.entries();
322+
while (entries.hasMoreElements()) {
323+
JarEntry entry = entries.nextElement();
324+
if (entry.getName().startsWith(prefix) && entry.getName().endsWith(".class")) {
325+
result.add(NormalizedResourceName.from(entry.getName()));
326+
}
327+
}
328+
return result;
329+
}
330+
}
331+
332+
private static class FilePathLocation extends Location {
333+
private static final String SCHEME = "file";
334+
335+
private FilePathLocation(NormalizedUri uri) {
336+
super(uri);
337+
checkScheme(SCHEME, uri);
338+
}
339+
340+
static URI newFileUri(String fileName) {
341+
return new File(fileName).toURI();
342+
}
343+
344+
static FilePathLocation from(URI uri) {
345+
return new FilePathLocation(NormalizedUri.from(uri));
346+
}
347+
348+
@Override
349+
ClassFileSource asClassFileSource(ImportOptions importOptions) {
350+
return new ClassFileSource.FromFilePath(Paths.get(uri.toURI()), importOptions);
351+
}
352+
353+
@Override
354+
public boolean isJar() {
355+
return false;
356+
}
357+
358+
@Override
359+
public boolean isArchive() {
360+
return false;
361+
}
362+
363+
@Override
364+
Collection<NormalizedResourceName> readResourceEntries() {
365+
try {
366+
return getAllFilesBeneath(uri);
367+
} catch (IOException e) {
368+
throw new LocationException(e);
369+
}
370+
}
371+
372+
private List<NormalizedResourceName> getAllFilesBeneath(NormalizedUri uri) throws IOException {
373+
File rootFile = new File(uri.toURI());
374+
if (!rootFile.exists()) {
375+
return emptyList();
376+
}
377+
378+
return getAllFilesBeneath(rootFile.toPath());
379+
}
380+
381+
private List<NormalizedResourceName> getAllFilesBeneath(Path root) throws IOException {
382+
ImmutableList.Builder<NormalizedResourceName> result = ImmutableList.builder();
383+
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
384+
@Override
385+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
386+
if (file.toString().endsWith(".class")) {
387+
result.add(NormalizedResourceName.from(root.relativize(file).toString()));
388+
}
389+
return FileVisitResult.CONTINUE;
390+
}
391+
});
392+
return result.build();
393+
}
394+
}
395+
}

0 commit comments

Comments
 (0)