Publishing of the Backend-API description via SwaggerUI (solution incl.) #4820
Description
Problem description:
It would be nice to have SwaggerUI available in SCDF Server.
Solution description:
There are only a small amount of adjustments required to publish the SwaggerUI:
pom.xml (Include spring.factories file and add dependency - version might be added in spring-cloud-dataflow-parent):
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.6</version>
</dependency>
....
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>**/*.factories</include>
</includes>
</resource>
....
</build
Additions in DataflowOAuthSecurityConfiguration
private static final String SWAGGER_UI_CONTEXT = "/swagger-ui/**";
@Value("${springdoc.api-docs.path:/v3/api-docs}")
private String apiDocsPath;
@Value("${springdoc.swagger-ui.path:/swagger-ui.html}")
private String swaggerUiPath;
@Value("${springdoc.webjars.prefix:/webjars}")
private String webjarsPrefix;
@Value("${springdoc.swagger-ui.configUrl:/v3/api-docs/swagger-config}")
private String swaggerUiConfig;
@Value("${springdoc.swagger-ui.validatorUrl:validator.swagger.io/validator}")
private String swaggerUiValidatorUrl;
@Value("${springdoc.swagger-ui.oauth2RedirectUrl:/swagger-ui/oauth2-redirect.html}")
private String swaggerUiOAuth2RedirectUrl;
@Override
public void configure(WebSecurity web) {
String apiDocsPathContext = StringUtils.substringBeforeLast(apiDocsPath, "/");
web.ignoring().antMatchers(
SWAGGER_UI_CONTEXT,
swaggerUiPath,
webjarsPrefix,
webjarsPrefix + "/**",
swaggerUiConfig,
swaggerUiValidatorUrl,
swaggerUiOAuth2RedirectUrl,
apiDocsPathContext + "/**");
}
New class DataFlowSwaggerApiDocsJsonDecodeFilter (Required because of issue springdoc/springdoc-openapi#624 (comment))
@Component
@Order
public class DataFlowSwaggerApiDocsJsonDecodeFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(DataFlowSwaggerApiDocsJsonDecodeFilter.class);
@Value("${springdoc.api-docs.path:/v3/api-docs}")
private String apiDocsPath;
@Value("${springdoc.swagger-ui.configUrl:/v3/api-docs/swagger-config}")
private String swaggerUiConfig;
@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);
String apiDocsPathContext = StringUtils.substringBeforeLast(apiDocsPath, "/");
String swaggerUiConfigContext = StringUtils.substringBeforeLast(swaggerUiConfig, "/");
if (httpServletRequestWrapper.getServletPath().startsWith(apiDocsPathContext) ||
httpServletRequestWrapper.getServletPath().startsWith(swaggerUiConfigContext)) {
// 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);
content = StringUtils.stripStart(content, "\"");
content = StringUtils.stripEnd(content, "\"");
if (LOG.isTraceEnabled()) {
LOG.trace("Using decoded JSON for serving api-docs: {}", content);
}
outputStream.write(StringEscapeUtils.unescapeJson(content).getBytes(StandardCharsets.UTF_8));
} else {
// all other scdf related api calls do nothing.
chain.doFilter(request, response);
}
}
}
New class DataFlowSwaggerApiEnvironmentPostProcessor (So that Swagger and Springdoc are disabled by default)
public class DataFlowSwaggerApiEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String SPRINGDOC_API_DOCS_ENABLED_KEY = "springdoc.api-docs.enabled";
private static final String SWAGGER_UI_ENABLED_KEY = "springdoc.swagger-ui.enabled";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Optional<Object> apiDocsEnabledOptional = environment.getPropertySources().stream()
.filter(p -> p.containsProperty(SPRINGDOC_API_DOCS_ENABLED_KEY))
.map(p -> p.getProperty(SPRINGDOC_API_DOCS_ENABLED_KEY))
.findAny();
// Apply default properties
if (!apiDocsEnabledOptional.isPresent()) {
applyDisableFeaturePropertySources(environment, "apiDocsDisabled", SPRINGDOC_API_DOCS_ENABLED_KEY);
}
Optional<Object> swaggerUiEnabledOptional = environment.getPropertySources().stream()
.filter(p -> p.containsProperty(SWAGGER_UI_ENABLED_KEY))
.map(p -> p.getProperty(SWAGGER_UI_ENABLED_KEY))
.findAny();
if (!(swaggerUiEnabledOptional.isPresent())) {
applyDisableFeaturePropertySources(environment, "swaggerUiDisabled", SWAGGER_UI_ENABLED_KEY);
}
}
private void applyDisableFeaturePropertySources(ConfigurableEnvironment environment, String name, String key) {
Properties properties = new Properties();
properties.setProperty(key, "false");
PropertiesPropertySource propertiesPropertySource = new PropertiesPropertySource(name, properties);
environment.getPropertySources().addLast(propertiesPropertySource);
}
}
META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.dataflow.server.processors.DataFlowSwaggerApiEnvironmentPostProcessor
Description of alternatives:
I guess openapi is very common for use and it is Apache Software License so free to use. No alternatives required.
Additional context:
The default values for the properties were determined from https://springdoc.org/#properties
For normal SwaggerAPI and Springdocs are enabled by default - I changed it so that if no properties are present the API descriptions are disabled by default.