Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

feature: springdoc integration #4836

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<wavefront-spring-boot-bom.version>2.2.0</wavefront-spring-boot-bom.version>
<spring-cloud-dataflow-apps-docs-plugin.version>1.0.4</spring-cloud-dataflow-apps-docs-plugin.version>
<spring-cloud-dataflow-apps-metadata-plugin.version>1.0.4</spring-cloud-dataflow-apps-metadata-plugin.version>
<springdoc-openapi-ui.version>1.6.6</springdoc-openapi-ui.version>
</properties>
<modules>
<module>spring-cloud-dataflow-container-registry</module>
Expand Down Expand Up @@ -245,6 +246,11 @@
<artifactId>aws-java-sdk-ecr</artifactId>
<version>${aws-java-sdk-ecr.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc-openapi-ui.version}</version>
</dependency>
<!-- only used for dataflow managed stream applications, e.g., tasklauncher -->
<dependency>
<groupId>com.wavefront</groupId>
Expand Down
4 changes: 4 additions & 0 deletions spring-cloud-dataflow-server-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2016-2017 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.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* Properties to read the spring doc configuration and provides it to some Spring Cloud Data Flow Server
* classes.
*/
@Configuration
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocConfigurationProperties {

public static final String SWAGGER_UI_CONTEXT = "/swagger-ui/**";

private Webjars webjars;

private ApiDocs apiDocs;

private SwaggerUi swaggerUi;

public static class Webjars {
private String prefix = "/webjars";

public String getPrefix() {
return prefix;
}

public void setPrefix(String prefix) {
this.prefix = prefix;
}
}

public static class ApiDocs {
private String path = "/v3/api-docs";

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}
}

public static class SwaggerUi {
private String path = "/swagger-ui.html";

private String configUrl = "/v3/api-docs/swagger-config";

private String validatorUrl = "validator.swagger.io/validator";

private String oauth2RedirectUrl = "/swagger-ui/oauth2-redirect.html";

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

public String getConfigUrl() {
return configUrl;
}

public void setConfigUrl(String configUrl) {
this.configUrl = configUrl;
}

public String getValidatorUrl() {
return validatorUrl;
}

public void setValidatorUrl(String validatorUrl) {
this.validatorUrl = validatorUrl;
}

public String getOauth2RedirectUrl() {
return oauth2RedirectUrl;
}

public void setOauth2RedirectUrl(String oauth2RedirectUrl) {
this.oauth2RedirectUrl = oauth2RedirectUrl;
}
}

public Webjars getWebjars() {
return webjars;
}

public void setWebjars(Webjars webjars) {
this.webjars = webjars;
}

public ApiDocs getApiDocs() {
return apiDocs;
}

public void setApiDocs(ApiDocs apiDocs) {
this.apiDocs = apiDocs;
}

public SwaggerUi getSwaggerUi() {
return swaggerUi;
}

public void setSwaggerUi(SwaggerUi swaggerUi) {
this.swaggerUi = swaggerUi;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;

/**
* Makes SpringDoc public available without any authentication required.
*
* @author Tobias Soloschenko
*/
@Configuration
public class SpringDocWebSecurityCustomizer implements WebSecurityCustomizer {

private final SpringDocConfigurationProperties springDocConfigurationProperties;

public SpringDocWebSecurityCustomizer(SpringDocConfigurationProperties springDocConfigurationProperties){
this.springDocConfigurationProperties = springDocConfigurationProperties;
}

@Override
public void customize(WebSecurity webSecurity) {
String apiDocsPath = springDocConfigurationProperties.getApiDocs().getPath();
String apiDocsPathContext = apiDocsPath.substring(0, apiDocsPath.lastIndexOf("/"));

webSecurity.ignoring().antMatchers(
SpringDocConfigurationProperties.SWAGGER_UI_CONTEXT,
springDocConfigurationProperties.getSwaggerUi().getPath(),
springDocConfigurationProperties.getSwaggerUi().getConfigUrl(),
springDocConfigurationProperties.getSwaggerUi().getValidatorUrl(),
springDocConfigurationProperties.getSwaggerUi().getOauth2RedirectUrl(),
springDocConfigurationProperties.getWebjars().getPrefix(),
springDocConfigurationProperties.getWebjars().getPrefix() + "/**",
apiDocsPathContext + "/**");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2016-2018 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.cloud.dataflow.server.config.SpringDocConfigurationProperties;
import org.springframework.stereotype.Component;
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
*/
@Component
public class SpringDocJsonDecodeFilter implements Filter {

private static final Logger LOG = LoggerFactory.getLogger(SpringDocJsonDecodeFilter.class);

private final SpringDocConfigurationProperties springDocConfigurationProperties;

public SpringDocJsonDecodeFilter(SpringDocConfigurationProperties springDocConfigurationProperties){
this.springDocConfigurationProperties = springDocConfigurationProperties;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String apiDocsPath = springDocConfigurationProperties.getApiDocs().getPath();
String swaggerUiConfigUrl = springDocConfigurationProperties.getSwaggerUi().getConfigUrl();

String apiDocsPathContext = apiDocsPath.substring(0, apiDocsPath.lastIndexOf("/"));
String swaggerUiConfigContext = swaggerUiConfigUrl.substring(0, swaggerUiConfigUrl.lastIndexOf("/"));

if (((HttpServletRequest) request).getServletPath().startsWith(apiDocsPathContext)
|| ((HttpServletRequest) request).getServletPath().startsWith(swaggerUiConfigContext)) {

final HttpServletRequestWrapper httpServletRequestWrapper = new HttpServletRequestWrapper((HttpServletRequest) request);
final ContentCachingResponseWrapper httpServletResponseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

// if api-docs path is requested, use wrapper classes, so that the body gets cached.
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));
} else {
// all other scdf related api calls do nothing.
chain.doFilter(request, response);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
management:
metrics:
tags:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.junit.Before;
import org.junit.Test;

import org.springframework.cloud.dataflow.server.config.SpringDocConfigurationProperties;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import static org.junit.Assert.assertEquals;

public class SpringDocJsonDecodeFilterTest {

private SpringDocJsonDecodeFilter springDocJsonDecodeFilter;

private SpringDocConfigurationProperties springDocConfigurationProperties;

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\"}}";

@Before
public void setup() {
springDocConfigurationProperties = new SpringDocConfigurationProperties();
springDocConfigurationProperties.setApiDocs(new SpringDocConfigurationProperties.ApiDocs());
springDocConfigurationProperties.setSwaggerUi(new SpringDocConfigurationProperties.SwaggerUi());
springDocJsonDecodeFilter = new SpringDocJsonDecodeFilter(springDocConfigurationProperties);
}

@Test
public void doFilterTest() throws ServletException, IOException {
MockHttpServletResponse mockHttpServletResponse = new MockHttpServletResponse();
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
mockHttpServletRequest.setServletPath(springDocConfigurationProperties.getApiDocs().getPath());
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);
}
};
springDocJsonDecodeFilter.doFilter(mockHttpServletRequest, mockHttpServletResponse, mockFilterChain);
assertEquals(OPENAPI_JSON_UNESCAPED_CONTENT, mockHttpServletResponse.getContentAsString());
}

}