Skip to content
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),
initializedConfigurationPropertiesShouldUse("java.util.Set", "java.util.LinkedHashSet",
annotationClass),
initializedConfigurationPropertiesShouldUse("java.util.Map",
List.of("java.util.LinkedHashMap", "java.util.EnumMap"), annotationClass));
}

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

private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType, String implementationType,
String annotationClass) {
return initializedConfigurationPropertiesShouldUse(propertyType, List.of(implementationType), annotationClass);
}

private static ArchRule initializedConfigurationPropertiesShouldUse(String propertyType,
List<String> implementationTypes, String annotationClass) {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith(annotationClass)
.or(areNestedInConfigurationPropertiesClasses(annotationClass))
.should(useImplementationForInitializedProperties(propertyType, implementationTypes))
.because("@ConfigurationProperties classes should preserve the property type's expected implementation")
.allowEmptyShould(true);
}

private static ArchCondition<JavaClass> useImplementationForInitializedProperties(String propertyType,
List<String> implementationTypes) {
String description = implementationTypes.stream().collect(Collectors.joining(" or "));
return check("use %s for initialized %s properties".formatted(description, propertyType),
(javaClass, events) -> javaClass.getFields()
.stream()
.filter((field) -> propertyType.equals(field.getRawType().getName()))
.forEach((field) -> checkPropertyInitializer(field, implementationTypes, events)));
}

private static void checkPropertyInitializer(JavaField field, List<String> implementationTypes,
ConditionEvents events) {
String description = implementationTypes.stream().collect(Collectors.joining(" or "));
field.getAccessesToSelf()
.stream()
.filter((access) -> access.getAccessType() == AccessType.SET)
.flatMap((access) -> initializerConstructorCalls(access).stream())
.filter((call) -> !implementationTypes.contains(call.getTargetOwner().getName()))
.forEach((call) -> addViolation(events, field, "%s should be initialized with %s instead of %s"
.formatted(field.getDescription(), description, 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> areNestedInConfigurationPropertiesClasses(String annotationClass) {
return DescribedPredicate.describe("are nested in @ConfigurationProperties",
(javaClass) -> javaClass.getEnclosingClass()
.map((enclosing) -> enclosing.isAnnotatedWith(annotationClass)
|| areNestedInConfigurationPropertiesClasses(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 @@ -62,6 +62,7 @@
* @author Ivan Malutin
* @author Dmytro Nosan
* @author Stefano Cordio
* @author Venkata Naga Sai Srikanth Gollapudi
*/
class ArchitectureCheckTests {

Expand Down Expand Up @@ -331,6 +332,41 @@ 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 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 whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashmap", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
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 whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashset", "annotations");
build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(),
Task.CHECK_ARCHITECTURE_MAIN);
}

@Test
void whenConfigurationPropertiesBindingBeanMethodIsNotStaticShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/bindingnonstatic", "annotations");
Expand Down
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;
}

}
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.linkedhashmap;

import java.util.LinkedHashMap;
import java.util.Map;

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

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

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

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.linkedhashset;

import java.util.LinkedHashSet;
import java.util.Set;

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

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

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

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

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@

package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.relaxedbinding.mapsfromenvironmentvariables;

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

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("my.props")
public class MyMapsProperties {

private final Map<String, String> values = new HashMap<>();
private final Map<String, String> values = new LinkedHashMap<>();

public Map<String, String> getValues() {
return this.values;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("my.props")
class MyMapsProperties {

val values: Map<String, String> = HashMap()
val values: Map<String, String> = LinkedHashMap()

}

Loading
Loading