diff --git a/pom.xml b/pom.xml
index 4db0dd398d..d0ca988bde 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,6 +90,7 @@
2.2.0
1.0.4
1.0.4
+ 1.6.6
spring-cloud-dataflow-container-registry
@@ -245,6 +246,11 @@
aws-java-sdk-ecr
${aws-java-sdk-ecr.version}
+
+ org.springdoc
+ springdoc-openapi-ui
+ ${springdoc-openapi-ui.version}
+
com.wavefront
diff --git a/spring-cloud-dataflow-server-core/pom.xml b/spring-cloud-dataflow-server-core/pom.xml
index fba56bd290..43748dee58 100644
--- a/spring-cloud-dataflow-server-core/pom.xml
+++ b/spring-cloud-dataflow-server-core/pom.xml
@@ -39,6 +39,10 @@
com.zaxxer
HikariCP
+
+ org.springdoc
+ springdoc-openapi-ui
+
org.springframework.boot
spring-boot-starter-data-jpa
diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java
index cf54311073..8ecd12990b 100644
--- a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java
+++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/DataflowOAuthSecurityConfiguration.java
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.cloud.dataflow.server.config;
import org.springframework.cloud.common.security.OAuthSecurityConfiguration;
diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/SpringDocAutoConfiguration.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/SpringDocAutoConfiguration.java
new file mode 100644
index 0000000000..1261b6e0da
--- /dev/null
+++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/config/SpringDocAutoConfiguration.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 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.server.config;
+
+import org.springdoc.core.SpringDocConfigProperties;
+import org.springdoc.core.SwaggerUiConfigProperties;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.cloud.dataflow.server.support.SpringDocJsonDecodeFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
+
+/**
+ * Makes SpringDoc public available without any authentication required by initializing a {@link WebSecurityCustomizer} and
+ * applying all path of SpringDoc to be ignored. Also applies a filter registration bean to unescape JSON content for the
+ * SpringDoc frontend.
+ *
+ * @author Tobias Soloschenko
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnBean({ SpringDocConfigProperties.class, SwaggerUiConfigProperties.class })
+public class SpringDocAutoConfiguration {
+
+ public static final String SWAGGER_UI_CONTEXT = "/swagger-ui/**";
+
+ private final SpringDocConfigProperties springDocConfigProperties;
+
+ private final SwaggerUiConfigProperties swaggerUiConfigProperties;
+
+ /**
+ * Creates the SpringDocConfiguration with the given properties.
+ *
+ * @param springDocConfigProperties the spring doc config properties
+ * @param swaggerUiConfigProperties the swagger ui config properties
+ */
+ public SpringDocAutoConfiguration(SpringDocConfigProperties springDocConfigProperties, SwaggerUiConfigProperties swaggerUiConfigProperties) {
+ this.springDocConfigProperties = springDocConfigProperties;
+ this.swaggerUiConfigProperties = swaggerUiConfigProperties;
+ }
+
+ /**
+ * Creates a web security customizer for the spring security which makes the SpringDoc frontend public available.
+ *
+ * @return a web security customizer with security settings for SpringDoc
+ */
+ @Bean("springDocWebSecurityCustomizer")
+ public WebSecurityCustomizer springDocCustomizer() {
+ return (webSecurity -> webSecurity.ignoring().antMatchers(
+ SWAGGER_UI_CONTEXT,
+ getApiDocsPathContext() + "/**",
+ swaggerUiConfigProperties.getPath(),
+ swaggerUiConfigProperties.getConfigUrl(),
+ swaggerUiConfigProperties.getValidatorUrl(),
+ swaggerUiConfigProperties.getOauth2RedirectUrl(),
+ springDocConfigProperties.getWebjars().getPrefix(),
+ springDocConfigProperties.getWebjars().getPrefix() + "/**"));
+ }
+
+ /**
+ * Applies {@link SpringDocJsonDecodeFilter} to the filter chain which decodes the JSON of ApiDocs and SwaggerUi so that the SpringDoc frontend is able
+ * to read it. Spring Cloud Data Flow however requires the JSON to be escaped and wrapped into quotes, because the
+ * Angular Ui frontend is using it that way.
+ *
+ * @return a filter registration bean which unescapes the content of the JSON endpoints of SpringDoc before it is returned.
+ */
+ @Bean
+ public FilterRegistrationBean springDocJsonDecodeFilterRegistration() {
+ String apiDocsPathContext = getApiDocsPathContext();
+ String swaggerUiConfigContext = getSwaggerUiConfigContext();
+
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
+ registrationBean.setFilter(new SpringDocJsonDecodeFilter());
+ registrationBean.addUrlPatterns(apiDocsPathContext, apiDocsPathContext + "/*", swaggerUiConfigContext,
+ swaggerUiConfigContext + "/*");
+
+ return registrationBean;
+ }
+
+ /**
+ * Gets the SwaggerUi config context. For example the default configuration for the SwaggerUi config is /v3/api-docs/swagger-config
+ * which results in a context of /v3/api-docs.
+ *
+ * @return the SwaggerUi config path context
+ */
+ private String getSwaggerUiConfigContext() {
+ String swaggerUiConfigUrl = swaggerUiConfigProperties.getConfigUrl();
+ return swaggerUiConfigUrl.substring(0, swaggerUiConfigUrl.lastIndexOf("/"));
+ }
+
+ /**
+ * Gets the ApiDocs context path. For example the default configuration for the ApiDocs path is /v3/api-docs
+ * which results in a context of /v3.
+ *
+ * @return the api docs path context
+ */
+ private String getApiDocsPathContext() {
+ String apiDocsPath = springDocConfigProperties.getApiDocs().getPath();
+ return apiDocsPath.substring(0, apiDocsPath.lastIndexOf("/"));
+ }
+}
diff --git a/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/support/SpringDocJsonDecodeFilter.java b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/support/SpringDocJsonDecodeFilter.java
new file mode 100644
index 0000000000..4afa63fa3c
--- /dev/null
+++ b/spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/support/SpringDocJsonDecodeFilter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 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.server.support;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.text.StringEscapeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+/**
+ * Sets up a filter that unescapes the JSON content of the API endpoints of OpenApi and Swagger.
+ * This is similar to the issue mentioned here: https://github.com/springdoc/springdoc-openapi/issues/624
+ * Spring Cloud Data Flow however needs the escaped JSON to show task logs in the UI.
+ *
+ * @author Tobias Soloschenko
+ */
+public class SpringDocJsonDecodeFilter implements Filter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpringDocJsonDecodeFilter.class);
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ final HttpServletRequestWrapper httpServletRequestWrapper = new HttpServletRequestWrapper((HttpServletRequest) request);
+ final ContentCachingResponseWrapper httpServletResponseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
+
+ chain.doFilter(httpServletRequestWrapper, httpServletResponseWrapper);
+
+ ServletOutputStream outputStream = httpServletResponseWrapper.getResponse().getOutputStream();
+
+ LOG.debug("Request for Swagger api-docs detected - unescaping json content.");
+ String content = new String(httpServletResponseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
+ // Replaces all escaped quotes
+ content = StringEscapeUtils.unescapeJson(content);
+ // Replaces first and last quote
+ content = content.substring(1, content.length() - 1);
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Using decoded JSON for serving api-docs: {}", content);
+ }
+ outputStream.write(content.getBytes(StandardCharsets.UTF_8));
+
+ }
+}
diff --git a/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml b/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml
index 0fac72e80c..6e879e1cd4 100644
--- a/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml
+++ b/spring-cloud-dataflow-server-core/src/main/resources/META-INF/dataflow-server-defaults.yml
@@ -243,3 +243,8 @@ spring:
# Tools
- POST /tools/** => hasRole('ROLE_VIEW')
+springdoc:
+ api-docs:
+ enabled: false
+ swagger-ui:
+ enabled: false
diff --git a/spring-cloud-dataflow-server-core/src/main/resources/META-INF/spring.factories b/spring-cloud-dataflow-server-core/src/main/resources/META-INF/spring.factories
index 3ddae42b73..1a69328580 100644
--- a/spring-cloud-dataflow-server-core/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-dataflow-server-core/src/main/resources/META-INF/spring.factories
@@ -3,4 +3,5 @@ org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.dataflow.server.config.MetricsReplicationEnvironmentPostProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.dataflow.server.config.DataFlowServerAutoConfiguration,\
- org.springframework.cloud.dataflow.server.config.DataFlowControllerAutoConfiguration
+ org.springframework.cloud.dataflow.server.config.DataFlowControllerAutoConfiguration, \
+ org.springframework.cloud.dataflow.server.config.SpringDocAutoConfiguration
diff --git a/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/support/SpringDocJsonDecodeFilterTest.java b/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/support/SpringDocJsonDecodeFilterTest.java
new file mode 100644
index 0000000000..5dc6dff9a5
--- /dev/null
+++ b/spring-cloud-dataflow-server-core/src/test/java/org/springframework/cloud/dataflow/server/support/SpringDocJsonDecodeFilterTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.server.support;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * This is a test for {@link SpringDocJsonDecodeFilter} to check if the json content is decoded correctly.
+ * @author Tobias Soloschenko
+ */
+public class SpringDocJsonDecodeFilterTest {
+
+ private static final String OPENAPI_JSON_ESCAPED_CONTENT = "\"{\\\"openapi:\\\"3.0.1\\\",\\\"info\\\":{\\\"title\\\":\\\"OpenAPI definition\\\",\\\"version\\\":\\\"v0\\\"}}\"";
+
+ private static final String OPENAPI_JSON_UNESCAPED_CONTENT = "{\"openapi:\"3.0.1\",\"info\":{\"title\":\"OpenAPI definition\",\"version\":\"v0\"}}";
+
+ @Test
+ public void doFilterTest() throws ServletException, IOException {
+ MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
+ MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
+ MockFilterChain mockFilterChain = new MockFilterChain() {
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+ response.getOutputStream().write(OPENAPI_JSON_ESCAPED_CONTENT.getBytes(StandardCharsets.UTF_8));
+ super.doFilter(request, response);
+ }
+ };
+ new SpringDocJsonDecodeFilter().doFilter(mockHttpServletRequest, mockHttpServletResponse, mockFilterChain);
+ assertThat(mockHttpServletResponse.getContentAsString()).isEqualTo(OPENAPI_JSON_UNESCAPED_CONTENT);
+ }
+
+}