Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public ArchitectureCheck() {
.classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONDITIONAL_ON_MISSING_BEAN))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation
.classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation
.classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE))));
getRules().addAll(whenMainSources(
() -> ArchitectureRules.configurationPropertiesBinding(ArchitectureCheckAnnotation.classFor(
getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public enum ArchitectureCheckAnnotation {
*/
CONFIGURATION_PROPERTIES,

/**
* Configuration properties source.
*/
CONFIGURATION_PROPERTIES_SOURCE,

/**
* Deprecated configuration property.
*/
Expand All @@ -56,6 +61,8 @@ public enum ArchitectureCheckAnnotation {
"org.springframework.boot.autoconfigure.condition.ConditionalOnClass", CONDITIONAL_ON_MISSING_BEAN.name(),
"org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean",
CONFIGURATION_PROPERTIES.name(), "org.springframework.boot.context.properties.ConfigurationProperties",
CONFIGURATION_PROPERTIES_SOURCE.name(),
"org.springframework.boot.context.properties.ConfigurationPropertiesSource",
DEPRECATED_CONFIGURATION_PROPERTY.name(),
"org.springframework.boot.context.properties.DeprecatedConfigurationProperty",
CONFIGURATION_PROPERTIES_BINDING.name(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaConstructor;
import com.tngtech.archunit.core.domain.JavaConstructorCall;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.domain.JavaFieldAccess;
import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType;
import com.tngtech.archunit.core.domain.JavaMember;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaModifier;
Expand Down Expand Up @@ -77,6 +80,7 @@
* @author Ngoc Nhan
* @author Moritz Halbritter
* @author Stefano Cordio
* @author Venkata Naga Sai Srikanth Gollapudi
*/
final class ArchitectureRules {

Expand Down Expand Up @@ -126,7 +130,11 @@ static List<ArchRule> conditionalOnMissingBean(String annotationClass) {

static List<ArchRule> configurationProperties(String annotationClass) {
return List.of(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass),
methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass));
methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass),
configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Set",
List.of("java.util.LinkedHashSet")),
configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Map",
List.of("java.util.LinkedHashMap", "java.util.EnumMap")));
}

static List<ArchRule> configurationPropertiesBinding(String annotationClass) {
Expand Down Expand Up @@ -428,6 +436,54 @@ private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(S
.allowEmptyShould(true);
}

private static ArchRule configurationPropertyOfTypeShouldUseImplementation(String annotationClass,
String propertyType, List<String> allowedImplementations) {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith(annotationClass)
.or(areNestedInClassAnnotatedWith(annotationClass))
.should(useAllowedImplementationForProperties(propertyType, allowedImplementations))
.allowEmptyShould(true);
}

private static ArchCondition<JavaClass> useAllowedImplementationForProperties(String propertyType,
List<String> allowedImplementations) {
return check(
"use %s for properties of type %s".formatted(String.join(" or ", allowedImplementations), propertyType),
(javaClass, events) -> javaClass.getFields()
.stream()
.filter((field) -> propertyType.equals(field.getRawType().getName()))
.forEach((field) -> checkPropertyImplementation(field, allowedImplementations, events)));
}

private static void checkPropertyImplementation(JavaField field, List<String> allowedImplementations,
ConditionEvents events) {
field.getAccessesToSelf()
.stream()
.filter((access) -> access.getAccessType() == AccessType.SET)
.flatMap((access) -> initializerConstructorCalls(access).stream())
.filter((call) -> !allowedImplementations.contains(call.getTargetOwner().getName()))
.forEach((call) -> addViolation(events, field,
"%s should be initialized with %s instead of %s".formatted(field.getDescription(),
String.join(" or ", allowedImplementations), call.getTargetOwner().getName())));
}

private static List<JavaConstructorCall> initializerConstructorCalls(JavaFieldAccess fieldAccess) {
return fieldAccess.getOrigin()
.getConstructorCallsFromSelf()
.stream()
.filter((call) -> call.getLineNumber() == fieldAccess.getLineNumber())
.toList();
}

private static DescribedPredicate<JavaClass> areNestedInClassAnnotatedWith(String annotationClass) {
return DescribedPredicate.describe("one of its inner class",
(javaClass) -> javaClass.getEnclosingClass()
.map((enclosing) -> enclosing.isAnnotatedWith(annotationClass)
|| areNestedInClassAnnotatedWith(annotationClass).test(enclosing))
.orElse(false));
}

private static ArchRule autoConfigurationClassesShouldBePublicAndFinal() {
return ArchRuleDefinition.classes()
.that(areRegularAutoConfiguration())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.springframework.boot.build.architecture.annotations.TestConditionalOnMissingBean;
import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesBinding;
import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource;
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileSystemUtils;
Expand All @@ -62,6 +63,7 @@
* @author Ivan Malutin
* @author Dmytro Nosan
* @author Stefano Cordio
* @author Venkata Naga Sai Srikanth Gollapudi
*/
class ArchitectureCheckTests {

Expand Down Expand Up @@ -331,6 +333,76 @@ void whenMethodLevelConfigurationPropertiesContainsOnlyValueShouldSucceedAndWrit
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesUsesHashMapShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashmap", "annotations");
buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap");
}

@Test
void whenConfigurationPropertiesSourceUsesHashMapShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashmap", "annotations");
buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap");
}

@Test
void whenConfigurationPropertiesUsesHashSetShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashset", "annotations");
buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet");
}

@Test
void whenConfigurationPropertiesSourceUsesHashSetShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashset", "annotations");
buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet");
}

@Test
void whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashmap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesSourceUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashmap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/enummap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesSourceUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/enummap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashset", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesSourceUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashset", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesBindingBeanMethodIsNotStaticShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/bindingnonstatic", "annotations");
Expand Down Expand Up @@ -568,6 +640,12 @@ GradleBuild withConfigurationPropertiesAnnotation() {
return this;
}

GradleBuild withConfigurationPropertiesSourceAnnotation() {
configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE.name(),
TestConfigurationPropertiesSource.class.getName());
return this;
}

GradleBuild withConfigurationPropertiesBindingAnnotation() {
configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING.name(),
TestConfigurationPropertiesBinding.class.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.architecture.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TestConfigurationPropertiesSource {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.architecture.configurationproperties.enummap;

import java.util.EnumMap;
import java.util.Map;

import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties;

/**
* Test {@link TestConfigurationProperties} using {@link EnumMap}.
*
* @author Venkata Naga Sai Srikanth Gollapudi
*/
@TestConfigurationProperties("testing")
public class ConfigurationPropertiesWithEnumMap {

private Map<Example, String> properties = new EnumMap<>(Example.class);

public Map<Example, String> getProperties() {
return this.properties;
}

public void setProperties(Map<Example, String> properties) {
this.properties = properties;
}

enum Example {

ONE

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.architecture.configurationproperties.hashmap;

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties;

/**
* Test {@link TestConfigurationProperties} using {@link HashMap}.
*
* @author Venkata Naga Sai Srikanth Gollapudi
*/
@TestConfigurationProperties("testing")
public class ConfigurationPropertiesWithHashMap {

private Map<String, String> properties = new HashMap<>();

public Map<String, String> getProperties() {
return this.properties;
}

public void setProperties(Map<String, String> properties) {
this.properties = properties;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2012-present the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.architecture.configurationproperties.hashset;

import java.util.HashSet;
import java.util.Set;

import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties;

/**
* Test {@link TestConfigurationProperties} using {@link HashSet}.
*
* @author Venkata Naga Sai Srikanth Gollapudi
*/
@TestConfigurationProperties("testing")
public class ConfigurationPropertiesWithHashSet {

private Set<String> items = new HashSet<>();

public Set<String> getItems() {
return this.items;
}

public void setItems(Set<String> items) {
this.items = items;
}

}
Loading
Loading