From 693d3bc20daa1c7ec77355827aea764663d3876f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20A=2E=20Alto=C3=A9=20Falqueto?= Date: Thu, 2 Jul 2026 13:34:11 -0300 Subject: [PATCH 1/4] Introduce SourceSet abstraction in the build system model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a SourceSet enum to the buildsystem package, modeling the main/test split of the standard directory layout shared by the supported build systems, alongside the existing DependencyScope classification. Co-Authored-By: Claude Fable 5 Signed-off-by: Denis A. Altoé Falqueto --- .../generator/buildsystem/SourceSet.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/SourceSet.java diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/SourceSet.java b/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/SourceSet.java new file mode 100644 index 0000000000..fce8efb9a4 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/SourceSet.java @@ -0,0 +1,51 @@ +/* + * 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 io.spring.initializr.generator.buildsystem; + +/** + * A source set of a generated project, following the standard directory layout of the + * supported build systems (for example {@code src/main} and {@code src/test}). + * + * @author Denis A. Altoé Falqueto + */ +public enum SourceSet { + + /** + * The main source set, packaged with the application. + */ + MAIN("main"), + + /** + * The test source set, only available to tests. + */ + TEST("test"); + + private final String directoryName; + + SourceSet(String directoryName) { + this.directoryName = directoryName; + } + + /** + * Returns the name of the directory of this source set, relative to {@code src}. + * @return the directory name + */ + public String getDirectoryName() { + return this.directoryName; + } + +} From f5ab6d22c64ac69b7d12adc8a5e618b7580dba47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20A=2E=20Alto=C3=A9=20Falqueto?= Date: Thu, 2 Jul 2026 13:34:33 -0300 Subject: [PATCH 2/4] Support source set and profile specific application properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApplicationProperties becomes a composite: the root instance keeps representing the main source set with the default profile, and a new section(SourceSet, String) method returns the properties of any other (source set, profile) pair, creating it on demand. Sections expose the exact same API as the root and cannot be nested. The properties and YAML contributors now share a new base class, AbstractApplicationPropertiesContributor, which writes one file per non-empty section following the src/{sourceSet}/resources/application[-{profile}] convention. The file for the main source set and default profile is still always written, preserving the existing behavior, and the public API is untouched. Compliance tests gain the (source set, profile) pair as an additional parameterized axis for both configuration file formats. Co-Authored-By: Claude Fable 5 Signed-off-by: Denis A. Altoé Falqueto --- ...tractApplicationPropertiesContributor.java | 90 +++++++++++++++++++ .../properties/ApplicationProperties.java | 77 ++++++++++++++++ .../ApplicationPropertiesContributor.java | 29 ++---- .../ApplicationYamlPropertiesContributor.java | 29 ++---- .../ApplicationPropertiesComplianceTests.java | 36 ++++++++ ...ApplicationPropertiesContributorTests.java | 52 +++++++++++ .../ApplicationPropertiesTests.java | 47 ++++++++++ 7 files changed, 314 insertions(+), 46 deletions(-) create mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/AbstractApplicationPropertiesContributor.java diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/AbstractApplicationPropertiesContributor.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/AbstractApplicationPropertiesContributor.java new file mode 100644 index 0000000000..3fb65ff808 --- /dev/null +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/AbstractApplicationPropertiesContributor.java @@ -0,0 +1,90 @@ +/* + * 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 io.spring.initializr.generator.spring.properties; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; + +import io.spring.initializr.generator.buildsystem.SourceSet; +import io.spring.initializr.generator.project.contributor.ProjectContributor; +import io.spring.initializr.generator.spring.properties.ApplicationProperties.SectionKey; +import org.jspecify.annotations.Nullable; + +/** + * Base {@link ProjectContributor} that contributes application configuration files to a + * project. A file is written per source set and Spring profile that has properties, + * following the {@code src/{sourceSet}/resources/application[-{profile}]} convention. The + * file for the main source set and the default profile is always written, even if it is + * empty. + * + * @author Denis A. Altoé Falqueto + * @see ApplicationProperties#section(SourceSet, String) + */ +public abstract class AbstractApplicationPropertiesContributor implements ProjectContributor { + + private final ApplicationProperties properties; + + private final String extension; + + protected AbstractApplicationPropertiesContributor(ApplicationProperties properties, String extension) { + this.properties = properties; + this.extension = extension; + } + + @Override + public void contribute(Path projectRoot) throws IOException { + writeSection(projectRoot, SourceSet.MAIN, null, this.properties); + for (Map.Entry entry : this.properties.getSections().entrySet()) { + ApplicationProperties section = entry.getValue(); + if (!section.isEmpty()) { + writeSection(projectRoot, entry.getKey().sourceSet(), entry.getKey().profile(), section); + } + } + } + + /** + * Writes the given properties using the given writer. + * @param properties the properties to write + * @param writer the writer to use + */ + protected abstract void write(ApplicationProperties properties, PrintWriter writer); + + private void writeSection(Path projectRoot, SourceSet sourceSet, @Nullable String profile, + ApplicationProperties properties) throws IOException { + Path output = projectRoot.resolve(resolveFileName(sourceSet, profile)); + if (!Files.exists(output)) { + Files.createDirectories(output.getParent()); + Files.createFile(output); + } + try (PrintWriter writer = new PrintWriter(Files.newOutputStream(output, StandardOpenOption.APPEND), false, + StandardCharsets.UTF_8)) { + write(properties, writer); + } + } + + private String resolveFileName(SourceSet sourceSet, @Nullable String profile) { + String profileSuffix = (profile != null) ? "-" + profile : ""; + return "src/%s/resources/application%s.%s".formatted(sourceSet.getDirectoryName(), profileSuffix, + this.extension); + } + +} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationProperties.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationProperties.java index 30d58f108b..e904ae9e26 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationProperties.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationProperties.java @@ -18,9 +18,12 @@ import java.io.PrintWriter; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import io.spring.initializr.generator.buildsystem.SourceSet; import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; @@ -28,6 +31,11 @@ /** * Application properties. + *

+ * Properties added directly to this instance belong to the main source set and the + * default profile, and are written to {@code src/main/resources}. Properties for another + * source set or Spring profile can be added to the {@link #section(SourceSet, String) + * matching section}. * * @author Moritz Halbritter * @author Rodrigo Mibielli Peixoto @@ -38,6 +46,57 @@ public class ApplicationProperties { private final Map properties = new HashMap<>(); + private final Map sections = new LinkedHashMap<>(); + + private final boolean root; + + /** + * Creates the application properties of the main source set and the default profile. + */ + public ApplicationProperties() { + this(true); + } + + private ApplicationProperties(boolean root) { + this.root = root; + } + + /** + * Returns the application properties for the given source set and profile, creating + * the section if necessary. Calling this method with {@link SourceSet#MAIN} and a + * {@code null} profile returns this instance. + * @param sourceSet the source set the properties belong to + * @param profile the Spring profile the properties belong to, or {@code null} for the + * default profile + * @return the application properties for the given source set and profile + * @throws IllegalStateException if called on a section rather than on the root + * application properties + */ + public ApplicationProperties section(SourceSet sourceSet, @Nullable String profile) { + Assert.notNull(sourceSet, "'sourceSet' must not be null"); + Assert.state(this.root, "Sections cannot be nested"); + if (profile != null) { + Assert.hasText(profile, "'profile' must not be empty"); + } + if (sourceSet == SourceSet.MAIN && profile == null) { + return this; + } + return this.sections.computeIfAbsent(new SectionKey(sourceSet, profile), + (key) -> new ApplicationProperties(false)); + } + + /** + * Returns the application properties for the given source set and the default + * profile, creating the section if necessary. + * @param sourceSet the source set the properties belong to + * @return the application properties for the given source set + * @throws IllegalStateException if called on a section rather than on the root + * application properties + */ + public ApplicationProperties section(SourceSet sourceSet) { + return section(sourceSet, null); + } + /** * Adds a new property. * @param key the key of the property @@ -198,4 +257,22 @@ private void add(String key, Object value) { this.properties.put(key, value); } + boolean isEmpty() { + return this.properties.isEmpty(); + } + + Map getSections() { + return Collections.unmodifiableMap(this.sections); + } + + /** + * The source set and profile a section of application properties belongs to. + * + * @param sourceSet the source set the properties belong to + * @param profile the Spring profile the properties belong to, or {@code null} for the + * default profile + */ + record SectionKey(SourceSet sourceSet, @Nullable String profile) { + } + } diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributor.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributor.java index e19a629438..57ef445855 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributor.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributor.java @@ -16,43 +16,26 @@ package io.spring.initializr.generator.spring.properties; -import java.io.IOException; import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import io.spring.initializr.generator.project.contributor.ProjectContributor; /** - * A {@link ProjectContributor} that contributes a {@code application.properties} file to - * a project. + * A {@link ProjectContributor} that contributes {@code application.properties} files to a + * project, one per source set and Spring profile that has properties. * * @author Stephane Nicoll * @author Moritz Halbritter */ -public class ApplicationPropertiesContributor implements ProjectContributor { - - private static final String FILE = "src/main/resources/application.properties"; - - private final ApplicationProperties properties; +public class ApplicationPropertiesContributor extends AbstractApplicationPropertiesContributor { public ApplicationPropertiesContributor(ApplicationProperties properties) { - this.properties = properties; + super(properties, "properties"); } @Override - public void contribute(Path projectRoot) throws IOException { - Path output = projectRoot.resolve(FILE); - if (!Files.exists(output)) { - Files.createDirectories(output.getParent()); - Files.createFile(output); - } - try (PrintWriter writer = new PrintWriter(Files.newOutputStream(output, StandardOpenOption.APPEND), false, - StandardCharsets.UTF_8)) { - this.properties.writeProperties(writer); - } + protected void write(ApplicationProperties properties, PrintWriter writer) { + properties.writeProperties(writer); } } diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributor.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributor.java index bfa40e8a26..73e57155f7 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributor.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributor.java @@ -16,42 +16,25 @@ package io.spring.initializr.generator.spring.properties; -import java.io.IOException; import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import io.spring.initializr.generator.project.contributor.ProjectContributor; /** - * A {@link ProjectContributor} that contributes a {@code application.yaml} file to a - * project. + * A {@link ProjectContributor} that contributes {@code application.yaml} files to a + * project, one per source set and Spring profile that has properties. * * @author Sijun Yang */ -public class ApplicationYamlPropertiesContributor implements ProjectContributor { - - private static final String FILE = "src/main/resources/application.yaml"; - - private final ApplicationProperties properties; +public class ApplicationYamlPropertiesContributor extends AbstractApplicationPropertiesContributor { public ApplicationYamlPropertiesContributor(ApplicationProperties properties) { - this.properties = properties; + super(properties, "yaml"); } @Override - public void contribute(Path projectRoot) throws IOException { - Path output = projectRoot.resolve(FILE); - if (!Files.exists(output)) { - Files.createDirectories(output.getParent()); - Files.createFile(output); - } - try (PrintWriter writer = new PrintWriter(Files.newOutputStream(output, StandardOpenOption.APPEND), false, - StandardCharsets.UTF_8)) { - this.properties.writeYaml(writer); - } + protected void write(ApplicationProperties properties, PrintWriter writer) { + properties.writeYaml(writer); } } diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java index 47c1e2f8ce..2a8af57fe9 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java @@ -19,6 +19,7 @@ import java.util.stream.Stream; import io.spring.initializr.generator.buildsystem.BuildSystem; +import io.spring.initializr.generator.buildsystem.SourceSet; import io.spring.initializr.generator.buildsystem.maven.MavenBuildSystem; import io.spring.initializr.generator.configuration.format.ConfigurationFileFormat; import io.spring.initializr.generator.configuration.format.properties.PropertiesFormat; @@ -27,6 +28,7 @@ import io.spring.initializr.generator.language.java.JavaLanguage; import io.spring.initializr.generator.spring.AbstractComplianceTests; import io.spring.initializr.generator.test.project.ProjectStructure; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -52,6 +54,15 @@ static Stream parameters() { Arguments.arguments(ConfigurationFileFormat.forId(YamlFormat.ID), "application.yaml")); } + static Stream sectionParameters() { + return parameters().flatMap((parameters) -> { + Object[] arguments = parameters.get(); + return Stream.of(Arguments.arguments(arguments[0], arguments[1], SourceSet.TEST, null), + Arguments.arguments(arguments[0], arguments[1], SourceSet.MAIN, "dev"), + Arguments.arguments(arguments[0], arguments[1], SourceSet.TEST, "integration")); + }); + } + @ParameterizedTest @MethodSource("parameters") void applicationPropertiesGenerated(ConfigurationFileFormat format, String fileName) { @@ -74,8 +85,33 @@ void applicationPropertiesWithCustomProperties(ConfigurationFileFormat format, S .hasSameContentAs(new ClassPathResource(path)); } + @ParameterizedTest + @MethodSource("sectionParameters") + void applicationPropertiesWithSectionProperties(ConfigurationFileFormat format, String fileName, + SourceSet sourceSet, @Nullable String profile) { + ProjectStructure project = generateProject(java, maven, "2.4.1", + (description) -> description.setConfigurationFileFormat(format), + (projectGenerationContext) -> projectGenerationContext.registerBean( + ApplicationPropertiesCustomizer.class, + () -> (properties) -> properties.section(sourceSet, profile) + .add("spring.application.name", "app-name"))); + String path = "project/properties/" + format + "/" + getAssertFileName(fileName); + assertThat(project).textFile(getSectionFilePath(fileName, sourceSet, profile)) + .as("Resource " + path) + .hasSameContentAs(new ClassPathResource(path)); + assertThat(project).filePaths().contains("src/main/resources/%s".formatted(fileName)); + } + private String getAssertFileName(String fileName) { return fileName + ".gen"; } + private String getSectionFilePath(String fileName, SourceSet sourceSet, @Nullable String profile) { + int extensionSeparator = fileName.lastIndexOf('.'); + String sectionFileName = (profile != null) + ? fileName.substring(0, extensionSeparator) + "-" + profile + fileName.substring(extensionSeparator) + : fileName; + return "src/%s/resources/%s".formatted(sourceSet.getDirectoryName(), sectionFileName); + } + } diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributorTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributorTests.java index 97f02698de..e034999ce8 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributorTests.java +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesContributorTests.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.file.Path; +import io.spring.initializr.generator.buildsystem.SourceSet; import io.spring.initializr.generator.test.project.ProjectStructure; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -55,4 +56,55 @@ void shouldAddStringProperty() throws IOException { .contains("spring.application.name=test"); } + @Test + void shouldWriteTestSourceSetPropertiesToTestResources() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.section(SourceSet.TEST, null).add("spring.datasource.url", "jdbc:h2:mem:test"); + new ApplicationPropertiesContributor(properties).contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile("src/test/resources/application.properties") + .lines() + .contains("spring.datasource.url=jdbc:h2:mem:test"); + } + + @Test + void shouldWriteProfilePropertiesToProfileSpecificFile() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.section(SourceSet.MAIN, "dev").add("logging.level.root", "DEBUG"); + new ApplicationPropertiesContributor(properties).contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application-dev.properties") + .lines() + .contains("logging.level.root=DEBUG"); + } + + @Test + void shouldWriteTestSourceSetProfileProperties() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.section(SourceSet.TEST, "integration").add("spring.application.name", "it"); + new ApplicationPropertiesContributor(properties).contribute(this.directory); + assertThat(new ProjectStructure(this.directory)) + .textFile("src/test/resources/application-integration.properties") + .lines() + .contains("spring.application.name=it"); + } + + @Test + void shouldNotWriteFileForEmptySection() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.section(SourceSet.TEST, null); + new ApplicationPropertiesContributor(properties).contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).filePaths() + .containsOnly("src/main/resources/application.properties"); + } + + @Test + void shouldAlwaysWriteMainDefaultFileEvenWhenOnlySectionsHaveProperties() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.section(SourceSet.MAIN, "dev").add("test", "value"); + new ApplicationPropertiesContributor(properties).contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).filePaths() + .containsOnly("src/main/resources/application.properties", "src/main/resources/application-dev.properties"); + assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application.properties") + .isEmpty(); + } + } diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesTests.java index 5b4e26c292..3a9b790f7f 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesTests.java +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesTests.java @@ -21,10 +21,12 @@ import java.util.Collections; import java.util.List; +import io.spring.initializr.generator.buildsystem.SourceSet; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** @@ -153,6 +155,51 @@ void booleanProperty() { assertThat(written).isEqualToIgnoringNewLines("test=false"); } + @Test + void sectionForMainSourceSetAndDefaultProfileReturnsRoot() { + ApplicationProperties properties = new ApplicationProperties(); + assertThat(properties.section(SourceSet.MAIN, null)).isSameAs(properties); + } + + @Test + void sectionReturnsSameInstanceForSameSourceSetAndProfile() { + ApplicationProperties properties = new ApplicationProperties(); + ApplicationProperties section = properties.section(SourceSet.TEST, "integration"); + assertThat(properties.section(SourceSet.TEST, "integration")).isSameAs(section); + } + + @Test + void sectionKeepsPropertiesIsolated() { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("test", "main-value"); + properties.section(SourceSet.TEST).add("test", "test-value"); + assertThat(properties.get("test")).isEqualTo("main-value"); + assertThat(properties.section(SourceSet.TEST).get("test")).isEqualTo("test-value"); + } + + @Test + void sectionsWithSameSourceSetAndDifferentProfilesAreIsolated() { + ApplicationProperties properties = new ApplicationProperties(); + properties.section(SourceSet.MAIN, "dev").add("test", "dev-value"); + properties.section(SourceSet.MAIN, "prod").add("test", "prod-value"); + assertThat(properties.section(SourceSet.MAIN, "dev").get("test")).isEqualTo("dev-value"); + assertThat(properties.section(SourceSet.MAIN, "prod").get("test")).isEqualTo("prod-value"); + } + + @Test + void sectionOfSectionThrows() { + ApplicationProperties properties = new ApplicationProperties(); + ApplicationProperties section = properties.section(SourceSet.TEST, null); + assertThatIllegalStateException().isThrownBy(() -> section.section(SourceSet.MAIN, "dev")) + .withMessage("Sections cannot be nested"); + } + + @Test + void sectionWithEmptyProfileThrows() { + ApplicationProperties properties = new ApplicationProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.section(SourceSet.MAIN, " ")); + } + @Test void shouldFailOnExistingProperty() { ApplicationProperties properties = new ApplicationProperties(); From 8f5bb82d9c1dfbe7574417ba0abc0191de1aaeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20A=2E=20Alto=C3=A9=20Falqueto?= Date: Thu, 2 Jul 2026 13:34:45 -0300 Subject: [PATCH 3/4] Document the ApplicationPropertiesCustomizer extension point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Describe in the configuration guide how application properties can be contributed to the generated project, including how to target another source set or Spring profile through sections, with a runnable example. Co-Authored-By: Claude Fable 5 Signed-off-by: Denis A. Altoé Falqueto --- .../src/main/asciidoc/configuration-guide.adoc | 14 ++++++++++++++ .../project/ProjectCustomizationExamples.java | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/initializr-docs/src/main/asciidoc/configuration-guide.adoc b/initializr-docs/src/main/asciidoc/configuration-guide.adoc index 5fe9adf6ab..31c4389203 100644 --- a/initializr-docs/src/main/asciidoc/configuration-guide.adoc +++ b/initializr-docs/src/main/asciidoc/configuration-guide.adoc @@ -189,6 +189,20 @@ requires you to add a certain plugin to the build, you can provide a `BuildCusto that adds the plugin and the customizer will be called according to the order specified on it. +Similarly, an `ApplicationPropertiesCustomizer` can be used to contribute properties to +the application configuration file of the generated project, that is +`application.properties` or `application.yaml` depending on the requested +`ConfigurationFileFormat`. Properties added directly to the `ApplicationProperties` +argument are written to the configuration file of the main source set and the default +profile. Properties for another source set or Spring profile can be added to the matching +`section`, and a `src/{sourceSet}/resources/application[-{profile}]` file is generated +for every section that contains properties, as shown in the following example: + +[source,java,indent=0] +---- +include::{code-examples}/doc/generator/project/ProjectCustomizationExamples.java[tag=application-properties-customizer] +---- + [[initializr-generator-spring-requirements]] diff --git a/initializr-docs/src/main/java/io/spring/initializr/doc/generator/project/ProjectCustomizationExamples.java b/initializr-docs/src/main/java/io/spring/initializr/doc/generator/project/ProjectCustomizationExamples.java index 2cc739bb66..f0447f6946 100644 --- a/initializr-docs/src/main/java/io/spring/initializr/doc/generator/project/ProjectCustomizationExamples.java +++ b/initializr-docs/src/main/java/io/spring/initializr/doc/generator/project/ProjectCustomizationExamples.java @@ -16,12 +16,14 @@ package io.spring.initializr.doc.generator.project; +import io.spring.initializr.generator.buildsystem.SourceSet; import io.spring.initializr.generator.buildsystem.gradle.GradleBuild; import io.spring.initializr.generator.buildsystem.gradle.GradleBuildSystem; import io.spring.initializr.generator.condition.ConditionalOnBuildSystem; import io.spring.initializr.generator.condition.ConditionalOnPackaging; import io.spring.initializr.generator.packaging.war.WarPackaging; import io.spring.initializr.generator.spring.build.BuildCustomizer; +import io.spring.initializr.generator.spring.properties.ApplicationPropertiesCustomizer; import org.springframework.context.annotation.Bean; @@ -41,4 +43,18 @@ public BuildCustomizer warPluginContributor() { } // end::war-plugin-contributor[] + // tag::application-properties-customizer[] + @Bean + public ApplicationPropertiesCustomizer applicationPropertiesContributor() { + return (properties) -> { + // src/main/resources/application.properties (or .yaml) + properties.add("spring.application.name", "acme"); + // src/test/resources/application.properties + properties.section(SourceSet.TEST).add("spring.datasource.url", "jdbc:h2:mem:acme"); + // src/main/resources/application-dev.properties + properties.section(SourceSet.MAIN, "dev").add("logging.level.root", "DEBUG"); + }; + } + // end::application-properties-customizer[] + } From f77586d4751464ca9438d6e6da3a823f61660d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20A=2E=20Alto=C3=A9=20Falqueto?= Date: Thu, 2 Jul 2026 14:28:11 -0300 Subject: [PATCH 4/4] Fix NullAway error in ApplicationPropertiesComplianceTests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare the local variable as @Nullable Object[] to match the JSpecify-annotated return type of Arguments.get() in recent JUnit versions. Co-Authored-By: Claude Fable 5 Signed-off-by: Denis A. Altoé Falqueto --- .../spring/properties/ApplicationPropertiesComplianceTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java index 2a8af57fe9..d7a239ad58 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesComplianceTests.java @@ -56,7 +56,7 @@ static Stream parameters() { static Stream sectionParameters() { return parameters().flatMap((parameters) -> { - Object[] arguments = parameters.get(); + @Nullable Object[] arguments = parameters.get(); return Stream.of(Arguments.arguments(arguments[0], arguments[1], SourceSet.TEST, null), Arguments.arguments(arguments[0], arguments[1], SourceSet.MAIN, "dev"), Arguments.arguments(arguments[0], arguments[1], SourceSet.TEST, "integration"));