diff --git a/initializr-docs/src/main/asciidoc/configuration-guide.adoc b/initializr-docs/src/main/asciidoc/configuration-guide.adoc index 79f0f46180..3acadd917b 100644 --- a/initializr-docs/src/main/asciidoc/configuration-guide.adoc +++ b/initializr-docs/src/main/asciidoc/configuration-guide.adoc @@ -215,6 +215,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 contribute properties to the +configuration files of the generated project, that is `application.properties` or +`application.yaml` depending on the requested `ConfigurationFileFormat`. Properties added +directly belong to the main source set and the default profile, `file` and `profile` +address the other source sets and Spring profiles. Each combination is written to ++src/{sourceSet}/resources/application[-{profile}]+, with the extension of the requested +format. Combinations without properties are skipped, except for the main source set and +the default profile, which is always written: + +[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 23bbbc42a4..90c0e53469 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,6 +16,7 @@ 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; @@ -25,6 +26,7 @@ import io.spring.initializr.generator.project.ProjectDescriptionCustomizer; import io.spring.initializr.generator.project.ProjectDescriptionField; import io.spring.initializr.generator.spring.build.BuildCustomizer; +import io.spring.initializr.generator.spring.properties.ApplicationPropertiesCustomizer; import org.springframework.context.annotation.Bean; @@ -32,6 +34,8 @@ * Examples of customizers. * * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Denis A. Altoé Falqueto */ public class ProjectCustomizationExamples { @@ -69,4 +73,18 @@ private boolean isBelowJava17(String jvmVersion) { } // end::jvm-version-change-reason[] + // tag::application-properties-customizer[] + @Bean + public ApplicationPropertiesCustomizer applicationPropertiesCustomizer() { + return (properties) -> { + // src/main/resources/application.properties (or .yaml) + properties.add("spring.application.name", "acme"); + // src/test/resources/application.properties (or .yaml) + properties.file(SourceSet.TEST).add("spring.datasource.url", "jdbc:h2:mem:acme"); + // src/main/resources/application-dev.properties (or .yaml) + properties.profile("dev").add("logging.level.root", "DEBUG"); + }; + } + // end::application-properties-customizer[] + } 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..99dc5c14db --- /dev/null +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/AbstractApplicationPropertiesContributor.java @@ -0,0 +1,118 @@ +/* + * 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.BuildSystem; +import io.spring.initializr.generator.buildsystem.SourceSet; +import io.spring.initializr.generator.configuration.format.ConfigurationFileFormat; +import io.spring.initializr.generator.language.Language; +import io.spring.initializr.generator.language.SourceStructure; +import io.spring.initializr.generator.project.ProjectDescription; +import io.spring.initializr.generator.project.contributor.ProjectContributor; + +import org.springframework.util.Assert; + +/** + * Base {@link ProjectContributor} that writes the application configuration files of a + * project, one per {@link ApplicationPropertiesFile} that has properties. The default + * file of the main source set is written even when empty. + * + * @author Denis A. Altoé Falqueto + * @author Moritz Halbritter + * @see ApplicationProperties + */ +abstract class AbstractApplicationPropertiesContributor implements ProjectContributor { + + private final ApplicationProperties properties; + + private final ProjectDescription description; + + private final ConfigurationFileFormat format; + + /** + * Creates a new instance. + * @param properties the application properties to contribute + * @param description the description of the project, used to resolve the source + * structures + * @param format the format of the written files, defining their extension + */ + protected AbstractApplicationPropertiesContributor(ApplicationProperties properties, ProjectDescription description, + ConfigurationFileFormat format) { + this.properties = properties; + this.description = description; + this.format = format; + } + + @Override + public void contribute(Path projectRoot) throws IOException { + BuildSystem buildSystem = this.description.getBuildSystem(); + Assert.state(buildSystem != null, "'buildSystem' must not be null"); + Language language = this.description.getLanguage(); + Assert.state(language != null, "'language' must not be null"); + for (SourceSet sourceSet : SourceSet.values()) { + contribute(buildSystem.getSource(projectRoot, language, sourceSet), sourceSet); + } + } + + private void contribute(SourceStructure sourceStructure, SourceSet sourceSet) throws IOException { + for (Map.Entry entry : this.properties.files(sourceSet).entrySet()) { + ProfileName profile = entry.getKey(); + ApplicationPropertiesFile file = entry.getValue(); + // Generated projects always ship a main application file, even an empty one. + // Every other file is written only if it has properties. + boolean mainDefault = (sourceSet == SourceSet.MAIN) && ProfileName.DEFAULT.equals(profile); + if (file.isEmpty() && !mainDefault) { + continue; + } + writeFile(sourceStructure, profile, file); + } + } + + private void writeFile(SourceStructure sourceStructure, ProfileName profile, ApplicationPropertiesFile file) + throws IOException { + Path output = resolveOutputFile(sourceStructure, 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)) { + writeProperties(file.properties(), writer); + } + } + + /** + * Writes the given properties in the format of this contributor. + * @param properties the properties to write, keyed by their dotted name + * @param writer the writer to use + */ + protected abstract void writeProperties(Map properties, PrintWriter writer); + + private Path resolveOutputFile(SourceStructure sourceStructure, ProfileName profile) { + String fileName = "application" + profile.fileSuffix() + this.format.fileExtension(); + return sourceStructure.getResourcesDirectory().resolve(fileName); + } + +} 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 ebc64f914c..bb3c13ce16 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 @@ -16,194 +16,190 @@ package io.spring.initializr.generator.spring.properties; -import java.io.PrintWriter; import java.util.Collection; 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; -import org.springframework.util.StringUtils; - /** - * Application properties. + * The application properties of a generated project, one + * {@link ApplicationPropertiesFile} per source set and Spring profile. + *

+ * Properties added directly belong to the main source set and the default profile, use + * {@link #profile(String)} or {@link #file(SourceSet)} for the others. + *

+ * Every file holding properties is written to + * {@code src/{sourceSet}/resources/application[-{profile}]}, resolved against the + * project's build system, with the extension of the requested + * {@link io.spring.initializr.generator.configuration.format.ConfigurationFileFormat}. + * The default file of the main source set is written even when empty. * * @author Moritz Halbritter * @author Rodrigo Mibielli Peixoto + * @author Denis A. Altoé Falqueto */ public class ApplicationProperties { - private static final String YAML_SPACE = " "; - - private final Map properties = new LinkedHashMap<>(); + private final Map files = createFiles(); /** - * Adds a new property. - * @param key the key of the property - * @param value the value of the property + * Adds a property to the main source set and the default profile. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists */ public void add(String key, long value) { - add(key, (Object) value); + main().add(key, value); } /** - * Adds a new property. - * @param key the key of the property - * @param value the value of the property + * Adds a property to the main source set and the default profile. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists */ public void add(String key, boolean value) { - add(key, (Object) value); + main().add(key, value); } /** - * Adds a new property. - * @param key the key of the property - * @param value the value of the property + * Adds a property to the main source set and the default profile. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists */ public void add(String key, double value) { - add(key, (Object) value); + main().add(key, value); } /** - * Adds a new property. - * @param key the key of the property - * @param value the value of the property + * Adds a property to the main source set and the default profile. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists */ public void add(String key, String value) { - add(key, (Object) value); + main().add(key, value); } /** - * Adds a new property. - * @param key the key of the property - * @param value the value of the property + * Adds a property to the main source set and the default profile. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists */ public void add(String key, Collection value) { - add(key, (Object) value); + main().add(key, value); } /** - * Tests if the specified key exists. - * @param key the key of the property + * Whether the given key exists in the main source set and the default profile. + * @param key the property key * @return true if the key exists */ public boolean contains(String key) { - return this.properties.containsKey(key); + return main().contains(key); } /** - * Returns the value cast to the class associated to the key. - * @param the type of the returned value - * @param key the associated key - * @param clazz the class or interface to cast the value - * @return the corresponding value cast or null if there is no mapping for the key - * @throws ClassCastException if the object is not null and is not assignable to the - * type T + * Returns the value of the given key in the main source set and the default profile, + * cast to the given type. + * @param the type of the value + * @param key the property key + * @param clazz the type of the value + * @return the value, or null if the key does not exist + * @throws ClassCastException if the value is not assignable to the type T */ public @Nullable T get(String key, Class clazz) { - return clazz.cast(get(key)); + return main().get(key, clazz); } /** - * Returns the value associated to the key. - * @param key the associated key - * @return the corresponding value or null if there is no mapping for the key + * Returns the value of the given key in the main source set and the default profile. + * @param key the property key + * @return the value, or null if the key does not exist */ public @Nullable Object get(String key) { - return this.properties.get(key); + return main().get(key); } /** - * Removes the key (and its corresponding value) if it exists. - * @param key the key that needs to be removed - * @return true if the key (and its corresponding value) has been removed + * Removes the given key and its value from the main source set and the default + * profile. + * @param key the property key + * @return true if the key has been removed */ public boolean remove(String key) { - return this.properties.remove(key) != null; - } - - void writeProperties(PrintWriter writer) { - for (Map.Entry entry : this.properties.entrySet()) { - Object value = (entry.getValue() instanceof Collection collection) - ? StringUtils.collectionToCommaDelimitedString(collection) : entry.getValue(); - writer.printf("%s=%s%n", entry.getKey(), value); - } - } - - void writeYaml(PrintWriter writer) { - Map nested = flattenToNestedMap(this.properties); - writeYamlRecursive(nested, writer, 0); + return main().remove(key); } - private static Map flattenToNestedMap(Map flatMap) { - Map nested = new LinkedHashMap<>(); - flatMap.forEach((key, value) -> { - String[] path = parseKeyPath(key); - insertValueAtPath(nested, path, value); - }); - return nested; + /** + * Returns the file of the default profile of the given source set, creating it if + * necessary. + * @param sourceSet the source set + * @return the file + */ + public ApplicationPropertiesFile file(SourceSet sourceSet) { + return file(sourceSet, ProfileName.DEFAULT); } - private static String[] parseKeyPath(String key) { - return key.split("\\."); + /** + * Returns the file of the given profile of the given source set, creating it if + * necessary. + * @param sourceSet the source set + * @param profile the Spring profile name + * @return the file + * @throws IllegalStateException if the profile name is invalid + */ + public ApplicationPropertiesFile file(SourceSet sourceSet, String profile) { + return file(sourceSet, new ProfileName(profile)); } - @SuppressWarnings("unchecked") - private static void insertValueAtPath(Map map, String[] path, Object value) { - Map current = map; - for (int i = 0; i < path.length - 1; i++) { - String segment = path[i]; - Object child = current.computeIfAbsent(segment, (k) -> new LinkedHashMap<>()); - Assert.state(child instanceof Map, () -> conflict(segment)); - current = (Map) child; - } - String leaf = path[path.length - 1]; - Assert.state(!(current.get(leaf) instanceof Map), () -> conflict(leaf)); - current.put(leaf, value); + /** + * Returns the file of the given profile of the main source set, creating it if + * necessary. Shortcut for {@code file(SourceSet.MAIN, profile)}. + * @param profile the Spring profile name + * @return the file + * @throws IllegalStateException if the profile name is invalid + */ + public ApplicationPropertiesFile profile(String profile) { + return file(SourceSet.MAIN, profile); } - private static String conflict(String key) { - return "Property '%s' can't be a value and a nested map at the same time".formatted(key); + Map files(SourceSet sourceSet) { + Map sourceSetFiles = new LinkedHashMap<>(); + this.files.forEach((key, file) -> { + if (key.sourceSet() != sourceSet) { + return; + } + sourceSetFiles.put(key.profile(), file); + }); + return sourceSetFiles; } - private static void writeYamlRecursive(Map map, PrintWriter writer, int indent) { - map.entrySet().forEach((entry) -> writeEntry(entry, writer, indent)); + private ApplicationPropertiesFile main() { + return file(SourceSet.MAIN); } - @SuppressWarnings("unchecked") - private static void writeEntry(Map.Entry entry, PrintWriter writer, int indent) { - String indentStr = YAML_SPACE.repeat(indent); - Object value = entry.getValue(); - - if (value instanceof Map nestedMap) { - writer.printf("%s%s:%n", indentStr, entry.getKey()); - writeYamlRecursive((Map) nestedMap, writer, indent + 1); - } - else { - if (value instanceof Collection collection) { - if (collection.isEmpty()) { - writer.printf("%s%s: []%n", indentStr, entry.getKey()); - } - else { - writer.printf("%s%s:%n", indentStr, entry.getKey()); - writeCollection(collection, writer, indent + 1); - } - } - else { - writer.printf("%s%s: %s%n", indentStr, entry.getKey(), value); - } - } + private ApplicationPropertiesFile file(SourceSet sourceSet, ProfileName profile) { + return this.files.computeIfAbsent(new Key(sourceSet, profile), (ignored) -> new ApplicationPropertiesFile()); } - private static void writeCollection(Collection collection, PrintWriter writer, int indent) { - String indentStr = YAML_SPACE.repeat(indent); - collection.forEach((element) -> writer.printf("%s- %s%n", indentStr, element)); + private static Map createFiles() { + Map files = new LinkedHashMap<>(); + files.put(new Key(SourceSet.MAIN, ProfileName.DEFAULT), new ApplicationPropertiesFile()); + return files; } - private void add(String key, Object value) { - Assert.state(!this.properties.containsKey(key), () -> "Property '%s' already exists".formatted(key)); - this.properties.put(key, value); + /** + * Identifies the file of a source set and profile combination. + * + * @param sourceSet the source set + * @param profile the profile + */ + private record Key(SourceSet sourceSet, ProfileName 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..5bdc3bc107 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,42 +16,44 @@ 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.Collection; +import java.util.Map; +import io.spring.initializr.generator.configuration.format.properties.PropertiesFormat; +import io.spring.initializr.generator.project.ProjectDescription; import io.spring.initializr.generator.project.contributor.ProjectContributor; +import org.springframework.util.StringUtils; + /** - * A {@link ProjectContributor} that contributes a {@code application.properties} file to - * a project. + * A {@link ProjectContributor} that contributes + * {@code application[-{profile}].properties} files to a project, one per source set and + * Spring profile that has properties. The {@code application.properties} of the main + * source set is contributed even when empty. * * @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 ApplicationPropertiesContributor(ApplicationProperties properties) { - this.properties = properties; +public class ApplicationPropertiesContributor extends AbstractApplicationPropertiesContributor { + + /** + * Creates a new instance. + * @param properties the application properties to contribute + * @param description the description of the project, used to resolve the source + * structures + */ + public ApplicationPropertiesContributor(ApplicationProperties properties, ProjectDescription description) { + super(properties, description, new PropertiesFormat()); } @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 writeProperties(Map properties, PrintWriter writer) { + for (Map.Entry entry : properties.entrySet()) { + // Collections are written as comma delimited values, for example 'a,b' + Object value = (entry.getValue() instanceof Collection collection) + ? StringUtils.collectionToCommaDelimitedString(collection) : entry.getValue(); + writer.printf("%s=%s%n", entry.getKey(), value); } } diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesFile.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesFile.java new file mode 100644 index 0000000000..166ec62b3c --- /dev/null +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesFile.java @@ -0,0 +1,142 @@ +/* + * 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.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * The properties written to a single {@code application[-{profile}]} file, keyed by their + * dotted name. The file extension depends on the requested + * {@link io.spring.initializr.generator.configuration.format.ConfigurationFileFormat}. + * + * @author Moritz Halbritter + * @author Rodrigo Mibielli Peixoto + */ +public class ApplicationPropertiesFile { + + private final Map properties = new LinkedHashMap<>(); + + /** + * Adds a property. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists + */ + public void add(String key, long value) { + add(key, (Object) value); + } + + /** + * Adds a property. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists + */ + public void add(String key, boolean value) { + add(key, (Object) value); + } + + /** + * Adds a property. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists + */ + public void add(String key, double value) { + add(key, (Object) value); + } + + /** + * Adds a property. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists + */ + public void add(String key, String value) { + add(key, (Object) value); + } + + /** + * Adds a property. + * @param key the property key + * @param value the property value + * @throws IllegalStateException if the key already exists + */ + public void add(String key, Collection value) { + add(key, (Object) value); + } + + /** + * Whether the given key exists. + * @param key the property key + * @return true if the key exists + */ + public boolean contains(String key) { + return this.properties.containsKey(key); + } + + /** + * Returns the value of the given key, cast to the given type. + * @param the type of the value + * @param key the property key + * @param clazz the type of the value + * @return the value, or null if the key does not exist + * @throws ClassCastException if the value is not assignable to the type T + */ + public @Nullable T get(String key, Class clazz) { + return clazz.cast(get(key)); + } + + /** + * Returns the value of the given key. + * @param key the property key + * @return the value, or null if the key does not exist + */ + public @Nullable Object get(String key) { + return this.properties.get(key); + } + + /** + * Removes the given key and its value. + * @param key the property key + * @return true if the key has been removed + */ + public boolean remove(String key) { + return this.properties.remove(key) != null; + } + + boolean isEmpty() { + return this.properties.isEmpty(); + } + + Map properties() { + return Collections.unmodifiableMap(this.properties); + } + + private void add(String key, Object value) { + Assert.state(!this.properties.containsKey(key), () -> "Property '%s' already exists".formatted(key)); + this.properties.put(key, value); + } + +} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesProjectGenerationConfiguration.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesProjectGenerationConfiguration.java index 86cbf83922..3c9ed6bd41 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesProjectGenerationConfiguration.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesProjectGenerationConfiguration.java @@ -19,6 +19,7 @@ import io.spring.initializr.generator.condition.ConditionalOnConfigurationFileFormat; import io.spring.initializr.generator.configuration.format.properties.PropertiesFormat; import io.spring.initializr.generator.configuration.format.yaml.YamlFormat; +import io.spring.initializr.generator.project.ProjectDescription; import io.spring.initializr.generator.project.ProjectGenerationConfiguration; import org.springframework.beans.factory.ObjectProvider; @@ -41,15 +42,16 @@ ApplicationProperties applicationProperties(ObjectProvider properties, PrintWriter writer) { + writeAll(toNestedMap(properties), writer, 0); + } + + /** + * Turns dotted property names into nested maps, for example {@code a.b=1} into + * {@code {a={b=1}}}. + * @param properties the properties to convert + * @return the nested properties + */ + private static Map toNestedMap(Map properties) { + // Keeps insertion order, so that the generated files are reproducible + Map nested = new LinkedHashMap<>(); + properties.forEach((key, value) -> insertValueAtPath(nested, key.split("\\."), value)); + return nested; + } + + @SuppressWarnings("unchecked") + private static void insertValueAtPath(Map map, String[] path, Object value) { + Map current = map; + for (int i = 0; i < path.length - 1; i++) { + String segment = path[i]; + Object child = current.computeIfAbsent(segment, (k) -> new LinkedHashMap<>()); + Assert.state(child instanceof Map, () -> conflict(segment)); + current = (Map) child; + } + String leaf = path[path.length - 1]; + Assert.state(!(current.get(leaf) instanceof Map), () -> conflict(leaf)); + current.put(leaf, value); + } + + private static String conflict(String key) { + return "Property '%s' can't be a value and a nested map at the same time".formatted(key); + } + + private static void writeAll(Map map, PrintWriter writer, int indent) { + map.entrySet().forEach((entry) -> writeEntry(entry, writer, indent)); + } + + @SuppressWarnings("unchecked") + private static void writeEntry(Map.Entry entry, PrintWriter writer, int indent) { + String indentStr = YAML_SPACE.repeat(indent); + Object value = entry.getValue(); + + if (value instanceof Map nestedMap) { + writer.printf("%s%s:%n", indentStr, entry.getKey()); + writeAll((Map) nestedMap, writer, indent + 1); + return; } - try (PrintWriter writer = new PrintWriter(Files.newOutputStream(output, StandardOpenOption.APPEND), false, - StandardCharsets.UTF_8)) { - this.properties.writeYaml(writer); + + if (value instanceof Collection collection) { + if (collection.isEmpty()) { + writer.printf("%s%s: []%n", indentStr, entry.getKey()); + return; + } + writer.printf("%s%s:%n", indentStr, entry.getKey()); + writeCollection(collection, writer, indent + 1); + return; } + + writer.printf("%s%s: %s%n", indentStr, entry.getKey(), value); + } + + private static void writeCollection(Collection collection, PrintWriter writer, int indent) { + String indentStr = YAML_SPACE.repeat(indent); + collection.forEach((element) -> writer.printf("%s- %s%n", indentStr, element)); } } diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ProfileName.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ProfileName.java new file mode 100644 index 0000000000..785b82b042 --- /dev/null +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/properties/ProfileName.java @@ -0,0 +1,114 @@ +/* + * 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.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A Spring profile name, validated with the same rules as Spring Boot's + * {@code ProfilesValidator}: letters, digits and {@value #ALLOWED_CHARS}, starting and + * ending with a letter or digit. + * + * @author Moritz Halbritter + * @see ProfilesValidator + */ +final class ProfileName { + + /** + * Characters allowed in a profile name in addition to letters and digits. + */ + private static final String ALLOWED_CHARS = "-_.+@"; + + /** + * {@link #ALLOWED_CHARS} as used in error messages, for example {@code '-', '_'}. + */ + private static final String ALLOWED_CHARS_MESSAGE = "'-', '_', '.', '+', '@'"; + + private static final String FILE_SUFFIX_SEPARATOR = "-"; + + /** + * Name of the profile which is active when no profile is set, used when printing + * {@link #DEFAULT}. + */ + private static final String DEFAULT_NAME = "default"; + + /** + * The default profile, which has no name and therefore no file name suffix. + */ + static final ProfileName DEFAULT = new ProfileName(); + + private final @Nullable String name; + + private ProfileName() { + this.name = null; + } + + ProfileName(String name) { + Assert.state(StringUtils.hasText(name), "'name' must not be empty"); + int[] codePoints = name.codePoints().toArray(); + for (int i = 0; i < codePoints.length; i++) { + boolean letterOrDigit = Character.isLetterOrDigit(codePoints[i]); + Assert.state(letterOrDigit || ALLOWED_CHARS.indexOf(codePoints[i]) != -1, + () -> "Profile '%s' must only contain letters, digits or allowed chars (%s)".formatted(name, + ALLOWED_CHARS_MESSAGE)); + // Only inner characters may be one of the allowed chars, which keeps the + // profile usable as a file name suffix (for example 'application-..' is + // rejected) + boolean innerChar = (i > 0) && (i < codePoints.length - 1); + Assert.state(letterOrDigit || innerChar, + () -> "Profile '%s' must start and end with a letter or digit".formatted(name)); + } + this.name = name; + } + + /** + * Returns the suffix this profile adds to the configuration file name, for example + * {@code -dev} for the {@code dev} profile and {@code ""} for {@link #DEFAULT}. + * @return the file name suffix + */ + String fileSuffix() { + return (this.name != null) ? FILE_SUFFIX_SEPARATOR + this.name : ""; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ProfileName other)) { + return false; + } + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.name); + } + + @Override + public String toString() { + return (this.name != null) ? this.name : DEFAULT_NAME; + } + +} 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..ac1ff5ef8b 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 @@ -16,9 +16,12 @@ package io.spring.initializr.generator.spring.properties; +import java.util.List; +import java.util.Map; 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; @@ -28,7 +31,6 @@ import io.spring.initializr.generator.spring.AbstractComplianceTests; import io.spring.initializr.generator.test.project.ProjectStructure; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.core.io.ClassPathResource; @@ -39,6 +41,7 @@ * Application properties compliance tests. * * @author Sijun Yang + * @author Moritz Halbritter */ class ApplicationPropertiesComplianceTests extends AbstractComplianceTests { @@ -46,36 +49,62 @@ class ApplicationPropertiesComplianceTests extends AbstractComplianceTests { private static final Language java = new JavaLanguage(); - static Stream parameters() { - return Stream.of( - Arguments.arguments(ConfigurationFileFormat.forId(PropertiesFormat.ID), "application.properties"), - Arguments.arguments(ConfigurationFileFormat.forId(YamlFormat.ID), "application.yaml")); + /** + * The content written for {@code spring.application.name=app-name}, keyed by format + * id. + */ + private static final Map> EXPECTED_CONTENT = Map.of(PropertiesFormat.ID, + List.of("spring.application.name=app-name"), YamlFormat.ID, + List.of("spring:", " application:", " name: app-name")); + + static Stream formats() { + return Stream.of(ConfigurationFileFormat.forId(PropertiesFormat.ID), + ConfigurationFileFormat.forId(YamlFormat.ID)); } @ParameterizedTest - @MethodSource("parameters") - void applicationPropertiesGenerated(ConfigurationFileFormat format, String fileName) { + @MethodSource("formats") + void shouldGenerateApplicationProperties(ConfigurationFileFormat format) { ProjectStructure project = generateProject(java, maven, "2.4.1", (description) -> description.setConfigurationFileFormat(format)); - assertThat(project).filePaths().contains("src/main/resources/%s".formatted(fileName)); + assertThat(project).filePaths().contains(mainFile(format)); } @ParameterizedTest - @MethodSource("parameters") - void applicationPropertiesWithCustomProperties(ConfigurationFileFormat format, String fileName) { + @MethodSource("formats") + void shouldWriteCustomProperties(ConfigurationFileFormat format) { ProjectStructure project = generateProject(java, maven, "2.4.1", (description) -> description.setConfigurationFileFormat(format), (projectGenerationContext) -> projectGenerationContext.registerBean( ApplicationPropertiesCustomizer.class, () -> (properties) -> properties.add("spring.application.name", "app-name"))); - String path = "project/properties/" + format + "/" + getAssertFileName(fileName); - assertThat(project).textFile("src/main/resources/%s".formatted(fileName)) + String path = "project/properties/%s/application%s.gen".formatted(format, format.fileExtension()); + assertThat(project).textFile(mainFile(format)) .as("Resource " + path) .hasSameContentAs(new ClassPathResource(path)); } - private String getAssertFileName(String fileName) { - return fileName + ".gen"; + @ParameterizedTest + @MethodSource("formats") + void shouldWriteSourceSetAndProfileFiles(ConfigurationFileFormat format) { + ProjectStructure project = generateProject(java, maven, "2.4.1", + (description) -> description.setConfigurationFileFormat(format), + (projectGenerationContext) -> projectGenerationContext + .registerBean(ApplicationPropertiesCustomizer.class, () -> (properties) -> { + properties.file(SourceSet.TEST).add("spring.application.name", "app-name"); + properties.profile("dev").add("spring.application.name", "app-name"); + })); + List expectedContent = EXPECTED_CONTENT.get(format.id()); + assertThat(project).textFile("src/test/resources/application%s".formatted(format.fileExtension())) + .lines() + .containsExactlyElementsOf(expectedContent); + assertThat(project).textFile("src/main/resources/application-dev%s".formatted(format.fileExtension())) + .lines() + .containsExactlyElementsOf(expectedContent); + } + + private String mainFile(ConfigurationFileFormat format) { + return "src/main/resources/application%s".formatted(format.fileExtension()); } } 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..9b0b227a01 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 @@ -18,10 +18,21 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import io.spring.initializr.generator.buildsystem.BuildSystem; +import io.spring.initializr.generator.buildsystem.SourceSet; +import io.spring.initializr.generator.language.Language; +import io.spring.initializr.generator.language.SourceStructure; +import io.spring.initializr.generator.language.java.JavaLanguage; +import io.spring.initializr.generator.project.MutableProjectDescription; import io.spring.initializr.generator.test.project.ProjectStructure; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; @@ -38,21 +49,141 @@ class ApplicationPropertiesContributorTests { Path directory; @Test - void applicationConfigurationWithDefaultSettings() throws IOException { - new ApplicationPropertiesContributor(new ApplicationProperties()).contribute(this.directory); + void shouldWriteEmptyMainFileByDefault() throws IOException { + new ApplicationPropertiesContributor(new ApplicationProperties(), TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + ProjectStructure project = new ProjectStructure(this.directory); + assertThat(project).filePaths().containsOnly("src/main/resources/application.properties"); + assertThat(project).textFile("src/main/resources/application.properties").isEmpty(); + } + + @Test + void shouldWriteAllValueTypes() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("string", "value"); + properties.add("long", 1L); + properties.add("double", 0.1); + properties.add("boolean", false); + properties.add("collection", List.of("value1", "value2")); + properties.add("empty-collection", Collections.emptyList()); + new ApplicationPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application.properties") - .isEmpty(); + .lines() + .containsExactly("string=value", "long=1", "double=0.1", "boolean=false", "collection=value1,value2", + "empty-collection="); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + MAIN, , src/main/resources/application.properties + MAIN, dev, src/main/resources/application-dev.properties + TEST, , src/test/resources/application.properties + TEST, integration, src/test/resources/application-integration.properties + """) + void shouldWriteProfileToMatchingFile(SourceSet sourceSet, @Nullable String profile, String expectedFile) + throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + file(properties, sourceSet, profile).add("spring.application.name", "test"); + new ApplicationPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile(expectedFile) + .lines() + .containsExactly("spring.application.name=test"); } @Test - void shouldAddStringProperty() throws IOException { + void shouldKeepProfilesWithSameKeyIsolated() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("spring.application.name", "main"); + properties.profile("dev").add("spring.application.name", "dev"); + new ApplicationPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + ProjectStructure project = new ProjectStructure(this.directory); + assertThat(project).textFile("src/main/resources/application.properties") + .lines() + .containsExactly("spring.application.name=main"); + assertThat(project).textFile("src/main/resources/application-dev.properties") + .lines() + .containsExactly("spring.application.name=dev"); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + TEST, + MAIN, dev + TEST, integration + """) + void shouldNotWriteFileWithoutProperties(SourceSet sourceSet, @Nullable String profile) throws IOException { ApplicationProperties properties = new ApplicationProperties(); - properties.add("spring.application.name", "test"); - ApplicationPropertiesContributor contributor = new ApplicationPropertiesContributor(properties); - contributor.contribute(this.directory); + file(properties, sourceSet, profile); + new ApplicationPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).filePaths() + .containsOnly("src/main/resources/application.properties"); + } + + @Test + void shouldAlwaysWriteMainDefaultFileEvenWhenOnlyProfilesHaveProperties() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.profile("dev").add("test", "value"); + new ApplicationPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .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(); + } + + @Test + void shouldResolvePathsThroughBuildSystem() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("spring.application.name", "main"); + properties.file(SourceSet.TEST).add("spring.application.name", "test"); + MutableProjectDescription description = new MutableProjectDescription(); + description.setBuildSystem(new CustomLayoutBuildSystem()); + description.setLanguage(new JavaLanguage()); + new ApplicationPropertiesContributor(properties, description).contribute(this.directory); + ProjectStructure project = new ProjectStructure(this.directory); + assertThat(project).filePaths() + .containsOnly("custom-main/resources/application.properties", + "custom-test/resources/application.properties"); + assertThat(project).textFile("custom-main/resources/application.properties") + .lines() + .containsExactly("spring.application.name=main"); + assertThat(project).textFile("custom-test/resources/application.properties") .lines() - .contains("spring.application.name=test"); + .containsExactly("spring.application.name=test"); + } + + private static ApplicationPropertiesFile file(ApplicationProperties properties, SourceSet sourceSet, + @Nullable String profile) { + return (profile != null) ? properties.file(sourceSet, profile) : properties.file(sourceSet); + } + + /** + * A {@link BuildSystem} with a non-standard layout, used to verify that the + * contributor resolves paths through {@link BuildSystem#getMainSource} and + * {@link BuildSystem#getTestSource} rather than hardcoding {@code src/main} and + * {@code src/test}. + */ + private static final class CustomLayoutBuildSystem implements BuildSystem { + + @Override + public String id() { + return "custom"; + } + + @Override + public SourceStructure getMainSource(Path projectRoot, Language language) { + return new SourceStructure(projectRoot.resolve("custom-main"), language); + } + + @Override + public SourceStructure getTestSource(Path projectRoot, Language language) { + return new SourceStructure(projectRoot.resolve("custom-test"), language); + } + } } diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesFileTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesFileTests.java new file mode 100644 index 0000000000..f8f6c147d7 --- /dev/null +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationPropertiesFileTests.java @@ -0,0 +1,110 @@ +/* + * 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 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.assertThatIllegalStateException; + +/** + * Tests for {@link ApplicationPropertiesFile}. + * + * @author Moritz Halbritter + * @author Rodrigo Mibielli Peixoto + */ +class ApplicationPropertiesFileTests { + + @Test + void shouldGetExistingKey() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", "123"); + Object value = file.get("test"); + assertThat(value).isEqualTo("123"); + } + + @Test + void shouldGetNullForMissingKey() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", "123"); + Object value = file.get("test2"); + assertThat(value).isNull(); + } + + @Test + void shouldGetExistingKeyWithCast() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", 123L); + Long value = file.get("test", Long.class); + assertThat(value).isEqualTo(123L); + } + + @Test + void shouldGetNullForMissingKeyWithCast() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", 123.4); + Double value = file.get("test2", Double.class); + assertThat(value).isNull(); + } + + @Test + void shouldFailOnWrongCast() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", 123L); + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> file.get("test", Integer.class)); + } + + @Test + void shouldContainExistingKey() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", "value"); + assertThat(file.contains("test")).isTrue(); + } + + @Test + void shouldNotContainMissingKey() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", "value"); + assertThat(file.contains("not-found")).isFalse(); + } + + @Test + void shouldRemoveExistingKey() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", "value"); + assertThat(file.remove("test")).isTrue(); + assertThat(file.contains("test")).isFalse(); + } + + @Test + void shouldNotRemoveMissingKey() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", "value"); + assertThat(file.remove("not-found")).isFalse(); + assertThat(file.contains("test")).isTrue(); + } + + @Test + void shouldFailOnExistingProperty() { + ApplicationPropertiesFile file = new ApplicationPropertiesFile(); + file.add("test", 1); + assertThatIllegalStateException().isThrownBy(() -> file.add("test", 2)) + .withMessage("Property 'test' already exists"); + } + +} 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 9131f484d8..6c428c52a8 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 @@ -16,232 +16,85 @@ package io.spring.initializr.generator.spring.properties; -import java.io.PrintWriter; -import java.io.StringWriter; -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.assertThatIllegalStateException; /** * Tests for {@link ApplicationProperties}. * * @author Moritz Halbritter - * @author Rodrigo Mibielli Peixoto */ class ApplicationPropertiesTests { @Test - void getKeyFound() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", "123"); - Object value = properties.get("test"); - assertThat(value).isEqualTo("123"); - } - - @Test - void getKeyNotFound() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", "123"); - Object value = properties.get("test2"); - assertThat(value).isNull(); - } - - @Test - void getKeyFoundWithCast() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", 123L); - Long value = properties.get("test", Long.class); - assertThat(value).isEqualTo(123L); - } - - @Test - void getKeyNotFoundWithCast() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", 123.4); - Double value = properties.get("test2", Double.class); - assertThat(value).isNull(); - } - - @Test - void getThrowsCastException() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", 123L); - assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> properties.get("test", Integer.class)); - } - - @Test - void containsKey() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", "value"); - assertThat(properties.contains("test")).isTrue(); - } - - @Test - void doesNotContainKey() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", "value"); - assertThat(properties.contains("not-found")).isFalse(); - } - - @Test - void removesKeyFound() { + void shouldSeePropertyAddedViaMainSourceSet() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", "value"); - assertThat(properties.remove("test")).isTrue(); - assertThat(properties.contains("test")).isFalse(); + properties.file(SourceSet.MAIN).add("test", "value"); + assertThat(properties.get("test")).isEqualTo("value"); } @Test - void doesNotRemoveKeyNotFound() { + void shouldSeePropertyAddedViaRootInMainSourceSet() { ApplicationProperties properties = new ApplicationProperties(); properties.add("test", "value"); - assertThat(properties.remove("not-found")).isFalse(); - assertThat(properties.contains("test")).isTrue(); - } - - @Test - void stringProperty() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", "string"); - String written = writeProperties(properties); - assertThat(written).isEqualToIgnoringNewLines("test=string"); - } - - @Test - void collectionProperty() { - ApplicationProperties properties = new ApplicationProperties(); - List strings = List.of("string1", "string2"); - properties.add("test", strings); - String written = writeProperties(properties); - assertThat(written).isEqualToIgnoringNewLines("test=string1,string2"); - } - - @Test - void emptyCollectionProperty() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", Collections.emptyList()); - String written = writeProperties(properties); - assertThat(written).isEqualToIgnoringNewLines("test="); - } - - @Test - void longProperty() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", 1); - String written = writeProperties(properties); - assertThat(written).isEqualToIgnoringNewLines("test=1"); - } - - @Test - void doubleProperty() { - ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", 0.1); - String written = writeProperties(properties); - assertThat(written).isEqualToIgnoringNewLines("test=0.1"); + assertThat(properties.file(SourceSet.MAIN).get("test")).isEqualTo("value"); } @Test - void booleanProperty() { + void shouldUseMainSourceSetForProfile() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", false); - String written = writeProperties(properties); - assertThat(written).isEqualToIgnoringNewLines("test=false"); + assertThat(properties.profile("dev")).isSameAs(properties.file(SourceSet.MAIN, "dev")); } @Test - void shouldFailOnExistingProperty() { + void shouldReturnSameFileForSameSourceSetAndProfile() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("test", 1); - assertThatIllegalStateException().isThrownBy(() -> properties.add("test", 2)) - .withMessage("Property 'test' already exists"); + assertThat(properties.file(SourceSet.TEST)).isSameAs(properties.file(SourceSet.TEST)); + assertThat(properties.file(SourceSet.TEST, "dev")).isSameAs(properties.file(SourceSet.TEST, "dev")); } @Test - void writeYaml() { + void shouldKeepFilesIsolated() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("name", "testapp"); - properties.add("port", 8080); - properties.add("app.version", "1.0"); - properties.add("db.host", "localhost"); - properties.add("app.config.debug", true); - properties.add("db.connection.timeout", 30); - String written = writeYaml(properties); - assertThat(written).isEqualToNormalizingNewlines(""" - name: testapp - port: 8080 - app: - version: 1.0 - config: - debug: true - db: - host: localhost - connection: - timeout: 30 - """); + properties.add("test", "main-value"); + properties.file(SourceSet.TEST).add("test", "test-value"); + properties.profile("dev").add("test", "dev-value"); + assertThat(properties.get("test")).isEqualTo("main-value"); + assertThat(properties.file(SourceSet.TEST).get("test")).isEqualTo("test-value"); + assertThat(properties.profile("dev").get("test")).isEqualTo("dev-value"); } @Test - void writeYamlCollection() { + void shouldAlwaysHaveMainDefaultFile() { ApplicationProperties properties = new ApplicationProperties(); - List ints = List.of(1, 2); - properties.add("test.sub", ints); - String written = writeYaml(properties); - assertThat(written).isEqualToNormalizingNewlines(""" - test: - sub: - - 1 - - 2 - """); + assertThat(properties.files(SourceSet.MAIN).keySet()).containsExactly(ProfileName.DEFAULT); } @Test - void writeEmptyYamlCollection() { + void shouldReturnFilesInAdditionOrder() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("test.sub", Collections.emptyList()); - String written = writeYaml(properties); - assertThat(written).isEqualToNormalizingNewlines(""" - test: - sub: [] - """); + properties.profile("dev"); + properties.profile("prod"); + assertThat(properties.files(SourceSet.MAIN).keySet()).containsExactly(ProfileName.DEFAULT, + new ProfileName("dev"), new ProfileName("prod")); } @Test - void writeYamlFailsOnValueAndNestedMapForSameKey() { + void shouldRejectProfileEscapingTheResourcesDirectory() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("app", "value"); - properties.add("app.name", "nested"); - assertThatIllegalStateException().isThrownBy(() -> writeYaml(properties)) - .withMessage("Property 'app' can't be a value and a nested map at the same time"); + assertThatIllegalStateException().isThrownBy(() -> properties.file(SourceSet.TEST, "../../etc")) + .withMessageContaining("must start and end with a letter or digit"); + assertThatIllegalStateException().isThrownBy(() -> properties.profile("../../etc")) + .withMessageContaining("must start and end with a letter or digit"); } @Test - void writeYamlFailsOnNestedMapAndValueForSameKey() { + void shouldReturnNoFilesForUnusedSourceSet() { ApplicationProperties properties = new ApplicationProperties(); - properties.add("app.name", "nested"); - properties.add("app", "value"); - assertThatIllegalStateException().isThrownBy(() -> writeYaml(properties)) - .withMessage("Property 'app' can't be a value and a nested map at the same time"); - } - - private String writeProperties(ApplicationProperties properties) { - StringWriter stringWriter = new StringWriter(); - try (PrintWriter writer = new PrintWriter(stringWriter)) { - properties.writeProperties(writer); - } - return stringWriter.toString(); - } - - private String writeYaml(ApplicationProperties properties) { - StringWriter stringWriter = new StringWriter(); - try (PrintWriter writer = new PrintWriter(stringWriter)) { - properties.writeYaml(writer); - } - return stringWriter.toString(); + assertThat(properties.files(SourceSet.TEST)).isEmpty(); } } diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributorTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributorTests.java new file mode 100644 index 0000000000..cff918e6d6 --- /dev/null +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ApplicationYamlPropertiesContributorTests.java @@ -0,0 +1,125 @@ +/* + * 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.nio.file.Path; +import java.util.Collections; +import java.util.List; + +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ApplicationYamlPropertiesContributor}. Profile to file mapping is + * covered by {@link ApplicationPropertiesContributorTests}, this class only covers the + * YAML specifics. + * + * @author Sijun Yang + * @author Moritz Halbritter + */ +class ApplicationYamlPropertiesContributorTests { + + @TempDir + @SuppressWarnings("NullAway.Init") + Path directory; + + @Test + void shouldWriteEmptyMainFileByDefault() throws IOException { + new ApplicationYamlPropertiesContributor(new ApplicationProperties(), TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application.yaml").isEmpty(); + } + + @Test + void shouldWriteStringProperty() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("spring.application.name", "test"); + new ApplicationYamlPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application.yaml") + .lines() + .containsExactly("spring:", " application:", " name: test"); + } + + @Test + void shouldNestDottedKeysKeepingInsertionOrder() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("name", "testapp"); + properties.add("app.version", "1.0"); + properties.add("db.host", "localhost"); + properties.add("app.config.debug", true); + new ApplicationYamlPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + // Nested keys are grouped under their first occurrence + assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application.yaml") + .lines() + .containsExactly("name: testapp", "app:", " version: 1.0", " config:", " debug: true", "db:", + " host: localhost"); + } + + @Test + void shouldWriteCollections() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("test.values", List.of(1, 2)); + properties.add("test.empty", Collections.emptyList()); + new ApplicationYamlPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile("src/main/resources/application.yaml") + .lines() + .containsExactly("test:", " values:", " - 1", " - 2", " empty: []"); + } + + @Test + void shouldFailOnValueAndNestedMapForSameKey() { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("app", "value"); + properties.add("app.name", "nested"); + ApplicationYamlPropertiesContributor contributor = new ApplicationYamlPropertiesContributor(properties, + TestProjectDescriptions.mavenJava()); + assertThatIllegalStateException().isThrownBy(() -> contributor.contribute(this.directory)) + .withMessage("Property 'app' can't be a value and a nested map at the same time"); + } + + @Test + void shouldFailOnNestedMapAndValueForSameKey() { + ApplicationProperties properties = new ApplicationProperties(); + properties.add("app.name", "nested"); + properties.add("app", "value"); + ApplicationYamlPropertiesContributor contributor = new ApplicationYamlPropertiesContributor(properties, + TestProjectDescriptions.mavenJava()); + assertThatIllegalStateException().isThrownBy(() -> contributor.contribute(this.directory)) + .withMessage("Property 'app' can't be a value and a nested map at the same time"); + } + + @Test + void shouldUseYamlExtensionForProfileFiles() throws IOException { + ApplicationProperties properties = new ApplicationProperties(); + properties.file(SourceSet.TEST, "integration").add("spring.application.name", "it"); + new ApplicationYamlPropertiesContributor(properties, TestProjectDescriptions.mavenJava()) + .contribute(this.directory); + assertThat(new ProjectStructure(this.directory)).textFile("src/test/resources/application-integration.yaml") + .lines() + .containsExactly("spring:", " application:", " name: it"); + } + +} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ProfileNameTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ProfileNameTests.java new file mode 100644 index 0000000000..cea0bf9097 --- /dev/null +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/ProfileNameTests.java @@ -0,0 +1,84 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ProfileName}. + * + * @author Moritz Halbritter + */ +class ProfileNameTests { + + @ParameterizedTest + @ValueSource(strings = { "dev", "integration-test_1.2+a@b", "a", "default" }) + void shouldAcceptValidProfile(String name) { + assertThat(new ProfileName(name)).hasToString(name); + } + + @ParameterizedTest + @ValueSource(strings = { " ", "" }) + void shouldRejectEmptyProfile(String name) { + assertThatIllegalStateException().isThrownBy(() -> new ProfileName(name)) + .withMessage("'name' must not be empty"); + } + + @ParameterizedTest + @ValueSource(strings = { "a/b", "a\\b", " dev ", "a b", "a\uD83D\uDE00b" }) + void shouldRejectDisallowedChars(String name) { + assertThatIllegalStateException().isThrownBy(() -> new ProfileName(name)) + .withMessage("Profile '%s' must only contain letters, digits or allowed chars ('-', '_', '.', '+', '@')" + .formatted(name)); + } + + @ParameterizedTest + @ValueSource(strings = { "..", "-dev", "dev-", "." }) + void shouldRejectProfileNotStartingOrEndingWithLetterOrDigit(String name) { + assertThatIllegalStateException().isThrownBy(() -> new ProfileName(name)) + .withMessage("Profile '%s' must start and end with a letter or digit".formatted(name)); + } + + @Test + void shouldUseDashPrefixedNameAsFileSuffix() { + assertThat(new ProfileName("dev").fileSuffix()).isEqualTo("-dev"); + } + + @Test + void shouldHaveNoFileSuffixForDefaultProfile() { + assertThat(ProfileName.DEFAULT.fileSuffix()).isEmpty(); + } + + @Test + void shouldBeEqualForSameName() { + assertThat(new ProfileName("dev")).isEqualTo(new ProfileName("dev")) + .hasSameHashCodeAs(new ProfileName("dev")) + .isNotEqualTo(new ProfileName("prod")) + .isNotEqualTo(ProfileName.DEFAULT); + } + + @Test + void shouldUseDefaultAsToStringForDefaultProfile() { + assertThat(ProfileName.DEFAULT).hasToString("default"); + } + +} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/TestProjectDescriptions.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/TestProjectDescriptions.java new file mode 100644 index 0000000000..ddab0613c3 --- /dev/null +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/properties/TestProjectDescriptions.java @@ -0,0 +1,45 @@ +/* + * 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 io.spring.initializr.generator.buildsystem.maven.MavenBuildSystem; +import io.spring.initializr.generator.language.java.JavaLanguage; +import io.spring.initializr.generator.project.MutableProjectDescription; +import io.spring.initializr.generator.project.ProjectDescription; + +/** + * Project descriptions used by the application properties tests. + * + * @author Moritz Halbritter + */ +final class TestProjectDescriptions { + + private TestProjectDescriptions() { + } + + /** + * Returns a description of a Maven and Java project. + * @return the project description + */ + static ProjectDescription mavenJava() { + MutableProjectDescription description = new MutableProjectDescription(); + description.setBuildSystem(new MavenBuildSystem()); + description.setLanguage(new JavaLanguage()); + return description; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/BuildSystem.java b/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/BuildSystem.java index 4acbacf8ef..21c3117eae 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/BuildSystem.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/BuildSystem.java @@ -29,6 +29,7 @@ * A build system that can be used by a generated project. * * @author Andy Wilkinson + * @author Moritz Halbritter */ public interface BuildSystem { @@ -67,6 +68,20 @@ default SourceStructure getTestSource(Path projectRoot, Language language) { return new SourceStructure(projectRoot.resolve("src/test/"), language); } + /** + * Returns a {@link SourceStructure} for the sources of the given source set. + * @param projectRoot the root of the project structure + * @param language the language of the project + * @param sourceSet the source set + * @return a {@link SourceStructure} for the assets of the source set + */ + default SourceStructure getSource(Path projectRoot, Language language, SourceSet sourceSet) { + return switch (sourceSet) { + case MAIN -> getMainSource(projectRoot, language); + case TEST -> getTestSource(projectRoot, language); + }; + } + static BuildSystem forId(String id) { return forIdAndDialect(id, null); } 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..d2fc1f940c --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/buildsystem/SourceSet.java @@ -0,0 +1,37 @@ +/* + * 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, + + /** + * The test source set, only available to tests. + */ + TEST + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormat.java b/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormat.java index 37d2ccd954..120d90dd1b 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormat.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormat.java @@ -22,6 +22,13 @@ import org.springframework.core.io.support.SpringFactoriesLoader; +/** + * The format of an application's configuration files, for example {@code properties} or + * {@code yaml}. + * + * @author Sijun Yang + * @author Moritz Halbritter + */ public interface ConfigurationFileFormat { /** @@ -30,6 +37,13 @@ public interface ConfigurationFileFormat { */ String id(); + /** + * Return the extension of the configuration files of this format, including the + * leading dot, for example {@code .yaml}. + * @return the file extension + */ + String fileExtension(); + /** * Creates the configuration file format for the given id. * @param id the id diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/properties/PropertiesFormat.java b/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/properties/PropertiesFormat.java index 89f7c38aa3..fbc4cf271b 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/properties/PropertiesFormat.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/properties/PropertiesFormat.java @@ -22,6 +22,7 @@ * Properties {@link ConfigurationFileFormat}. * * @author Sijun Yang + * @author Moritz Halbritter */ public final class PropertiesFormat implements ConfigurationFileFormat { @@ -35,6 +36,11 @@ public String id() { return ID; } + @Override + public String fileExtension() { + return ".properties"; + } + @Override public String toString() { return id(); diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/yaml/YamlFormat.java b/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/yaml/YamlFormat.java index ac2fd09c83..8acd6a149c 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/yaml/YamlFormat.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/configuration/format/yaml/YamlFormat.java @@ -22,6 +22,7 @@ * YAML {@link ConfigurationFileFormat}. * * @author Sijun Yang + * @author Moritz Halbritter */ public final class YamlFormat implements ConfigurationFileFormat { @@ -35,6 +36,11 @@ public String id() { return ID; } + @Override + public String fileExtension() { + return ".yaml"; + } + @Override public String toString() { return id(); diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/buildsystem/BuildSystemTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/buildsystem/BuildSystemTests.java index da1c39a8d5..eafb4886c1 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/buildsystem/BuildSystemTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/buildsystem/BuildSystemTests.java @@ -33,6 +33,7 @@ * Tests for {@link BuildSystem}. * * @author Stephane Nicoll + * @author Moritz Halbritter */ class BuildSystemTests { @@ -66,6 +67,22 @@ void defaultTestSource(@TempDir Path directory) { assertThat(testCodeStructure.getSourcesDirectory()).isEqualTo(directory.resolve("src/test/kotlin")); } + @Test + void sourceOfMainSourceSet(@TempDir Path directory) { + SourceStructure structure = BuildSystem.forId("gradle") + .getSource(directory, new JavaLanguage(), SourceSet.MAIN); + assertThat(structure.getRootDirectory()).isEqualTo(directory.resolve("src/main")); + assertThat(structure.getSourcesDirectory()).isEqualTo(directory.resolve("src/main/java")); + } + + @Test + void sourceOfTestSourceSet(@TempDir Path directory) { + SourceStructure structure = BuildSystem.forId("gradle") + .getSource(directory, new KotlinLanguage(), SourceSet.TEST); + assertThat(structure.getRootDirectory()).isEqualTo(directory.resolve("src/test")); + assertThat(structure.getSourcesDirectory()).isEqualTo(directory.resolve("src/test/kotlin")); + } + @Test void unknownBuildSystem() { assertThatIllegalStateException().isThrownBy(() -> BuildSystem.forId("unknown")) diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormatTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormatTests.java new file mode 100644 index 0000000000..b0063fe702 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/configuration/format/ConfigurationFileFormatTests.java @@ -0,0 +1,57 @@ +/* + * 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.configuration.format; + +import io.spring.initializr.generator.configuration.format.properties.PropertiesFormat; +import io.spring.initializr.generator.configuration.format.yaml.YamlFormat; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ConfigurationFileFormat}. + * + * @author Moritz Halbritter + */ +class ConfigurationFileFormatTests { + + @Test + void propertiesFormat() { + ConfigurationFileFormat format = ConfigurationFileFormat.forId(PropertiesFormat.ID); + assertThat(format).isInstanceOf(PropertiesFormat.class); + assertThat(format.id()).isEqualTo("properties"); + assertThat(format.fileExtension()).isEqualTo(".properties"); + assertThat(format).hasToString("properties"); + } + + @Test + void yamlFormat() { + ConfigurationFileFormat format = ConfigurationFileFormat.forId(YamlFormat.ID); + assertThat(format).isInstanceOf(YamlFormat.class); + assertThat(format.id()).isEqualTo("yaml"); + assertThat(format.fileExtension()).isEqualTo(".yaml"); + assertThat(format).hasToString("yaml"); + } + + @Test + void unknownFormat() { + assertThatIllegalStateException().isThrownBy(() -> ConfigurationFileFormat.forId("unknown")) + .withMessageContaining("Unrecognized configuration file format id 'unknown'"); + } + +}