Skip to content

Commit a650361

Browse files
committed
Merge branch 'gh-17299' into 2.6.x
Closes gh-17299
2 parents 8bcde6d + 4bb13bc commit a650361

File tree

14 files changed

+394
-20
lines changed

14 files changed

+394
-20
lines changed

buildSrc/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ext {
2525
dependencies {
2626
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
2727
implementation(platform("org.springframework:spring-framework-bom:5.3.15"))
28+
implementation("com.tngtech.archunit:archunit:1.0.0")
2829
implementation("com.fasterxml.jackson.core:jackson-databind:2.11.4")
2930
implementation("commons-codec:commons-codec:1.13")
3031
implementation("org.apache.maven:maven-embedder:3.6.2")
@@ -51,6 +52,10 @@ gradlePlugin {
5152
id = "org.springframework.boot.annotation-processor"
5253
implementationClass = "org.springframework.boot.build.processors.AnnotationProcessorPlugin"
5354
}
55+
architecturePlugin {
56+
id = "org.springframework.boot.architecture"
57+
implementationClass = "org.springframework.boot.build.architecture.ArchitecturePlugin"
58+
}
5459
autoConfigurationPlugin {
5560
id = "org.springframework.boot.auto-configuration"
5661
implementationClass = "org.springframework.boot.build.autoconfigure.AutoConfigurationPlugin"

buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.gradle.testretry.TestRetryPlugin;
4949
import org.gradle.testretry.TestRetryTaskExtension;
5050

51+
import org.springframework.boot.build.architecture.ArchitecturePlugin;
5152
import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies;
5253
import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
5354
import org.springframework.boot.build.testing.TestFailuresPlugin;
@@ -61,8 +62,8 @@
6162
* <ul>
6263
* <li>The project is configured with source and target compatibility of 1.8
6364
* <li>{@link SpringJavaFormatPlugin Spring Java Format}, {@link CheckstylePlugin
64-
* Checkstyle}, {@link TestFailuresPlugin Test Failures}, and {@link TestRetryPlugin Test
65-
* Retry} plugins are applied
65+
* Checkstyle}, {@link TestFailuresPlugin Test Failures}, {@link TestRetryPlugin Test
66+
* Retry}, and {@link ArchitecturePlugin Architecture} plugins are applied
6667
* <li>{@link Test} tasks are configured:
6768
* <ul>
6869
* <li>to use JUnit Platform
@@ -107,6 +108,7 @@ class JavaConventions {
107108
void apply(Project project) {
108109
project.getPlugins().withType(JavaBasePlugin.class, (java) -> {
109110
project.getPlugins().apply(TestFailuresPlugin.class);
111+
project.getPlugins().apply(ArchitecturePlugin.class);
110112
configureSpringJavaFormat(project);
111113
configureJavaConventions(project);
112114
configureJavadocConventions(project);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
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+
* https://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+
17+
package org.springframework.boot.build.architecture;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.gradle.api.Plugin;
23+
import org.gradle.api.Project;
24+
import org.gradle.api.Task;
25+
import org.gradle.api.plugins.JavaBasePlugin;
26+
import org.gradle.api.plugins.JavaPluginExtension;
27+
import org.gradle.api.tasks.SourceSet;
28+
import org.gradle.api.tasks.TaskProvider;
29+
import org.gradle.language.base.plugins.LifecycleBasePlugin;
30+
31+
import org.springframework.util.StringUtils;
32+
33+
/**
34+
* {@link Plugin} for verifying a project's architecture.
35+
*
36+
* @author Andy Wilkinson
37+
*/
38+
public class ArchitecturePlugin implements Plugin<Project> {
39+
40+
@Override
41+
public void apply(Project project) {
42+
project.getPlugins().withType(JavaBasePlugin.class, (javaPlugin) -> registerTasks(project));
43+
}
44+
45+
private void registerTasks(Project project) {
46+
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
47+
List<TaskProvider<PackageTangleCheck>> packageTangleChecks = new ArrayList<>();
48+
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
49+
TaskProvider<PackageTangleCheck> checkPackageTangles = project.getTasks().register(
50+
"checkForPackageTangles" + StringUtils.capitalize(sourceSet.getName()), PackageTangleCheck.class,
51+
(task) -> {
52+
task.setClasses(sourceSet.getOutput().getClassesDirs());
53+
task.setDescription("Checks the classes of the " + sourceSet.getName()
54+
+ " source set for package tangles.");
55+
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
56+
});
57+
packageTangleChecks.add(checkPackageTangles);
58+
}
59+
if (!packageTangleChecks.isEmpty()) {
60+
TaskProvider<Task> checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME);
61+
checkTask.configure((check) -> check.dependsOn(packageTangleChecks));
62+
}
63+
}
64+
65+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
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+
* https://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+
17+
package org.springframework.boot.build.architecture;
18+
19+
import java.io.File;
20+
import java.io.FileWriter;
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.StandardOpenOption;
25+
import java.util.stream.Collectors;
26+
27+
import com.tngtech.archunit.core.domain.JavaClasses;
28+
import com.tngtech.archunit.core.importer.ClassFileImporter;
29+
import com.tngtech.archunit.lang.EvaluationResult;
30+
import com.tngtech.archunit.library.dependencies.SliceRule;
31+
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
32+
import org.gradle.api.DefaultTask;
33+
import org.gradle.api.GradleException;
34+
import org.gradle.api.Task;
35+
import org.gradle.api.file.DirectoryProperty;
36+
import org.gradle.api.file.FileCollection;
37+
import org.gradle.api.file.FileTree;
38+
import org.gradle.api.tasks.IgnoreEmptyDirectories;
39+
import org.gradle.api.tasks.InputFiles;
40+
import org.gradle.api.tasks.Internal;
41+
import org.gradle.api.tasks.OutputDirectory;
42+
import org.gradle.api.tasks.PathSensitive;
43+
import org.gradle.api.tasks.PathSensitivity;
44+
import org.gradle.api.tasks.SkipWhenEmpty;
45+
import org.gradle.api.tasks.TaskAction;
46+
47+
import org.springframework.util.FileCopyUtils;
48+
49+
/**
50+
* {@link Task} that checks for package tangles.
51+
*
52+
* @author Andy Wilkinson
53+
*/
54+
public abstract class PackageTangleCheck extends DefaultTask {
55+
56+
private FileCollection classes;
57+
58+
public PackageTangleCheck() {
59+
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
60+
}
61+
62+
@TaskAction
63+
void checkForPackageTangles() throws IOException {
64+
JavaClasses javaClasses = new ClassFileImporter()
65+
.importPaths(this.classes.getFiles().stream().map(File::toPath).collect(Collectors.toList()));
66+
SliceRule freeOfCycles = SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
67+
EvaluationResult result = freeOfCycles.evaluate(javaClasses);
68+
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
69+
outputFile.getParentFile().mkdirs();
70+
if (result.hasViolation()) {
71+
Files.write(outputFile.toPath(), result.getFailureReport().toString().getBytes(StandardCharsets.UTF_8),
72+
StandardOpenOption.CREATE);
73+
FileWriter writer = new FileWriter(outputFile);
74+
FileCopyUtils.copy(result.getFailureReport().toString(), writer);
75+
throw new GradleException("Package tangle check failed. See '" + outputFile + "' for details.");
76+
}
77+
else {
78+
outputFile.createNewFile();
79+
}
80+
}
81+
82+
public void setClasses(FileCollection classes) {
83+
this.classes = classes;
84+
}
85+
86+
@Internal
87+
public FileCollection getClasses() {
88+
return this.classes;
89+
}
90+
91+
@InputFiles
92+
@SkipWhenEmpty
93+
@IgnoreEmptyDirectories
94+
@PathSensitive(PathSensitivity.RELATIVE)
95+
final FileTree getInputClasses() {
96+
return this.classes.getAsFileTree();
97+
}
98+
99+
@OutputDirectory
100+
public abstract DirectoryProperty getOutputDirectory();
101+
102+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
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+
* https://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+
17+
package org.springframework.boot.build.architecture;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
22+
import org.gradle.api.GradleException;
23+
import org.gradle.api.Project;
24+
import org.gradle.testfixtures.ProjectBuilder;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.io.TempDir;
27+
28+
import org.springframework.core.io.Resource;
29+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
30+
import org.springframework.util.FileSystemUtils;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
34+
35+
/**
36+
* Tests for {@link PackageTangleCheck}.
37+
*
38+
* @author Andy Wilkinson
39+
*/
40+
class PackageTangleCheckTests {
41+
42+
@TempDir
43+
File temp;
44+
45+
@Test
46+
void whenPackagesAreTangledTaskFailsAndWritesAReport() throws Exception {
47+
prepareTask("tangled", (packageTangleCheck) -> {
48+
assertThatExceptionOfType(GradleException.class)
49+
.isThrownBy(() -> packageTangleCheck.checkForPackageTangles());
50+
assertThat(
51+
new File(packageTangleCheck.getProject().getBuildDir(), "checkForPackageTangles/failure-report.txt")
52+
.length()).isGreaterThan(0);
53+
});
54+
}
55+
56+
@Test
57+
void whenPackagesAreNotTangledTaskSucceedsAndWritesAnEmptyReport() throws Exception {
58+
prepareTask("untangled", (packageTangleCheck) -> {
59+
packageTangleCheck.checkForPackageTangles();
60+
assertThat(
61+
new File(packageTangleCheck.getProject().getBuildDir(), "checkForPackageTangles/failure-report.txt")
62+
.length()).isEqualTo(0);
63+
});
64+
}
65+
66+
private void prepareTask(String classes, Callback<PackageTangleCheck> callback) throws Exception {
67+
File projectDir = new File(this.temp, "project");
68+
projectDir.mkdirs();
69+
copyClasses(classes, projectDir);
70+
Project project = ProjectBuilder.builder().withProjectDir(projectDir).build();
71+
PackageTangleCheck packageTangleCheck = project.getTasks().create("checkForPackageTangles",
72+
PackageTangleCheck.class, (task) -> task.setClasses(project.files("classes")));
73+
callback.accept(packageTangleCheck);
74+
}
75+
76+
private void copyClasses(String name, File projectDir) throws IOException {
77+
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
78+
Resource root = resolver.getResource("classpath:org/springframework/boot/build/architecture/" + name);
79+
FileSystemUtils.copyRecursively(root.getFile(),
80+
new File(projectDir, "classes/org/springframework/boot/build/architecture/" + name));
81+
82+
}
83+
84+
private interface Callback<T> {
85+
86+
void accept(T item) throws Exception;
87+
88+
}
89+
90+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
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+
* https://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+
17+
package org.springframework.boot.build.architecture.tangled;
18+
19+
import org.springframework.boot.build.architecture.tangled.sub.TangledTwo;
20+
21+
public final class TangledOne {
22+
23+
public static final String ID = TangledTwo.class.getName() + "One";
24+
25+
private TangledOne() {
26+
27+
}
28+
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
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+
* https://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+
17+
package org.springframework.boot.build.architecture.tangled.sub;
18+
19+
import org.springframework.boot.build.architecture.tangled.TangledOne;
20+
21+
public final class TangledTwo {
22+
23+
public static final String ID = TangledOne.ID + "-Two";
24+
25+
private TangledTwo() {
26+
27+
}
28+
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
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+
* https://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+
17+
package org.springframework.boot.build.architecture.untangled;
18+
19+
import org.springframework.boot.build.architecture.untangled.sub.UntangledTwo;
20+
21+
public final class UntangledOne {
22+
23+
public static final String ID = UntangledTwo.class.getName() + "One";
24+
25+
private UntangledOne() {
26+
27+
}
28+
29+
}

0 commit comments

Comments
 (0)