From 9995d6a2e97ac2d54051815414f2f57abed22c14 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 31 May 2023 14:39:36 +0200 Subject: [PATCH] Polish "Add support for Docker Compose" See gh-1417 --- .../compose/ComposeProjectContributor.java | 63 +++++ .../DockerComposeHelpDocumentCustomizer.java | 28 +- .../compose}/Markdown.java | 2 +- .../compose}/package-info.java | 2 +- .../dockercompose/DockerComposeFile.java | 70 ----- .../DockerComposeFileCustomizer.java | 42 --- .../DockerComposeProjectContributor.java | 48 ---- ...ComposeProjectGenerationConfiguration.java | 49 ---- .../dockercompose/DockerComposeService.java | 254 ------------------ .../ComposeProjectContributorTests.java | 73 +++++ ...kerComposeHelpDocumentCustomizerTests.java | 16 +- .../compose/MarkdownTests.java} | 20 +- .../dockercompose/DockerComposeFileTests.java | 58 ---- .../DockerComposeProjectContributorTests.java | 53 ---- ...seProjectGenerationConfigurationTests.java | 71 ----- .../DockerComposeServiceHelper.java | 49 ---- .../DockerComposeServiceTests.java | 116 -------- .../container/docker/compose/ComposeFile.java | 28 +- .../docker/compose/ComposeFileWriter.java | 82 ++++++ .../docker/compose/ComposeService.java | 151 +++++++++++ .../compose/ComposeServiceContainer.java | 82 ++++++ .../docker/compose/package-info.java | 20 ++ .../compose/ComposeFileWriterTests.java | 99 +++++++ .../compose/ComposeServiceContainerTests.java | 182 +++++++++++++ .../InitializrAutoConfiguration.java | 3 +- src/checkstyle/checkstyle-suppressions.xml | 2 - 26 files changed, 807 insertions(+), 856 deletions(-) create mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributor.java rename initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/{dockercompose => docker/compose}/DockerComposeHelpDocumentCustomizer.java (62%) rename initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/{dockercompose => docker/compose}/Markdown.java (98%) rename initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/{dockercompose => docker/compose}/package-info.java (90%) delete mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFile.java delete mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileCustomizer.java delete mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributor.java delete mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfiguration.java delete mode 100644 initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeService.java create mode 100644 initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributorTests.java rename initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/{dockercompose => docker/compose}/DockerComposeHelpDocumentCustomizerTests.java (84%) rename initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/{dockercompose/MarkdownTableTests.java => docker/compose/MarkdownTests.java} (78%) delete mode 100644 initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileTests.java delete mode 100644 initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributorTests.java delete mode 100644 initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfigurationTests.java delete mode 100644 initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceHelper.java delete mode 100644 initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceTests.java rename initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTests.java => initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFile.java (56%) create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriter.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeService.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainer.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/package-info.java create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriterTests.java create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainerTests.java diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributor.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributor.java new file mode 100644 index 00000000..16f728b6 --- /dev/null +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +import io.spring.initializr.generator.container.docker.compose.ComposeFile; +import io.spring.initializr.generator.container.docker.compose.ComposeFileWriter; +import io.spring.initializr.generator.io.IndentingWriter; +import io.spring.initializr.generator.io.IndentingWriterFactory; +import io.spring.initializr.generator.project.contributor.ProjectContributor; + +/** + * A {@link ProjectContributor} that creates a 'compose.yaml' file through a + * {@link ComposeFile}. + * + * @author Moritz Halbritter + * @author Stephane Nicoll + */ +public class ComposeProjectContributor implements ProjectContributor { + + private final ComposeFile composeFile; + + private final IndentingWriterFactory indentingWriterFactory; + + private final ComposeFileWriter composeFileWriter; + + public ComposeProjectContributor(ComposeFile composeFile, IndentingWriterFactory indentingWriterFactory) { + this.composeFile = composeFile; + this.indentingWriterFactory = indentingWriterFactory; + this.composeFileWriter = new ComposeFileWriter(); + } + + @Override + public void contribute(Path projectRoot) throws IOException { + Path file = Files.createFile(projectRoot.resolve("compose.yaml")); + writeComposeFile(Files.newBufferedWriter(file)); + } + + void writeComposeFile(Writer out) throws IOException { + try (IndentingWriter writer = this.indentingWriterFactory.createIndentingWriter("yaml", out)) { + this.composeFileWriter.writeTo(writer, this.composeFile); + } + } + +} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeHelpDocumentCustomizer.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/DockerComposeHelpDocumentCustomizer.java similarity index 62% rename from initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeHelpDocumentCustomizer.java rename to initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/DockerComposeHelpDocumentCustomizer.java index de53ed76..9a2a044b 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeHelpDocumentCustomizer.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/DockerComposeHelpDocumentCustomizer.java @@ -14,35 +14,35 @@ * limitations under the License. */ -package io.spring.initializr.generator.spring.container.dockercompose; +package io.spring.initializr.generator.spring.container.docker.compose; -import java.util.Collection; import java.util.HashMap; import java.util.Map; -import io.spring.initializr.generator.spring.container.dockercompose.Markdown.MarkdownTable; +import io.spring.initializr.generator.container.docker.compose.ComposeFile; +import io.spring.initializr.generator.container.docker.compose.ComposeService; +import io.spring.initializr.generator.spring.container.docker.compose.Markdown.MarkdownTable; import io.spring.initializr.generator.spring.documentation.HelpDocument; import io.spring.initializr.generator.spring.documentation.HelpDocumentCustomizer; /** - * Provide additional information in the {@link HelpDocument} if the - * {@link DockerComposeFile} isn't empty. + * A {@link HelpDocumentCustomizer} that provide additional information about the + * {@link ComposeService compose services} that are defined for the project. * * @author Moritz Halbritter */ -class DockerComposeHelpDocumentCustomizer implements HelpDocumentCustomizer { +public class DockerComposeHelpDocumentCustomizer implements HelpDocumentCustomizer { - private final DockerComposeFile composeFile; + private final ComposeFile composeFile; - DockerComposeHelpDocumentCustomizer(DockerComposeFile composeFile) { + public DockerComposeHelpDocumentCustomizer(ComposeFile composeFile) { this.composeFile = composeFile; } @Override public void customize(HelpDocument document) { - Collection services = this.composeFile.getServices(); Map model = new HashMap<>(); - if (services.isEmpty()) { + if (this.composeFile.services().isEmpty()) { model.put("serviceTable", null); document.getWarnings() .addItem( @@ -50,10 +50,10 @@ class DockerComposeHelpDocumentCustomizer implements HelpDocumentCustomizer { } else { MarkdownTable serviceTable = Markdown.table("Service name", "Image", "Tag", "Website"); - for (DockerComposeService service : services) { - serviceTable.addRow(service.getName(), Markdown.code(service.getImage()), - Markdown.code(service.getImageTag()), Markdown.link("Website", service.getImageWebsite())); - } + this.composeFile.services() + .values() + .forEach((service) -> serviceTable.addRow(service.getName(), Markdown.code(service.getImage()), + Markdown.code(service.getImageTag()), Markdown.link("Website", service.getImageWebsite()))); model.put("serviceTable", serviceTable.toMarkdown()); } document.addSection("documentation/docker-compose", model); diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/Markdown.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/Markdown.java similarity index 98% rename from initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/Markdown.java rename to initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/Markdown.java index 0ffd7da0..6100c0d7 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/Markdown.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/Markdown.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.spring.initializr.generator.spring.container.dockercompose; +package io.spring.initializr.generator.spring.container.docker.compose; import java.util.ArrayList; import java.util.List; diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/package-info.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/package-info.java similarity index 90% rename from initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/package-info.java rename to initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/package-info.java index 8f58b886..5ca62430 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/package-info.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/docker/compose/package-info.java @@ -17,4 +17,4 @@ /** * Support for Docker Compose. */ -package io.spring.initializr.generator.spring.container.dockercompose; +package io.spring.initializr.generator.spring.container.docker.compose; diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFile.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFile.java deleted file mode 100644 index 8ae80bb8..00000000 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFile.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import java.io.PrintWriter; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.TreeMap; - -/** - * A Docker Compose file. - * - * @author Moritz Halbritter - */ -public class DockerComposeFile { - - // TreeMap to sort services by name - private final Map services = new TreeMap<>(); - - /** - * Adds a {@link DockerComposeService service} to the file. If a service with the same - * {@link DockerComposeService#getName() name} already exists, it is replaced. - * @param service the service to add - * @return {@code this} - */ - public DockerComposeFile addService(DockerComposeService service) { - this.services.put(service.getName(), service); - return this; - } - - /** - * Returns the service with the given name. - * @param name the name - * @return the service or {@code null} if no service with the given name exists - */ - public DockerComposeService getService(String name) { - return this.services.get(name); - } - - /** - * Returns all services. - * @return all services - */ - public Collection getServices() { - return Collections.unmodifiableCollection(this.services.values()); - } - - void write(PrintWriter writer) { - writer.println("services:"); - for (DockerComposeService service : this.services.values()) { - service.write(writer, 1); - } - } - -} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileCustomizer.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileCustomizer.java deleted file mode 100644 index c057cee6..00000000 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileCustomizer.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import org.springframework.core.Ordered; - -/** - * Callback for customizing a project's {@link DockerComposeFile}. Invoked with an - * {@link Ordered order} of {@code 0} by default, considering overriding - * {@link #getOrder()} to customize this behaviour. - * - * @author Moritz Halbritter - */ -@FunctionalInterface -public interface DockerComposeFileCustomizer extends Ordered { - - /** - * Customizes the given {@link DockerComposeFile}. - * @param composeFile the compose file to customize - */ - void customize(DockerComposeFile composeFile); - - @Override - default int getOrder() { - return 0; - } - -} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributor.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributor.java deleted file mode 100644 index 5c279622..00000000 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributor.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.Path; - -import io.spring.initializr.generator.project.contributor.ProjectContributor; - -/** - * A {@link ProjectContributor} which contributes a 'compose.yaml' file through a - * {@link DockerComposeFile}. - * - * @author Moritz Halbritter - */ -class DockerComposeProjectContributor implements ProjectContributor { - - private final DockerComposeFile composeFile; - - DockerComposeProjectContributor(DockerComposeFile composeFile) { - this.composeFile = composeFile; - } - - @Override - public void contribute(Path projectRoot) throws IOException { - Path file = Files.createFile(projectRoot.resolve("compose.yaml")); - try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(file))) { - this.composeFile.write(writer); - } - } - -} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfiguration.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfiguration.java deleted file mode 100644 index 1d6ba186..00000000 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import io.spring.initializr.generator.project.ProjectGenerationConfiguration; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.context.annotation.Bean; - -/** - * Configuration for contributions specific to the Docker Compose parts of a project. - * - * @author Moritz Halbritter - */ -@ProjectGenerationConfiguration -public class DockerComposeProjectGenerationConfiguration { - - @Bean - DockerComposeFile composeFile(ObjectProvider composeFileCustomizers) { - DockerComposeFile composeFile = new DockerComposeFile(); - composeFileCustomizers.orderedStream().forEach((customizer) -> customizer.customize(composeFile)); - return composeFile; - } - - @Bean - DockerComposeProjectContributor dockerComposeProjectContributor(DockerComposeFile composeFile) { - return new DockerComposeProjectContributor(composeFile); - } - - @Bean - DockerComposeHelpDocumentCustomizer dockerComposeHelpDocumentCustomizer(DockerComposeFile composeFile) { - return new DockerComposeHelpDocumentCustomizer(composeFile); - } - -} diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeService.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeService.java deleted file mode 100644 index 30c84acc..00000000 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeService.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import java.io.PrintWriter; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * A Docker Compose service. - * - * @author Moritz Halbritter - */ -public final class DockerComposeService { - - private static final String INDENT = " "; - - private final String name; - - private final String image; - - private final String imageTag; - - private final String imageWebsite; - - private final Map environment; - - private final Set ports; - - private DockerComposeService(Builder builder) { - this.name = builder.name; - this.image = builder.image; - this.imageTag = builder.imageTag; - this.imageWebsite = builder.imageWebsite; - this.environment = builder.environment; - this.ports = builder.ports; - } - - public String getName() { - return this.name; - } - - public String getImage() { - return this.image; - } - - public String getImageTag() { - return this.imageTag; - } - - public String getImageWebsite() { - return this.imageWebsite; - } - - public Map getEnvironment() { - return Collections.unmodifiableMap(this.environment); - } - - public Set getPorts() { - return Collections.unmodifiableSet(this.ports); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DockerComposeService service = (DockerComposeService) o; - return Objects.equals(this.name, service.name) && Objects.equals(this.image, service.image) - && Objects.equals(this.imageTag, service.imageTag) - && Objects.equals(this.imageWebsite, service.imageWebsite) - && Objects.equals(this.environment, service.environment) && Objects.equals(this.ports, service.ports); - } - - @Override - public int hashCode() { - return Objects.hash(this.name, this.image, this.imageTag, this.imageWebsite, this.environment, this.ports); - } - - @Override - public String toString() { - return "DockerComposeService{" + "name='" + this.name + '\'' + ", image='" + this.image + '\'' + ", imageTag='" - + this.imageTag + '\'' + ", imageWebsite='" + this.imageWebsite + '\'' + ", environment=" - + this.environment + ", ports=" + this.ports + '}'; - } - - void write(PrintWriter writer, int indentation) { - int currentIndent = indentation; - println(writer, this.name + ":", currentIndent); - currentIndent++; - println(writer, "image: '%s:%s'".formatted(this.image, this.imageTag), currentIndent); - if (!this.environment.isEmpty()) { - writeEnvironment(writer, currentIndent); - } - if (!this.ports.isEmpty()) { - writePorts(writer, currentIndent); - } - } - - private void writePorts(PrintWriter writer, int currentIndent) { - println(writer, "ports:", currentIndent); - for (Integer port : this.ports) { - println(writer, "- '%d'".formatted(port), currentIndent + 1); - } - } - - private void writeEnvironment(PrintWriter writer, int currentIndent) { - println(writer, "environment:", currentIndent); - for (Map.Entry env : this.environment.entrySet()) { - println(writer, "- '%s=%s'".formatted(env.getKey(), env.getValue()), currentIndent + 1); - } - } - - private void println(PrintWriter writer, String value, int indentation) { - writer.write(INDENT.repeat(indentation)); - writer.println(value); - } - - /** - * Initialize a new {@link Builder} with the given image. The name is automatically - * deduced. - * @param image the image - * @param tag the image tag - * @return a new builder - */ - public static Builder withImage(String image, String tag) { - // See https://github.com/docker/compose/pull/1624 - String name = image.replaceAll("[^a-zA-Z0-9._\\-]", "_"); - return new Builder(name, image, tag); - } - - /** - * Initialize a new {@link Builder} with the given image. The name is automatically - * deduced. - * @param imageAndTag the image and tag in the format {@code image:tag} - * @return a new builder - */ - public static Builder withImage(String imageAndTag) { - String[] split = imageAndTag.split(":", 2); - if (split.length == 1) { - return withImage(split[0], "latest"); - } - else { - return withImage(split[0], split[1]); - } - } - - /** - * Initialize a {@link Builder} with the given service. - * @param service the service to initialize from - * @return a new builder - */ - public static Builder from(DockerComposeService service) { - return new Builder(service.name, service.image, service.imageTag).imageWebsite(service.imageWebsite) - .environment(service.environment) - .ports(service.ports); - } - - /** - * Builder for {@link DockerComposeService}. - */ - public static final class Builder { - - private String name; - - private String image; - - private String imageTag; - - private String imageWebsite; - - private final Map environment = new TreeMap<>(); - - private final Set ports = new TreeSet<>(); - - private Builder(String name, String image, String imageTag) { - this.name = name; - this.image = image; - this.imageTag = imageTag; - } - - public Builder name(String name) { - this.name = name; - return this; - } - - public Builder image(String image) { - this.image = image; - return this; - } - - public Builder imageTag(String imageTag) { - this.imageTag = imageTag; - return this; - } - - public Builder imageWebsite(String imageWebsite) { - this.imageWebsite = imageWebsite; - return this; - } - - public Builder environment(String key, String value) { - this.environment.put(key, value); - return this; - } - - public Builder environment(Map environment) { - this.environment.putAll(environment); - return this; - } - - public Builder ports(Collection ports) { - this.ports.addAll(ports); - return this; - } - - public Builder ports(int... ports) { - return ports(Arrays.stream(ports).boxed().toList()); - } - - /** - * Builds the {@link DockerComposeService} instance. - * @return the built instance - */ - public DockerComposeService build() { - return new DockerComposeService(this); - } - - } - -} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributorTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributorTests.java new file mode 100644 index 00000000..c0fff691 --- /dev/null +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/ComposeProjectContributorTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Path; + +import io.spring.initializr.generator.container.docker.compose.ComposeFile; +import io.spring.initializr.generator.io.IndentingWriterFactory; +import io.spring.initializr.generator.io.SimpleIndentStrategy; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ComposeProjectContributor}. + * + * @author Moritz Halbritter + * @author Stephane Nicoll + */ +class ComposeProjectContributorTests { + + @Test + void composeFileIsContributedInProjectStructure(@TempDir Path projectDir) throws IOException { + ComposeFile compose = new ComposeFile(); + compose.services().add("test", (service) -> service.image("my-image:1.2.3")); + new ComposeProjectContributor(compose, IndentingWriterFactory.withDefaultSettings()).contribute(projectDir); + Path composeFile = projectDir.resolve("compose.yaml"); + assertThat(composeFile).isRegularFile(); + } + + @Test + void composeFileIsContributedUsingYamlContentId() throws IOException { + IndentingWriterFactory indentingWriterFactory = IndentingWriterFactory.create(new SimpleIndentStrategy(" "), + (factory) -> factory.indentingStrategy("yaml", new SimpleIndentStrategy("\t"))); + ComposeFile composeFile = new ComposeFile(); + composeFile.services() + .add("test", (service) -> service.imageAndTag("image:1.3.3").environment("a", "aa").environment("b", "bb")); + assertThat(generateComposeFile(composeFile, indentingWriterFactory)).isEqualToIgnoringNewLines(""" + services: + test: + image: 'image:1.3.3' + environment: + - 'a=aa' + - 'b=bb' + """); + + } + + private String generateComposeFile(ComposeFile composeFile, IndentingWriterFactory indentingWriterFactory) + throws IOException { + StringWriter writer = new StringWriter(); + new ComposeProjectContributor(composeFile, indentingWriterFactory).writeComposeFile(writer); + return writer.toString(); + } + +} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeHelpDocumentCustomizerTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/DockerComposeHelpDocumentCustomizerTests.java similarity index 84% rename from initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeHelpDocumentCustomizerTests.java rename to initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/DockerComposeHelpDocumentCustomizerTests.java index 44caa065..401e68d1 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeHelpDocumentCustomizerTests.java +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/DockerComposeHelpDocumentCustomizerTests.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package io.spring.initializr.generator.spring.container.dockercompose; +package io.spring.initializr.generator.spring.container.docker.compose; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import io.spring.initializr.generator.container.docker.compose.ComposeFile; import io.spring.initializr.generator.io.template.MustacheTemplateRenderer; import io.spring.initializr.generator.io.text.MustacheSection; import io.spring.initializr.generator.io.text.Section; @@ -38,17 +39,18 @@ class DockerComposeHelpDocumentCustomizerTests { private DockerComposeHelpDocumentCustomizer customizer; - private DockerComposeFile dockerComposeFile; + private ComposeFile dockerComposeFile; @BeforeEach void setUp() { - this.dockerComposeFile = new DockerComposeFile(); + this.dockerComposeFile = new ComposeFile(); this.customizer = new DockerComposeHelpDocumentCustomizer(this.dockerComposeFile); } @Test void addsDockerComposeSection() throws IOException { - this.dockerComposeFile.addService(DockerComposeServiceHelper.service()); + this.dockerComposeFile.services() + .add("test", (service) -> service.imageAndTag("image-1:1.2.3").imageWebsite("https:/example.com/image-1")); HelpDocument helpDocument = helpDocument(); this.customizer.customize(helpDocument); assertThat(helpDocument.getSections()).hasSize(1); @@ -63,9 +65,9 @@ class DockerComposeHelpDocumentCustomizerTests { In this file, the following services have been defined: - | Service name | Image | Tag | Website | - | ------------ | --------- | ------------- | --------------------------------- | - | service-1 | `image-1` | `image-tag-1` | [Website](https://service-1.org/) | + | Service name | Image | Tag | Website | + | ------------ | --------- | ------- | ------------------------------------- | + | test | `image-1` | `1.2.3` | [Website](https:/example.com/image-1) | Please review the tags of the used images and set them to the same as you're running in production."""); diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTableTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/MarkdownTests.java similarity index 78% rename from initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTableTests.java rename to initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/MarkdownTests.java index 5a2c3591..899323eb 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTableTests.java +++ b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/docker/compose/MarkdownTests.java @@ -14,20 +14,32 @@ * limitations under the License. */ -package io.spring.initializr.generator.spring.container.dockercompose; +package io.spring.initializr.generator.spring.container.docker.compose; -import io.spring.initializr.generator.spring.container.dockercompose.Markdown.MarkdownTable; +import io.spring.initializr.generator.spring.container.docker.compose.Markdown.MarkdownTable; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Tests for {@link MarkdownTable}. + * Tests for {@link Markdown}. * * @author Moritz Halbritter */ -class MarkdownTableTests { +class MarkdownTests { + + @Test + void shouldFormatCode() { + String code = Markdown.code("c = a + b"); + assertThat(code).isEqualTo("`c = a + b`"); + } + + @Test + void shouldFormatLink() { + String link = Markdown.link("Spring Website", "https://spring.io/"); + assertThat(link).isEqualTo("[Spring Website](https://spring.io/)"); + } @Test void shouldFormatCorrectly() { diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileTests.java deleted file mode 100644 index 39d93134..00000000 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeFileTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link DockerComposeFile}. - * - * @author Moritz Halbritter - */ -class DockerComposeFileTests { - - @Test - void write() { - DockerComposeFile file = new DockerComposeFile(); - file.addService(DockerComposeServiceHelper.service(1)); - file.addService(DockerComposeServiceHelper.service(2)); - StringWriter writer = new StringWriter(); - file.write(new PrintWriter(writer)); - assertThat(writer.toString()).isEqualToIgnoringNewLines(""" - services: - service-1: - image: 'image-1:image-tag-1' - service-2: - image: 'image-2:image-tag-2' - """); - } - - @Test - void servicesAreOrderedByName() { - DockerComposeFile file = new DockerComposeFile(); - file.addService(DockerComposeServiceHelper.service(2)); - file.addService(DockerComposeServiceHelper.service(1)); - assertThat(file.getServices()).containsExactly(DockerComposeServiceHelper.service(1), - DockerComposeServiceHelper.service(2)); - } - -} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributorTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributorTests.java deleted file mode 100644 index 41ea0a9b..00000000 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectContributorTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link DockerComposeProjectContributor}. - * - * @author Moritz Halbritter - */ -class DockerComposeProjectContributorTests { - - private DockerComposeProjectContributor contributor; - - private DockerComposeFile dockerComposeFile; - - @BeforeEach - void setUp() { - this.dockerComposeFile = new DockerComposeFile(); - this.contributor = new DockerComposeProjectContributor(this.dockerComposeFile); - } - - @Test - void writesComposeYamlFile(@TempDir Path tempDir) throws IOException { - this.dockerComposeFile.addService(DockerComposeServiceHelper.service()); - this.contributor.contribute(tempDir); - assertThat(tempDir.resolve("compose.yaml")).content(StandardCharsets.UTF_8).startsWith("services:"); - } - -} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfigurationTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfigurationTests.java deleted file mode 100644 index 0d16061b..00000000 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeProjectGenerationConfigurationTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link DockerComposeProjectGenerationConfiguration}. - * - * @author Moritz Halbritter - */ -class DockerComposeProjectGenerationConfigurationTests { - - private final ApplicationContextRunner runner = new ApplicationContextRunner() - .withUserConfiguration(DockerComposeProjectGenerationConfiguration.class); - - @Test - void providesBeans() { - this.runner.run((context) -> { - assertThat(context).hasSingleBean(DockerComposeFile.class); - assertThat(context).hasSingleBean(DockerComposeProjectContributor.class); - assertThat(context).hasSingleBean(DockerComposeHelpDocumentCustomizer.class); - }); - } - - @Test - void callsCustomizers() { - DockerComposeService service = DockerComposeServiceHelper.service(3); - DockerComposeFileCustomizer customizer = (composeFile) -> composeFile.addService(service); - this.runner.withBean(DockerComposeFileCustomizer.class, () -> customizer).run((context) -> { - DockerComposeFile composeFile = context.getBean(DockerComposeFile.class); - assertThat(composeFile.getServices()).containsExactly(service); - }); - } - - @Configuration - static class Services { - - @Bean - DockerComposeService service1() { - return DockerComposeServiceHelper.service(1); - } - - @Bean - DockerComposeService service2() { - return DockerComposeServiceHelper.service(2); - } - - } - -} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceHelper.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceHelper.java deleted file mode 100644 index 9af020d4..00000000 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -/** - * Helper class for {@link DockerComposeService}. - * - * @author Moritz Halbritter - */ -final class DockerComposeServiceHelper { - - private DockerComposeServiceHelper() { - } - - /** - * Creates a new {@link DockerComposeService}. - * @return a new {@link DockerComposeService} - */ - static DockerComposeService service() { - return service(1); - } - - /** - * Creates a new {@link DockerComposeService} with the given suffix. - * @param suffix the suffix - * @return a new {@link DockerComposeService} - */ - static DockerComposeService service(int suffix) { - return DockerComposeService.withImage("image-" + suffix, "image-tag-" + suffix) - .name("service-" + suffix) - .imageWebsite("https://service-" + suffix + ".org/") - .build(); - } - -} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceTests.java b/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceTests.java deleted file mode 100644 index 6c0109f5..00000000 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/DockerComposeServiceTests.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2012-2023 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.container.dockercompose; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -/** - * Tests for {@link DockerComposeService}. - * - * @author Moritz Halbritter - */ -class DockerComposeServiceTests { - - @Test - void shouldWriteToPrinter() { - StringWriter stringWriter = new StringWriter(); - DockerComposeService service = DockerComposeService.withImage("elasticsearch:8.6.1") - .imageWebsite("https://www.docker.elastic.co/r/elasticsearch") - .environment("ELASTIC_PASSWORD", "secret") - .environment("discovery.type", "single-node") - .ports(9200, 9300) - .build(); - service.write(new PrintWriter(stringWriter), 0); - String content = stringWriter.toString(); - assertThat(content).isEqualToIgnoringNewLines(""" - elasticsearch: - image: 'elasticsearch:8.6.1' - environment: - - 'ELASTIC_PASSWORD=secret' - - 'discovery.type=single-node' - ports: - - '9200' - - '9300' - """); - } - - @Test - void nameIsDeduced() { - DockerComposeService service = DockerComposeService.withImage("elasticsearch:8.6.1").build(); - assertThat(service.getName()).isEqualTo("elasticsearch"); - } - - @Test - void tagIsSetToLatestIfNotGiven() { - DockerComposeService service = DockerComposeService.withImage("redis").build(); - assertThat(service.getImage()).isEqualTo("redis"); - assertThat(service.getImageTag()).isEqualTo("latest"); - } - - @Test - void removesIllegalCharsFromDecudedName() { - DockerComposeService service = DockerComposeService.withImage("SOME._-name<>;|\uD83C\uDF31").build(); - assertThat(service.getName()).isEqualTo("SOME._-name_____"); - } - - @Test - void portsAreSorted() { - DockerComposeService service = DockerComposeService.withImage("redis").ports(5, 3, 4, 2, 1).build(); - assertThat(service.getPorts()).containsExactly(1, 2, 3, 4, 5); - } - - @Test - void environmentIsSorted() { - DockerComposeService service = DockerComposeService.withImage("redis") - .environment("z", "zz") - .environment("a", "aa") - .build(); - assertThat(service.getEnvironment()).containsExactly(entry("a", "aa"), entry("z", "zz")); - } - - @Test - void builderFrom() { - DockerComposeService service = DockerComposeService.withImage("elasticsearch", "8.6.1") - .imageWebsite("https://hub.docker.com/_/redis") - .environment(Map.of("some", "value")) - .ports(6379) - .build(); - DockerComposeService service2 = DockerComposeService.from(service).build(); - assertThat(service).isEqualTo(service2); - } - - @Test - void equalsAndHashcode() { - DockerComposeService service1 = DockerComposeService.withImage("redis").build(); - DockerComposeService service2 = DockerComposeService.withImage("elasticsearch:8.6.1").build(); - DockerComposeService service3 = DockerComposeService.withImage("redis").build(); - assertThat(service1).isEqualTo(service3); - assertThat(service1).hasSameHashCodeAs(service3); - assertThat(service3).isEqualTo(service1); - assertThat(service3).hasSameHashCodeAs(service1); - assertThat(service1).isNotEqualTo(service2); - assertThat(service2).isNotEqualTo(service1); - } - -} diff --git a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTests.java b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFile.java similarity index 56% rename from initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTests.java rename to initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFile.java index 291dc1d2..959428f3 100644 --- a/initializr-generator-spring/src/test/java/io/spring/initializr/generator/spring/container/dockercompose/MarkdownTests.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFile.java @@ -14,29 +14,25 @@ * limitations under the License. */ -package io.spring.initializr.generator.spring.container.dockercompose; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; +package io.spring.initializr.generator.container.docker.compose; /** - * Tests for {@link Markdown}. + * Model for a Docker Compose file. * * @author Moritz Halbritter + * @author Stephane Nicoll */ -class MarkdownTests { +public class ComposeFile { - @Test - void shouldFormatCode() { - String code = Markdown.code("c = a + b"); - assertThat(code).isEqualTo("`c = a + b`"); - } + private final ComposeServiceContainer services = new ComposeServiceContainer(); - @Test - void shouldFormatLink() { - String link = Markdown.link("Spring Website", "https://spring.io/"); - assertThat(link).isEqualTo("[Spring Website](https://spring.io/)"); + /** + * Return the {@linkplain ComposeServiceContainer service container} to use to + * configure services. + * @return the {@link ComposeServiceContainer} + */ + public ComposeServiceContainer services() { + return this.services; } } diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriter.java new file mode 100644 index 00000000..944e2278 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriter.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.util.Comparator; +import java.util.Map; +import java.util.Set; + +import io.spring.initializr.generator.io.IndentingWriter; + +/** + * A {@link ComposeFile} writer for {@code compose.yaml}. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + */ +public class ComposeFileWriter { + + /** + * Write a {@linkplain ComposeFile compose.yaml} using the specified + * {@linkplain IndentingWriter writer}. + * @param writer the writer to use + * @param compose the compose file to write + */ + public void writeTo(IndentingWriter writer, ComposeFile compose) { + writer.println("services:"); + compose.services() + .values() + .sorted(Comparator.comparing(ComposeService::getName)) + .forEach((service) -> writeService(writer, service)); + } + + private void writeService(IndentingWriter writer, ComposeService service) { + writer.indented(() -> { + writer.println(service.getName() + ":"); + writer.indented(() -> { + writer.println("image: '%s:%s'".formatted(service.getImage(), service.getImageTag())); + writerServiceEnvironment(writer, service.getEnvironment()); + writerServicePorts(writer, service.getPorts()); + }); + }); + } + + private void writerServiceEnvironment(IndentingWriter writer, Map environment) { + if (environment.isEmpty()) { + return; + } + writer.println("environment:"); + writer.indented(() -> { + for (Map.Entry env : environment.entrySet()) { + writer.println("- '%s=%s'".formatted(env.getKey(), env.getValue())); + } + }); + } + + private void writerServicePorts(IndentingWriter writer, Set ports) { + if (ports.isEmpty()) { + return; + } + writer.println("ports:"); + writer.indented(() -> { + for (Integer port : ports) { + writer.println("- '%d'".formatted(port)); + } + }); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeService.java b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeService.java new file mode 100644 index 00000000..8d7b91f7 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeService.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * A service to be declared in a Docker Compose file. + * + * @author Moritz Halbritter + * @author Stephane Nicoll + */ +public final class ComposeService { + + private final String name; + + private final String image; + + private final String imageTag; + + private final String imageWebsite; + + private final Map environment; + + private final Set ports; + + private ComposeService(Builder builder) { + this.name = builder.name; + this.image = builder.image; + this.imageTag = builder.imageTag; + this.imageWebsite = builder.imageWebsite; + this.environment = Collections.unmodifiableMap(new TreeMap<>(builder.environment)); + this.ports = Collections.unmodifiableSet(new TreeSet<>(builder.ports)); + } + + public String getName() { + return this.name; + } + + public String getImage() { + return this.image; + } + + public String getImageTag() { + return this.imageTag; + } + + public String getImageWebsite() { + return this.imageWebsite; + } + + public Map getEnvironment() { + return this.environment; + } + + public Set getPorts() { + return this.ports; + } + + /** + * Builder for {@link ComposeService}. + */ + public static class Builder { + + private final String name; + + private String image; + + private String imageTag = "latest"; + + private String imageWebsite; + + private final Map environment = new TreeMap<>(); + + private final Set ports = new TreeSet<>(); + + protected Builder(String name) { + this.name = name; + } + + public Builder imageAndTag(String imageAndTag) { + String[] split = imageAndTag.split(":", 2); + String tag = (split.length == 1) ? "latest" : split[1]; + return image(split[0]).imageTag(tag); + } + + public Builder image(String image) { + this.image = image; + return this; + } + + public Builder imageTag(String imageTag) { + this.imageTag = imageTag; + return this; + } + + public Builder imageWebsite(String imageWebsite) { + this.imageWebsite = imageWebsite; + return this; + } + + public Builder environment(String key, String value) { + this.environment.put(key, value); + return this; + } + + public Builder environment(Map environment) { + this.environment.putAll(environment); + return this; + } + + public Builder ports(Collection ports) { + this.ports.addAll(ports); + return this; + } + + public Builder ports(int... ports) { + return ports(Arrays.stream(ports).boxed().toList()); + } + + /** + * Builds the {@link ComposeService} instance. + * @return the built instance + */ + public ComposeService build() { + return new ComposeService(this); + } + + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainer.java b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainer.java new file mode 100644 index 00000000..f1376613 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import io.spring.initializr.generator.container.docker.compose.ComposeService.Builder; + +/** + * A container for {@linkplain ComposeService Docker Compose services}. + * + * @author Stephane Nicoll + */ +public class ComposeServiceContainer { + + private final Map services = new LinkedHashMap<>(); + + /** + * Specify if this container is empty. + * @return {@code true} if no service is registered + */ + public boolean isEmpty() { + return this.services.isEmpty(); + } + + /** + * Specify if this container has a service customization with the specified + * {@code name}. + * @param name the name of a service + * @return {@code true} if a customization for a service with the specified + * {@code name} exists + */ + public boolean has(String name) { + return this.services.containsKey(name); + } + + /** + * Return the {@link ComposeService services} to customize. + * @return the compose services + */ + public Stream values() { + return this.services.values().stream().map(Builder::build); + } + + /** + * Add a {@link ComposeService} with the specified name and {@link Consumer} to + * customize the object. If the service has already been added, the consumer can be + * used to further tune the existing service configuration. + * @param name the name of the service + * @param service a {@link Consumer} to customize the {@link ComposeService} + */ + public void add(String name, Consumer service) { + service.accept(this.services.computeIfAbsent(name, Builder::new)); + } + + /** + * Remove the service with the specified {@code name}. + * @param name the name of the service + * @return {@code true} if such a service was registered, {@code false} otherwise + */ + public boolean remove(String name) { + return this.services.remove(name) != null; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/package-info.java b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/package-info.java new file mode 100644 index 00000000..45ebcd5c --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/container/docker/compose/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Docker Compose support. + */ +package io.spring.initializr.generator.container.docker.compose; diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriterTests.java new file mode 100644 index 00000000..04b3a01a --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeFileWriterTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.io.StringWriter; +import java.util.function.Consumer; + +import io.spring.initializr.generator.container.docker.compose.ComposeService.Builder; +import io.spring.initializr.generator.io.IndentingWriter; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ComposeFile}. + * + * @author Moritz Halbritter + * @author Stephane Nicoll + */ +class ComposeFileWriterTests { + + @Test + void writeBasicServices() { + ComposeFile file = new ComposeFile(); + file.services().add("first", withSuffix(1)); + file.services().add("second", withSuffix(2)); + assertThat(write(file)).isEqualToIgnoringNewLines(""" + services: + first: + image: 'image-1:image-tag-1' + second: + image: 'image-2:image-tag-2' + """); + } + + @Test + void writeDetailedService() { + ComposeFile file = new ComposeFile(); + file.services() + .add("elasticsearch", + (builder) -> builder.image("elasticsearch") + .imageTag("8.6.1") + .imageWebsite("https://www.docker.elastic.co/r/elasticsearch") + .environment("ELASTIC_PASSWORD", "secret") + .environment("discovery.type", "single-node") + .ports(9200, 9300)); + assertThat(write(file)).isEqualToIgnoringNewLines(""" + services: + elasticsearch: + image: 'elasticsearch:8.6.1' + environment: + - 'ELASTIC_PASSWORD=secret' + - 'discovery.type=single-node' + ports: + - '9200' + - '9300' + """); + } + + @Test + void servicesAreOrderedByName() { + ComposeFile file = new ComposeFile(); + file.services().add("b", withSuffix(2)); + file.services().add("a", withSuffix(1)); + assertThat(write(file)).isEqualToIgnoringNewLines(""" + services: + a: + image: 'image-1:image-tag-1' + b: + image: 'image-2:image-tag-2' + """); + } + + private Consumer withSuffix(int suffix) { + return (builder) -> builder.image("image-" + suffix).imageTag("image-tag-" + suffix); + } + + private String write(ComposeFile file) { + StringWriter out = new StringWriter(); + IndentingWriter writer = new IndentingWriter(out, "\t"::repeat); + new ComposeFileWriter().writeTo(writer, file); + return out.toString(); + } + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainerTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainerTests.java new file mode 100644 index 00000000..c8e51b08 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/container/docker/compose/ComposeServiceContainerTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2012-2023 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.container.docker.compose; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ComposeServiceContainer}. + * + * @author Stephane Nicoll + */ +class ComposeServiceContainerTests { + + @Test + void isEmptyWithEmptyContainer() { + ComposeServiceContainer container = new ComposeServiceContainer(); + assertThat(container.isEmpty()).isTrue(); + } + + @Test + void isEmptyWithService() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.image("my-image")); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + void hasWithMatchingService() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.image("my-image")); + assertThat(container.has("test")).isTrue(); + } + + @Test + void hasWithNonMatchingName() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.image("my-image")); + assertThat(container.has("another")).isFalse(); + } + + @Test + void tagIsSetToLatestIfNotGiven() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.image("my-image")); + assertThat(container.values()).singleElement().satisfies((service) -> { + assertThat(service.getImage()).isEqualTo("my-image"); + assertThat(service.getImageTag()).isEqualTo("latest"); + }); + } + + @Test + void tagIsSetToLatestIfNotGivenInImageTag() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.imageAndTag("my-image")); + assertThat(container.values()).singleElement().satisfies((service) -> { + assertThat(service.getImage()).isEqualTo("my-image"); + assertThat(service.getImageTag()).isEqualTo("latest"); + }); + } + + @Test + void tagIsSetToGivenInImageTag() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.imageAndTag("my-image:1.2.3")); + assertThat(container.values()).singleElement().satisfies((service) -> { + assertThat(service.getImage()).isEqualTo("my-image"); + assertThat(service.getImageTag()).isEqualTo("1.2.3"); + }); + } + + @Test + void portsAreSorted() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.imageAndTag("my-image").ports(8080)); + container.add("test", (service) -> service.ports(7070)); + assertThat(container.values()).singleElement() + .satisfies((service) -> assertThat(service.getPorts()).containsExactly(7070, 8080)); + } + + @Test + void environmentKeysAreSorted() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.imageAndTag("my-image").environment("z", "zz")); + container.add("test", (service) -> service.environment("a", "aa")); + assertThat(container.values()).singleElement() + .satisfies((service) -> assertThat(service.getEnvironment()).containsExactly(entry("a", "aa"), + entry("z", "zz"))); + } + + @Test + void environmentIsMerged() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.imageAndTag("my-image").environment(Map.of("a", "aa", "z", "zz"))); + container.add("test", (service) -> service.environment(Map.of("a", "aaa", "b", "bb"))); + assertThat(container.values()).singleElement() + .satisfies((service) -> assertThat(service.getEnvironment()).containsExactly(entry("a", "aaa"), + entry("b", "bb"), entry("z", "zz"))); + } + + @Test + void customizeService() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> { + service.image("my-image"); + service.imageTag("my-image-tag"); + service.imageWebsite("https://example.com/my-image"); + service.environment("param", "value"); + service.ports(8080); + }); + assertThat(container.values()).singleElement().satisfies((service) -> { + assertThat(service.getName()).isEqualTo("test"); + assertThat(service.getImage()).isEqualTo("my-image"); + assertThat(service.getImageTag()).isEqualTo("my-image-tag"); + assertThat(service.getImageWebsite()).isEqualTo("https://example.com/my-image"); + assertThat(service.getEnvironment()).containsOnly(entry("param", "value")); + assertThat(service.getPorts()).containsOnly(8080); + }); + } + + @Test + void customizeTaskSeveralTimeReuseConfiguration() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> { + service.image("my-image"); + service.imageTag("my-image-tag"); + service.imageWebsite("https://example.com/my-image"); + service.environment("param", "value"); + service.ports(7070); + }); + container.add("test", (service) -> { + service.image("my-image"); + service.imageTag("my-image-tag"); + service.imageWebsite("https://example.com/my-image"); + service.environment("param", "value2"); + service.ports(8080); + }); + assertThat(container.values()).singleElement().satisfies((service) -> { + assertThat(service.getName()).isEqualTo("test"); + assertThat(service.getImage()).isEqualTo("my-image"); + assertThat(service.getImageTag()).isEqualTo("my-image-tag"); + assertThat(service.getImageWebsite()).isEqualTo("https://example.com/my-image"); + assertThat(service.getEnvironment()).containsOnly(entry("param", "value2")); + assertThat(service.getPorts()).containsOnly(7070, 8080); + }); + } + + @Test + void removeWithMatchingService() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.image("my-image")); + assertThat(container.remove("test")).isTrue(); + assertThat(container.isEmpty()).isTrue(); + } + + @Test + void removeWithNonMatchingName() { + ComposeServiceContainer container = new ComposeServiceContainer(); + container.add("test", (service) -> service.image("my-image")); + assertThat(container.remove("another")).isFalse(); + assertThat(container.isEmpty()).isFalse(); + } + +} diff --git a/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java b/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java index 7c46a38c..d45f637d 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/autoconfigure/InitializrAutoConfiguration.java @@ -87,7 +87,8 @@ public class InitializrAutoConfiguration { @Bean @ConditionalOnMissingBean public IndentingWriterFactory indentingWriterFactory() { - return IndentingWriterFactory.create(new SimpleIndentStrategy("\t")); + return IndentingWriterFactory.create(new SimpleIndentStrategy("\t"), + (builder) -> builder.indentingStrategy("yaml", new SimpleIndentStrategy(" "))); } @Bean diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index eb2abf5f..9981e9f6 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -6,6 +6,4 @@ - -