diff --git a/spring-cloud-dataflow-core/src/main/java/org/springframework/cloud/dataflow/core/AppBootSchemaVersion.java b/spring-cloud-dataflow-core/src/main/java/org/springframework/cloud/dataflow/core/AppBootSchemaVersion.java new file mode 100644 index 0000000000..e1c8d6d63b --- /dev/null +++ b/spring-cloud-dataflow-core/src/main/java/org/springframework/cloud/dataflow/core/AppBootSchemaVersion.java @@ -0,0 +1,58 @@ +/* + * Copyright 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 org.springframework.cloud.dataflow.core; + +import java.util.Arrays; + +/** + * Defines the possible schema versions that currently map to Spring {@code "Boot"}. A registered application can only support one schema version. + * + *

Each value defines the supported Spring Boot version that represents the changes in the schemas or Spring Batch and Task.

+ * + * @author Chris Bono + * @author Corneil du Plessis + */ +public enum AppBootSchemaVersion { + + BOOT2("2"), + BOOT3("3"); + + private String bootVersion; + + AppBootSchemaVersion(String bootVersion) { + this.bootVersion = bootVersion; + } + + public static AppBootSchemaVersion defaultVersion() { + return BOOT2; + } + + public static AppBootSchemaVersion fromBootVersion(String bootVersion) { + return Arrays.stream(AppBootSchemaVersion.values()) + .filter((bv) -> bv.bootVersion.equals(bootVersion)) + .findFirst().orElseThrow(() -> new IllegalArgumentException("Invalid AppBootSchemaVersion: " + bootVersion)); + } + + public String getBootVersion() { + return this.bootVersion; + } + + @Override + public String toString() { + return "AppBootVersion{bootVersion='" + this.bootVersion + "'}"; + } +} diff --git a/spring-cloud-dataflow-core/src/test/java/org/springframework/cloud/dataflow/core/AppBootSchemaVersionTests.java b/spring-cloud-dataflow-core/src/test/java/org/springframework/cloud/dataflow/core/AppBootSchemaVersionTests.java new file mode 100644 index 0000000000..5332812713 --- /dev/null +++ b/spring-cloud-dataflow-core/src/test/java/org/springframework/cloud/dataflow/core/AppBootSchemaVersionTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 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 org.springframework.cloud.dataflow.core; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link AppBootSchemaVersion}. + * + * @author Chris Bono + * @author Corneil du Plessis + */ +public class AppBootSchemaVersionTests { + + @Test + void bootVersion2() { + assertThat(AppBootSchemaVersion.BOOT2.getBootVersion()).isEqualTo("2"); + } + + @Test + void bootVersion3() { + assertThat(AppBootSchemaVersion.BOOT3.getBootVersion()).isEqualTo("3"); + } + + @Test + void fromBootVersionWithValidValues() { + assertThat(AppBootSchemaVersion.fromBootVersion("2")).isEqualTo(AppBootSchemaVersion.BOOT2); + assertThat(AppBootSchemaVersion.fromBootVersion("3")).isEqualTo(AppBootSchemaVersion.BOOT3); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { "Boot2", "boot2", "BOOT2", "foo", "Boot3", "boot3", "BOOT3" }) + void fromBootVersionWithInvalidValues(String invalidBootVersion) { + assertThatIllegalArgumentException() + .isThrownBy(() -> AppBootSchemaVersion.fromBootVersion(invalidBootVersion)) + .withMessage("Invalid AppBootSchemaVersion: %s", invalidBootVersion); + } +} diff --git a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryOperations.java b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryOperations.java index b89878e760..95bcb956cc 100644 --- a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryOperations.java +++ b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-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. @@ -18,6 +18,7 @@ import java.util.Properties; +import org.springframework.cloud.dataflow.core.AppBootSchemaVersion; import org.springframework.cloud.dataflow.core.ApplicationType; import org.springframework.cloud.dataflow.rest.resource.AppRegistrationResource; import org.springframework.cloud.dataflow.rest.resource.DetailedAppRegistrationResource; @@ -32,6 +33,7 @@ * @author Patrick Peralta * @author Mark Fisher * @author Chris Schaefer + * @author Chris Bono */ public interface AppRegistryOperations { @@ -81,9 +83,24 @@ public interface AppRegistryOperations { * @param metadataUri URI for the application metadata artifact * @param force if {@code true}, overwrites a pre-existing registration * @return the new app registration + * @deprecated in favor of {@link #register(String, ApplicationType, AppBootSchemaVersion, String, String, boolean)} */ + @Deprecated AppRegistrationResource register(String name, ApplicationType type, String uri, String metadataUri, boolean force); + /** + * Register an application name, type, and boot version with its Maven coordinates. + * + * @param name application name + * @param type application type + * @param bootVersion application boot version + * @param uri URI for the application artifact + * @param metadataUri URI for the application metadata artifact + * @param force if {@code true}, overwrites a pre-existing registration + * @return the new app registration + */ + AppRegistrationResource register(String name, ApplicationType type, AppBootSchemaVersion bootVersion, String uri, String metadataUri, boolean force); + /** * Register an application name, type and version with its Maven coordinates. * @@ -94,10 +111,27 @@ public interface AppRegistryOperations { * @param metadataUri URI for the application metadata artifact * @param force if {@code true}, overwrites a pre-existing registration * @return the new app registration + * @deprecated in favor of {@link #register(String, ApplicationType, AppBootSchemaVersion, String, String, String, boolean)} */ + @Deprecated AppRegistrationResource register(String name, ApplicationType type, String version, String uri, String metadataUri, boolean force); + /** + * Register an application name, type, boot version, and version with its Maven coordinates. + * + * @param name application name + * @param type application type + * @param bootVersion application boot version + * @param version application version + * @param uri URI for the application artifact + * @param metadataUri URI for the application metadata artifact + * @param force if {@code true}, overwrites a pre-existing registration + * @return the new app registration + */ + AppRegistrationResource register(String name, ApplicationType type, AppBootSchemaVersion bootVersion, String version, String uri, + String metadataUri, boolean force); + /** * Unregister an application name and type. * diff --git a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryTemplate.java b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryTemplate.java index 96ce270cd5..44966a2f06 100644 --- a/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryTemplate.java +++ b/spring-cloud-dataflow-rest-client/src/main/java/org/springframework/cloud/dataflow/rest/client/AppRegistryTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-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. @@ -18,6 +18,7 @@ import java.util.Properties; +import org.springframework.cloud.dataflow.core.AppBootSchemaVersion; import org.springframework.cloud.dataflow.core.ApplicationType; import org.springframework.cloud.dataflow.rest.resource.AppRegistrationResource; import org.springframework.cloud.dataflow.rest.resource.DetailedAppRegistrationResource; @@ -40,6 +41,7 @@ * @author Patrick Peralta * @author Christian Tzolov * @author Chris Schaefer + * @author Chris Bono */ public class AppRegistryTemplate implements AppRegistryOperations { /** @@ -112,15 +114,14 @@ public DetailedAppRegistrationResource info(String name, ApplicationType type, S } @Override - public AppRegistrationResource register(String name, ApplicationType type, String uri, String metadataUri, - boolean force) { - MultiValueMap values = new LinkedMultiValueMap(); - values.add("uri", uri); - if (metadataUri != null) { - values.add("metadata-uri", metadataUri); - } - values.add("force", Boolean.toString(force)); + public AppRegistrationResource register(String name, ApplicationType type, String uri, String metadataUri, boolean force) { + return register(name, type, (AppBootSchemaVersion) null, uri, metadataUri, force); + } + @Override + public AppRegistrationResource register(String name, ApplicationType type, AppBootSchemaVersion bootVersion, + String uri, String metadataUri, boolean force) { + MultiValueMap values = valuesForRegisterPost(bootVersion, uri, metadataUri, force); return restTemplate.postForObject(appsLink.getHref() + "/{type}/{name}", values, AppRegistrationResource.class, type, name); } @@ -128,15 +129,29 @@ public AppRegistrationResource register(String name, ApplicationType type, Strin @Override public AppRegistrationResource register(String name, ApplicationType type, String version, String uri, String metadataUri, boolean force) { + return this.register(name, type, null, version, uri, metadataUri, force); + } + + @Override + public AppRegistrationResource register(String name, ApplicationType type, AppBootSchemaVersion bootVersion, + String version, String uri, String metadataUri, boolean force) { + MultiValueMap values = valuesForRegisterPost(bootVersion, uri, metadataUri, force); + return restTemplate.postForObject(appsLink.getHref() + "/{type}/{name}/{version}", values, + AppRegistrationResource.class, type, name, version); + } + + private MultiValueMap valuesForRegisterPost(AppBootSchemaVersion bootVersion, String uri, + String metadataUri, boolean force) { MultiValueMap values = new LinkedMultiValueMap<>(); values.add("uri", uri); if (metadataUri != null) { values.add("metadata-uri", metadataUri); } + if (bootVersion != null) { + values.add("bootVersion", bootVersion.getBootVersion()); + } values.add("force", Boolean.toString(force)); - - return restTemplate.postForObject(appsLink.getHref() + "/{type}/{name}/{version}", values, - AppRegistrationResource.class, type, name, version); + return values; } @Override diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/web/WebConfiguration.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/web/WebConfiguration.java index f5ab03cc76..db93744889 100644 --- a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/web/WebConfiguration.java +++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/web/WebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-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. @@ -34,12 +34,15 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.cloud.dataflow.core.AppBootSchemaVersion; import org.springframework.cloud.dataflow.rest.support.jackson.ISO8601DateFormatWithMilliSeconds; import org.springframework.cloud.dataflow.rest.support.jackson.Jackson2DataflowModule; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.ContextClosedEvent; +import org.springframework.core.convert.converter.Converter; +import org.springframework.format.FormatterRegistry; import org.springframework.hateoas.server.core.DefaultLinkRelationProvider; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; @@ -58,6 +61,8 @@ * @author Christian Tzolov * @author David Turanski * @author Michael Wirth + * @author Chris Bono + * @author Corneil du Plessis */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @@ -92,6 +97,11 @@ public WebMvcConfigurer configurer() { public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setUseSuffixPatternMatch(false); } + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new AppBootVersionConverter()); + } }; } @@ -133,4 +143,13 @@ public void onApplicationEvent(ContextClosedEvent event) { this.longTaskSample = null; } } + + static class AppBootVersionConverter implements Converter { + + @Override + public AppBootSchemaVersion convert(String value) { + return value != null ? AppBootSchemaVersion.fromBootVersion(value) : null; + } + } + } diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/AppRegistryController.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/AppRegistryController.java index 02bc84fdba..54b0242a38 100644 --- a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/AppRegistryController.java +++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/AppRegistryController.java @@ -35,6 +35,7 @@ import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; import org.springframework.cloud.dataflow.configuration.metadata.ApplicationConfigurationMetadataResolver; +import org.springframework.cloud.dataflow.core.AppBootSchemaVersion; import org.springframework.cloud.dataflow.core.AppRegistration; import org.springframework.cloud.dataflow.core.ApplicationType; import org.springframework.cloud.dataflow.core.StreamAppDefinition; @@ -221,17 +222,21 @@ else if (entry.getKey().equals("outbound")) { * @param type module type * @param name module name * @param version module version + * @param bootVersion module boot version or {@code null} to use the default * @param uri URI for the module artifact (e.g. {@literal maven://group:artifact:version}) * @param metadataUri URI for the metadata artifact * @param force if {@code true}, overwrites a pre-existing registration */ @RequestMapping(value = "/{type}/{name}/{version:.+}", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) - public void register(@PathVariable("type") ApplicationType type, @PathVariable("name") String name, + public void register( + @PathVariable("type") ApplicationType type, + @PathVariable("name") String name, @PathVariable("version") String version, - @RequestParam("uri") String uri, @RequestParam(name = "metadata-uri", required = false) String metadataUri, + @RequestParam(name = "bootVersion", required = false) AppBootSchemaVersion bootVersion, + @RequestParam("uri") String uri, + @RequestParam(name = "metadata-uri", required = false) String metadataUri, @RequestParam(value = "force", defaultValue = "false") boolean force) { - validateApplicationName(name); appRegistryService.validate(appRegistryService.getDefaultApp(name, type), uri, version); AppRegistration previous = appRegistryService.find(name, type, version); @@ -251,11 +256,15 @@ public void register(@PathVariable("type") ApplicationType type, @PathVariable(" @Deprecated @RequestMapping(value = "/{type}/{name}", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) - public void register(@PathVariable("type") ApplicationType type, @PathVariable("name") String name, - @RequestParam("uri") String uri, @RequestParam(name = "metadata-uri", required = false) String metadataUri, + public void register( + @PathVariable("type") ApplicationType type, + @PathVariable("name") String name, + @RequestParam(name = "bootVersion", required = false) AppBootSchemaVersion bootVersion, + @RequestParam("uri") String uri, + @RequestParam(name = "metadata-uri", required = false) String metadataUri, @RequestParam(value = "force", defaultValue = "false") boolean force) { String version = this.appRegistryService.getResourceVersion(uri); - this.register(type, name, version, uri, metadataUri, force); + this.register(type, name, version, bootVersion, uri, metadataUri, force); } /** diff --git a/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/command/AppRegistryCommands.java b/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/command/AppRegistryCommands.java index 71ce505bea..0965453ffb 100644 --- a/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/command/AppRegistryCommands.java +++ b/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/command/AppRegistryCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-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. @@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.cloud.dataflow.core.AppBootSchemaVersion; import org.springframework.cloud.dataflow.core.ApplicationType; import org.springframework.cloud.dataflow.rest.client.AppRegistryOperations; import org.springframework.cloud.dataflow.rest.resource.AppRegistrationResource; @@ -241,13 +242,13 @@ public String defaultApplication( public String register( @ShellOption(value = { "", "--name" }, help = "the name for the registered application") String name, @ShellOption(help = "the type for the registered application", valueProvider = EnumValueProvider.class) ApplicationType type, + @ShellOption(value = { "-bv", "--bootVersion" }, help = "the boot version to use for the registered application", defaultValue = ShellOption.NULL) AppBootSchemaVersion bootVersion, @ShellOption(help = "URI for the application artifact") String uri, - @ShellOption(value = { "--metadata-uri", "--metadataUri"}, help = "Metadata URI for the application artifact", defaultValue = ShellOption.NULL) String metadataUri, + @ShellOption(value = { "-m", "--metadata-uri", "--metadataUri"}, help = "Metadata URI for the application artifact", defaultValue = ShellOption.NULL) String metadataUri, @ShellOption(help = "force update if application is already registered (only if not in use)", defaultValue = "false") boolean force) { - - appRegistryOperations().register(name, type, uri, metadataUri, force); - - return String.format(("Successfully registered application '%s:%s'"), type, name); + appRegistryOperations().register(name, type, bootVersion, uri, metadataUri, force); + return String.format(("Successfully registered application '%s:%s%s"), type, name, + bootVersion == null ? "" : " (boot " + bootVersion.getBootVersion() + ")"); } @ShellMethod(key = LIST_APPLICATIONS, value = "List all registered applications") diff --git a/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/converter/AppBootVersionConverter.java b/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/converter/AppBootVersionConverter.java new file mode 100644 index 0000000000..4a86609bde --- /dev/null +++ b/spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/converter/AppBootVersionConverter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.dataflow.shell.converter; + +import org.springframework.cloud.dataflow.core.AppBootSchemaVersion; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +/** + * Converts strings to {@link AppBootSchemaVersion} + * + * @author Chris Bono + */ +@Component +public class AppBootVersionConverter implements Converter { + + @Override + public AppBootSchemaVersion convert(String value) { + return value != null ? AppBootSchemaVersion.fromBootVersion(value) : null; + } +} diff --git a/spring-cloud-dataflow-shell-core/src/test/java/org/springframework/cloud/dataflow/shell/ShellCommandsTests.java b/spring-cloud-dataflow-shell-core/src/test/java/org/springframework/cloud/dataflow/shell/ShellCommandsTests.java index 5c7c91e649..5b42207bf2 100644 --- a/spring-cloud-dataflow-shell-core/src/test/java/org/springframework/cloud/dataflow/shell/ShellCommandsTests.java +++ b/spring-cloud-dataflow-shell-core/src/test/java/org/springframework/cloud/dataflow/shell/ShellCommandsTests.java @@ -69,6 +69,7 @@ public void unregisterAll() { @Test public void testSingleFileCommand() { String commandFiles = toAbsolutePaths("commands/registerTask_timestamp.txt"); + // TODO add boot 3 checks assertThat(runShell(commandFiles)).isTrue(); assertAppExists("timestamp", ApplicationType.task); }