package com.tngtech.archunit;
import java.lang.annotation.Annotation;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.MayResolveTypesViaReflection;
import com.tngtech.archunit.core.ResolvesTypesViaReflection;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaAccess.Functions.Get;
import com.tngtech.archunit.core.domain.JavaCall;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.properties.HasOwner;
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.DomainBuilders;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.core.importer.Location;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchRules;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.runner.RunWith;
import static com.tngtech.archunit.base.DescribedPredicate.not;
import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.origin;
import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.target;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DONT_INCLUDE_TESTS;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.has;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.is;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(
packagesOf = ArchUnitArchitectureTest.class,
importOptions = ArchUnitArchitectureTest.ArchUnitProductionCode.class)
public class ArchUnitArchitectureTest {
static final String THIRDPARTY_PACKAGE_IDENTIFIER = "..thirdparty..";
@ArchTest
public static final ArchRule layers_are_respected = layeredArchitecture()
.layer("Root").definedBy("com.tngtech.archunit")
.layer("Base").definedBy("com.tngtech.archunit.base..")
.layer("Core").definedBy("com.tngtech.archunit.core..")
.layer("Lang").definedBy("com.tngtech.archunit.lang..")
.layer("Library").definedBy("com.tngtech.archunit.library..")
.layer("JUnit").definedBy("com.tngtech.archunit.junit..")
.whereLayer("JUnit").mayNotBeAccessedByAnyLayer()
.whereLayer("Library").mayOnlyBeAccessedByLayers("JUnit")
.whereLayer("Lang").mayOnlyBeAccessedByLayers("Library", "JUnit")
.whereLayer("Core").mayOnlyBeAccessedByLayers("Lang", "Library", "JUnit")
.whereLayer("Base").mayOnlyBeAccessedByLayers("Root", "Core", "Lang", "Library", "JUnit");
@ArchTest
public static final ArchRule domain_does_not_access_importer =
noClasses().that().resideInAPackage("..core.domain..")
.should().accessClassesThat(belong_to_the_import_context());
@ArchTest
public static final ArchRule types_are_only_resolved_via_reflection_in_allowed_places =
noClasses().that().resideOutsideOfPackage(THIRDPARTY_PACKAGE_IDENTIFIER)
.should().callMethodWhere(typeIsIllegallyResolvedViaReflection())
.as("no classes should illegally resolve classes via reflection");
@ArchTest
public static final ArchRules public_API_rules =
ArchRules.in(PublicAPIRules.class);
private static DescribedPredicate<JavaClass> belong_to_the_import_context() {
return new DescribedPredicate<JavaClass>("belong to the import context") {
@Override
public boolean apply(JavaClass input) {
return input.getPackage().startsWith(ClassFileImporter.class.getPackage().getName())
&& !input.getName().contains(DomainBuilders.class.getSimpleName());
}
};
}
private static DescribedPredicate<JavaCall<?>> typeIsIllegallyResolvedViaReflection() {
DescribedPredicate<JavaCall<?>> explicitlyAllowedUsage =
origin(is(annotatedWith(MayResolveTypesViaReflection.class)))
.or(contextIsAnnotatedWith(MayResolveTypesViaReflection.class)).forSubType();
return classIsResolvedViaReflection().and(not(explicitlyAllowedUsage));
}
private static DescribedPredicate<JavaAccess<?>> contextIsAnnotatedWith(final Class<? extends Annotation> annotationType) {
return origin(With.owner(withAnnotation(annotationType)));
}
private static DescribedPredicate<JavaClass> withAnnotation(final Class<? extends Annotation> annotationType) {
return new DescribedPredicate<JavaClass>("annotated with @" + annotationType.getName()) {
@Override
public boolean apply(JavaClass input) {
return input.isAnnotatedWith(annotationType)
|| enclosingClassIsAnnotated(input);
}
private boolean enclosingClassIsAnnotated(JavaClass input) {
return input.getEnclosingClass().isPresent() &&
input.getEnclosingClass().get().isAnnotatedWith(annotationType);
}
};
}
private static DescribedPredicate<JavaCall<?>> classIsResolvedViaReflection() {
DescribedPredicate<JavaCall<?>> defaultClassForName =
target(HasOwner.Functions.Get.<JavaClass>owner()
.is(equivalentTo(Class.class)))
.and(target(has(name("forName"))))
.forSubType();
DescribedPredicate<JavaCall<?>> targetIsMarked =
annotatedWith(ResolvesTypesViaReflection.class).onResultOf(Get.target());
return defaultClassForName.or(targetIsMarked);
}
public static class ArchUnitProductionCode implements ImportOption {
private static final Set<String> SOURCE_ROOTS = sourceRootsOf(ArchConfiguration.class, ArchUnitRunner.class);
private static Set<String> sourceRootsOf(Class<?>... classes) {
ImmutableSet.Builder<String> result = ImmutableSet.builder();
for (Class<?> c : classes) {
String classFile = "/" + c.getName().replace('.', '/') + ".class";
String file = c.getResource(classFile).getFile();
result.add(file.substring(0, file.indexOf(classFile)));
}
return result.build();
}
@Override
public boolean includes(Location location) {
boolean include = false;
for (String sourceRoot : SOURCE_ROOTS) {
if (location.contains(sourceRoot)) {
include = true;
}
}
return include && DONT_INCLUDE_TESTS.includes(location);
}
}
}