GH-246 - Upgrade to ArchUnit 1.1.0.

Related tickets: GH-221.
This commit is contained in:
Oliver Drotbohm
2023-08-09 21:17:23 +02:00
parent 290b1c899b
commit 43418b5fe0
2 changed files with 1 additions and 398 deletions

View File

@@ -33,7 +33,7 @@
<properties>
<archunit.version>1.0.1</archunit.version>
<archunit.version>1.1.0</archunit.version>
<artifactory-maven-plugin.version>3.5.1</artifactory-maven-plugin.version>
<flapdoodle-mongodb.version>4.6.1</flapdoodle-mongodb.version>
<jmolecules-bom.version>2023.0.0</jmolecules-bom.version>
@@ -463,8 +463,6 @@ limitations under the License.
<doclint>accessibility,html,reference,syntax</doclint>
<show>package</show>
<quiet>true</quiet>
<excludePackageNames>com.tngtech.archunit.core.importer</excludePackageNames>
<javadocVersion>8</javadocVersion>
<links>
<link>https://docs.spring.io/spring-boot/docs/${spring-boot.version}/api/</link>
<link>https://docs.spring.io/spring/docs/${spring.version}/javadoc-api/</link>

View File

@@ -1,395 +0,0 @@
/*
* Copyright 2014-2023 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.core.importer;
import static com.tngtech.archunit.PublicAPI.Usage.*;
import static com.tngtech.archunit.thirdparty.com.google.common.base.Preconditions.*;
import static java.util.Collections.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.ArchUnitException.LocationException;
import com.tngtech.archunit.base.ArchUnitException.UnsupportedUriSchemeException;
import com.tngtech.archunit.core.InitialConfiguration;
import com.tngtech.archunit.thirdparty.com.google.common.cache.Cache;
import com.tngtech.archunit.thirdparty.com.google.common.cache.CacheBuilder;
import com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableList;
/**
* Handles various forms of location from where classes can be imported in a consistent way. Any location will be
* treated like a {@link URI}, thus there will not be any platform dependent file separator problems.<br>
* <br>
* Examples for locations could be
* <ul>
* <li><code>file:///home/someuser/workspace/myproject/target/classes/myproject/Foo.class</code></li>
* <li><code>jar:file:///home/someuser/.m2/repository/myproject/foolib.jar!/myproject/Foo.class</code></li>
* </ul>
*/
@PublicAPI(usage = ACCESS)
public abstract class Location {
private static final InitialConfiguration<Set<Factory>> factories = new InitialConfiguration<>();
static {
ImportPlugin.Loader.loadForCurrentPlatform().plugInLocationFactories(factories);
}
private static final Cache<NormalizedUri, Collection<NormalizedResourceName>> ENTRY_CACHE = CacheBuilder.newBuilder()
.build();
final NormalizedUri uri;
Location(NormalizedUri uri) {
this.uri = checkNotNull(uri);
}
@PublicAPI(usage = ACCESS)
public URI asURI() {
return uri.toURI();
}
abstract ClassFileSource asClassFileSource(ImportOptions importOptions);
/**
* @param part A part to check the respective location {@link URI} for
* @return {@code true}, if the respective {@link URI} contains the given part, {@code false} otherwise
*/
@PublicAPI(usage = ACCESS)
public boolean contains(String part) {
return uri.toString().contains(part);
}
/**
* @param pattern A pattern to compare the respective location {@link URI} against
* @return {@code true}, if the respective {@link URI} matches the given pattern, {@code false} otherwise
*/
@PublicAPI(usage = ACCESS)
public boolean matches(Pattern pattern) {
return pattern.matcher(uri.toString()).matches();
}
@PublicAPI(usage = ACCESS)
public abstract boolean isJar();
/**
* This is a generalization of {@link #isJar()}. Before JDK 9, the only archives were Jar files, starting with JDK 9,
* we also have JRTs (the JDK modules).
*
* @return {@code true}, if this location represents an archive, like a JAR or JRT, {@code false} otherwise
*/
@PublicAPI(usage = ACCESS)
public abstract boolean isArchive();
// NOTE: URI behaves strange, if it is a JAR Uri, i.e. jar:file://.../some.jar!/, resolve does not work like expected
Location append(String relativeURI) {
relativeURI = encodeIllegalCharacters(relativeURI);
if (uri.toString().endsWith("/") && relativeURI.startsWith("/")) {
relativeURI = relativeURI.substring(1);
}
if (!uri.toString().endsWith("/") && !relativeURI.startsWith("/")) {
relativeURI = "/" + relativeURI;
}
return Location.of(URI.create(uri + relativeURI));
}
// NOTE: new URI(..) with more than one argument does URL encoding of illegal characters. URLEncoder on the other
// hand form-encodes all characters, even '/' which we do not want.
private String encodeIllegalCharacters(String relativeURI) {
try {
return new URI(null, null, relativeURI, null).toString();
} catch (URISyntaxException e) {
throw new LocationException(e);
}
}
void checkScheme(String scheme, NormalizedUri uri) {
String actualScheme = uri.getScheme();
checkArgument(scheme.equals(actualScheme),
"URI %s of Location must have scheme %s, but has %s", uri, scheme, actualScheme);
}
/**
* @return A Stream containing all class file names under this location, e.g. relative file names, Jar entry names,
* ...
*/
final Stream<NormalizedResourceName> streamEntries() {
try {
return ENTRY_CACHE.get(uri, this::readResourceEntries).stream();
} catch (ExecutionException e) {
throw new LocationException(e);
}
}
abstract Collection<NormalizedResourceName> readResourceEntries();
@Override
public int hashCode() {
return Objects.hash(uri);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Location other = (Location) obj;
return Objects.equals(this.uri, other.uri);
}
@Override
public String toString() {
return "Location{uri=" + uri + '}';
}
@PublicAPI(usage = ACCESS)
public static Location of(URL url) {
return of(toURI(url));
}
@PublicAPI(usage = ACCESS)
public static Location of(URI uri) {
uri = JarFileLocation.ensureJarProtocol(uri);
for (Factory factory : factories.get()) {
if (factory.supports(uri.getScheme())) {
return factory.create(uri);
}
}
throw new UnsupportedUriSchemeException(uri);
}
@PublicAPI(usage = ACCESS)
public static Location of(JarFile jar) {
return JarFileLocation.from(jar);
}
@PublicAPI(usage = ACCESS)
public static Location of(Path path) {
return FilePathLocation.from(path.toUri());
}
static URI toURI(URL url) {
try {
return url.toURI();
} catch (URISyntaxException e) {
throw new LocationException(e);
}
}
interface Factory {
boolean supports(String scheme);
Location create(URI uri);
}
static class JarFileLocationFactory implements Factory {
@Override
public boolean supports(String scheme) {
return JarFileLocation.SCHEME.equals(scheme);
}
@Override
public Location create(URI uri) {
return JarFileLocation.from(uri);
}
}
static class FilePathLocationFactory implements Factory {
@Override
public boolean supports(String scheme) {
return FilePathLocation.SCHEME.equals(scheme);
}
@Override
public Location create(URI uri) {
return FilePathLocation.from(uri);
}
}
private static class JarFileLocation extends Location {
private static final String SCHEME = "jar";
private JarFileLocation(NormalizedUri uri) {
super(uri);
checkScheme(SCHEME, uri);
}
static URI ensureJarProtocol(URI uri) {
return !SCHEME.equals(uri.getScheme()) && uri.getPath().endsWith(".jar") ? newJarUri(uri) : uri;
}
static JarFileLocation from(URI uri) {
checkArgument(uri.toString().contains("!/"), "JAR URI must contain '!/'");
return new JarFileLocation(NormalizedUri.from(uri));
}
static JarFileLocation from(JarFile jar) {
return from(newJarUri(FilePathLocation.newFileUri(jar.getName())));
}
private static URI newJarUri(URI uri) {
return URI.create(String.format("%s:%s!/", SCHEME, uri));
}
@Override
ClassFileSource asClassFileSource(ImportOptions importOptions) {
try {
String uriString = uri.toString();
int index = uriString.lastIndexOf("!/");
String[] parts = { uriString.substring(0, index), uriString.substring(index + 2) };
return new ClassFileSource.FromJar(new URL(parts[0] + "!/"), parts[1], importOptions);
} catch (IOException e) {
throw new LocationException(e);
}
}
@Override
public boolean isJar() {
return true;
}
@Override
public boolean isArchive() {
return true;
}
@Override
Collection<NormalizedResourceName> readResourceEntries() {
File file = getFileOfJar();
if (!file.exists()) {
return emptySet();
}
return readJarFileContent(file);
}
private File getFileOfJar() {
return new File(URI.create(uri.toString()
.replaceAll("^" + SCHEME + ":", "")
.replaceAll("!/.*", "")));
}
private Collection<NormalizedResourceName> readJarFileContent(File fileOfJar) {
ImmutableList.Builder<NormalizedResourceName> result = ImmutableList.builder();
String prefix = uri.toString().replaceAll(".*!/", "");
try (JarFile jarFile = new JarFile(fileOfJar)) {
result.addAll(readEntries(prefix, jarFile));
} catch (IOException e) {
throw new LocationException(e);
}
return result.build();
}
private List<NormalizedResourceName> readEntries(String prefix, JarFile jarFile) {
List<NormalizedResourceName> result = new ArrayList<>();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith(prefix) && entry.getName().endsWith(".class")) {
result.add(NormalizedResourceName.from(entry.getName()));
}
}
return result;
}
}
private static class FilePathLocation extends Location {
private static final String SCHEME = "file";
private FilePathLocation(NormalizedUri uri) {
super(uri);
checkScheme(SCHEME, uri);
}
static URI newFileUri(String fileName) {
return new File(fileName).toURI();
}
static FilePathLocation from(URI uri) {
return new FilePathLocation(NormalizedUri.from(uri));
}
@Override
ClassFileSource asClassFileSource(ImportOptions importOptions) {
return new ClassFileSource.FromFilePath(Paths.get(uri.toURI()), importOptions);
}
@Override
public boolean isJar() {
return false;
}
@Override
public boolean isArchive() {
return false;
}
@Override
Collection<NormalizedResourceName> readResourceEntries() {
try {
return getAllFilesBeneath(uri);
} catch (IOException e) {
throw new LocationException(e);
}
}
private List<NormalizedResourceName> getAllFilesBeneath(NormalizedUri uri) throws IOException {
File rootFile = new File(uri.toURI());
if (!rootFile.exists()) {
return emptyList();
}
return getAllFilesBeneath(rootFile.toPath());
}
private List<NormalizedResourceName> getAllFilesBeneath(Path root) throws IOException {
ImmutableList.Builder<NormalizedResourceName> result = ImmutableList.builder();
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".class")) {
result.add(NormalizedResourceName.from(root.relativize(file).toString()));
}
return FileVisitResult.CONTINUE;
}
});
return result.build();
}
}
}