frankgh commented on code in PR #239: URL: https://github.com/apache/cassandra-sidecar/pull/239#discussion_r2232185048
########## server/src/main/java/org/apache/cassandra/sidecar/config/OpenApiConfiguration.java: ########## @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.config; + +/** + * Configuration for OpenAPI documentation + */ +public interface OpenApiConfiguration +{ + /** + * @return whether OpenAPI documentation is enabled + */ + boolean enabled(); + + /** + * @return the title for the API documentation + */ + String title(); + + /** + * @return the description for the API documentation + */ + String description(); + + /** + * @return the version of the API + */ + String version(); Review Comment: Use `org.apache.cassandra.sidecar.common.server.utils.SidecarVersionProvider` instead for the version ########## server/src/main/java/org/apache/cassandra/sidecar/docs/OpenApiDocumentationGenerator.java: ########## @@ -0,0 +1,1343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.docs; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.yaml.OpenApiConfigurationImpl; + +/** + * Utility class for generating OpenAPI documentation files + */ +public class OpenApiDocumentationGenerator +{ + private static final String HTML_TEMPLATE = + "<!DOCTYPE html>\n" + + "<html lang=\"en\">\n" + + "<head>\n" + + " <meta charset=\"UTF-8\">\n" + + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + + " <title>Cassandra Sidecar API Documentation</title>\n" + + " <link rel=\"stylesheet\" type=\"text/css\" href=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css\" />\n" + + " <style>\n" + + " html {\n" + + " box-sizing: border-box;\n" + + " overflow: -moz-scrollbars-vertical;\n" + + " overflow-y: scroll;\n" + + " }\n" + + " *, *:before, *:after {\n" + + " box-sizing: inherit;\n" + + " }\n" + + " body {\n" + + " margin:0;\n" + + " background: #fafafa;\n" + + " }\n" + + " .swagger-ui .topbar { display: none; }\n" + + " .swagger-ui .info { margin: 50px 0; }\n" + + " .swagger-ui .info hgroup.main { margin: 0 0 20px 0; }\n" + + " .swagger-ui .info h1 { color: #3b4151; }\n" + + " </style>\n" + + "</head>\n" + + "<body>\n" + + " <div id=\"swagger-ui\"></div>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js\"></script>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js\"></script>\n" + + " <script>\n" + + " const spec = %s;\n" + + " window.onload = function() {\n" + + " SwaggerUIBundle({\n" + + " spec: spec,\n" + + " dom_id: '#swagger-ui',\n" + + " deepLinking: true,\n" + + " presets: [\n" + + " SwaggerUIBundle.presets.apis,\n" + + " SwaggerUIStandalonePreset\n" + + " ],\n" + + " plugins: [\n" + + " SwaggerUIBundle.plugins.DownloadUrl\n" + + " ],\n" + + " layout: \"StandaloneLayout\",\n" + + " defaultModelsExpandDepth: 1,\n" + + " defaultModelExpandDepth: 1\n" + + " });\n" + + " };\n" + + " </script>\n" + + "</body>\n" + + "</html>"; + + /** + * Generates OpenAPI documentation files + * + * @param args command line arguments: outputDir + */ + public static void main(String[] args) throws IOException + { + if (args.length < 1) + { + throw new IllegalArgumentException("Usage: OpenApiDocumentationGenerator <output-directory>"); + } + + String outputDir = args[0]; + Path outputPath = Paths.get(outputDir); + + // Create output directory if it doesn't exist + Files.createDirectories(outputPath); + + // Generate OpenAPI specification + OpenApiConfiguration config = new OpenApiConfigurationImpl(); + var openApi = createOpenApiFromConfig(config); + + // Scan for annotated handler classes + openApi = scanForAnnotations(openApi); + + // Generate JSON file + String jsonSpec = Json.pretty(openApi); + Path jsonFile = outputPath.resolve("openapi.json"); + Files.write(jsonFile, jsonSpec.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", jsonFile.toAbsolutePath()); + + // Generate YAML file + String yamlSpec = Yaml.pretty(openApi); + Path yamlFile = outputPath.resolve("openapi.yaml"); + Files.write(yamlFile, yamlSpec.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", yamlFile.toAbsolutePath()); + + // Generate HTML file with embedded specification + String htmlContent = HTML_TEMPLATE.replace("%s", jsonSpec); + Path htmlFile = outputPath.resolve("api-docs.html"); + Files.write(htmlFile, htmlContent.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", htmlFile.toAbsolutePath()); + + System.out.printf("OpenAPI documentation generated successfully!%n"); + System.out.printf("Open %s in your browser to view the documentation.%n", htmlFile.toAbsolutePath()); + } + + /** + * Creates an OpenAPI configuration from the given configuration + */ + private static OpenAPI createOpenApiFromConfig(OpenApiConfiguration config) + { + OpenAPI openApi = new OpenAPI(); + + // Set basic info + io.swagger.v3.oas.models.info.Info info = new io.swagger.v3.oas.models.info.Info(); Review Comment: why not import the class? ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.OpenAPI; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * Handler that serves the OpenAPI specification + */ +@Tag(name = "Documentation", description = "API documentation endpoints") +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private final OpenApiConfiguration openApiConfig; + + @Inject + public OpenApiHandler(SidecarConfiguration sidecarConfiguration) + { + this.openApiConfig = sidecarConfiguration.openApiConfiguration(); + } + + @Operation( + summary = "Get OpenAPI specification", + description = "Returns the OpenAPI specification for this API in JSON format" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "OpenAPI specification retrieved successfully", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = OpenAPI.class) + ) + ) + }) + @Override + public void handle(RoutingContext context) + { + OpenAPI openAPI = createOpenApiFromConfig(openApiConfig); + String openApiJson = Json.pretty(openAPI); + + context.response() + .putHeader("Content-Type", "application/json") + .end(openApiJson); Review Comment: NIT: ```suggestion context.json(openApiJson); ``` ########## server/build.gradle: ########## @@ -240,3 +254,62 @@ checkstyleContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } spotbugsContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } check.dependsOn containerTest, integrationTest, jacocoTestReport + +// Task to generate OpenAPI documentation +tasks.register('generateOpenApiDocs', JavaExec) { + group = 'documentation' + description = 'Generates OpenAPI documentation files (JSON, YAML, HTML)' + + dependsOn compileJava + classpath = sourceSets.main.runtimeClasspath + mainClass = 'org.apache.cassandra.sidecar.docs.OpenApiDocumentationGenerator' + + def outputDir = layout.buildDirectory.dir('docs/openapi').get().asFile + args outputDir.absolutePath + + outputs.dir outputDir + + doFirst { + if (!outputDir.exists()) { + outputDir.mkdirs() + } + } + + doLast { + println "\nOpenAPI documentation generated at:" + println " JSON: ${outputDir}/openapi.json" + println " YAML: ${outputDir}/openapi.yaml" + println " HTML: ${outputDir}/api-docs.html" Review Comment: should this be sidecar-api-docs instead? ########## server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java: ########## @@ -118,6 +192,14 @@ VertxRoute cassandraSchemaRoute(RouteBuilder.Factory factory, } @Deprecated + @GET + @Path(ApiEndpointsV1.DEPRECATED_ALL_KEYSPACES_SCHEMA_ROUTE) + @Operation(summary = "Get all keyspaces schema (deprecated)", Review Comment: Also mark it as deprecated in the spec? ```suggestion @Operation(summary = "Get all keyspaces schema (deprecated)", deprecated = true, ``` ########## server/src/test/java/org/apache/cassandra/sidecar/docs/OpenApiDocumentationGeneratorTest.java: ########## @@ -0,0 +1,386 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.docs; + +import java.io.IOException; +import java.nio.file.Files; +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 com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; Review Comment: we should only use `org.assertj.core.api.Assertions.assertThat` ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Singleton; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler that serves the OpenAPI specification + * + * This handler serves the OpenAPI specification generated by the Gradle generateOpenApiSpec task + * which processes JAX-RS/MicroProfile annotations to create comprehensive API documentation. + */ +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiHandler.class); + + @Override + public void handle(RoutingContext context) + { + try + { + // Determine format from request path or Accept header + boolean isYaml = isYamlRequest(context); + String openApiContent = loadGeneratedOpenApiSpec(isYaml); + String contentType = isYaml ? "application/yaml" : "application/json"; + + context.response() + .putHeader("Content-Type", contentType) + .end(openApiContent); + } + catch (Exception e) + { + LOGGER.warn("Failed to load generated OpenAPI specification, falling back to basic config", e); + context.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()) + .putHeader("Content-Type", "application/json") + .end(); + } + } + + /** + * Determines if the request is for YAML format based on path or Accept header + */ + private boolean isYamlRequest(RoutingContext context) + { + String path = context.request().path(); + if (path != null && path.endsWith(".yaml")) + { + return true; + } + + String acceptHeader = context.request().getHeader("Accept"); + return acceptHeader != null && acceptHeader.contains("application/yaml"); + } + + /** + * Loads the generated OpenAPI specification from resources or build output + */ + private String loadGeneratedOpenApiSpec(boolean isYaml) throws IOException + { + // First try to load from classpath resources (for packaged deployments) + String content = loadFromClasspathResource(isYaml); + if (content != null) + { + return content; + } + + // Fallback to file system paths (for development) + return loadFromFileSystem(isYaml); + } + + /** + * Attempts to load the OpenAPI spec from classpath resources + */ + private String loadFromClasspathResource(boolean isYaml) throws IOException + { + String fileName = isYaml ? "openapi.yaml" : "openapi.json"; + String resourcePath = "/openapi/" + fileName; + + InputStream inputStream = getClass().getResourceAsStream(resourcePath); + if (inputStream == null) + { + return null; + } + + LOGGER.debug("Loading OpenAPI specification from classpath: {}", resourcePath); + try + { + byte[] bytes = inputStream.readAllBytes(); + String content = new String(bytes, StandardCharsets.UTF_8); + LOGGER.info("Loaded OpenAPI specification from classpath resource"); + return content; + } + finally + { + inputStream.close(); Review Comment: we should just use vertx filesystem operations here, it allows you to read from the resources as well ########## conf/sidecar.yaml: ########## @@ -398,3 +398,14 @@ live_migration: - glob:${DATA_FILE_DIR}/*/*/snapshots # Excludes snapshot directories in data folder to copy to destination migration_map: # Map of source and destination Cassandra instances # localhost1: localhost4 # This entry says that localhost1 will be migrated to localhost4 + +# OpenAPI documentation configuration +openapi: + enabled: true + title: "Cassandra Sidecar API" + description: "REST API for managing Apache Cassandra operations" + version: "1.0.0" Review Comment: Ideally, we should read this value from the version provider and not configuration. ```suggestion version: "0.1.0" ``` ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java: ########## @@ -81,8 +81,8 @@ protected void handleInternal(RoutingContext context, throw new IllegalStateException("Unknown state: " + request.state()); } }) - .onSuccess(ignored -> context.json(OK_STATUS)) - .onFailure(cause -> processFailure(cause, context, host, remoteAddress, request)); + .onSuccess(ignored -> context.json(OK_STATUS)) Review Comment: unrelated to this PR, please revert ########## server/src/test/java/org/apache/cassandra/sidecar/HelperTestModules.java: ########## @@ -94,7 +94,8 @@ protected void configure() .thenAnswer(invocation -> instanceMetadataList.stream() .filter(instanceMetadata -> invocation.getArgument(0).equals(instanceMetadata.id())) .findFirst() - .orElseThrow(() -> new NoSuchCassandraInstanceException("No Cassandra instance exists with given ID"))); + .orElseThrow(() -> new NoSuchCassandraInstanceException( Review Comment: unrelated, please revert ########## server/src/main/java/org/apache/cassandra/sidecar/config/yaml/OpenApiConfigurationImpl.java: ########## @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.config.yaml; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; + +/** + * Implementation of {@link OpenApiConfiguration} + */ +public class OpenApiConfigurationImpl implements OpenApiConfiguration +{ + public static final boolean DEFAULT_ENABLED = true; + public static final String DEFAULT_TITLE = "Cassandra Sidecar API"; + public static final String DEFAULT_DESCRIPTION = "REST API for managing Apache Cassandra operations"; + public static final String DEFAULT_VERSION = "1.0.0"; + public static final String DEFAULT_LICENSE_NAME = "Apache License 2.0"; + public static final String DEFAULT_LICENSE_URL = "https://www.apache.org/licenses/LICENSE-2.0"; + public static final String DEFAULT_SERVER_URL = "http://localhost:9043/api/v1"; + public static final String DEFAULT_SERVER_DESCRIPTION = "Development server"; + + @JsonProperty(value = "enabled", defaultValue = "true") + protected final boolean enabled; + + @JsonProperty(value = "title", defaultValue = DEFAULT_TITLE) + protected final String title; + + @JsonProperty(value = "description", defaultValue = DEFAULT_DESCRIPTION) + protected final String description; + + @JsonProperty(value = "version", defaultValue = DEFAULT_VERSION) + protected final String version; + + @JsonProperty(value = "license_name", defaultValue = DEFAULT_LICENSE_NAME) + protected final String licenseName; + + @JsonProperty(value = "license_url", defaultValue = DEFAULT_LICENSE_URL) + protected final String licenseUrl; + + @JsonProperty(value = "server_url", defaultValue = DEFAULT_SERVER_URL) + protected final String serverUrl; + + @JsonProperty(value = "server_description", defaultValue = DEFAULT_SERVER_DESCRIPTION) + protected final String serverDescription; + + public OpenApiConfigurationImpl() + { + this(DEFAULT_ENABLED, DEFAULT_TITLE, DEFAULT_DESCRIPTION, DEFAULT_VERSION, + DEFAULT_LICENSE_NAME, DEFAULT_LICENSE_URL, DEFAULT_SERVER_URL, DEFAULT_SERVER_DESCRIPTION); + } + + public OpenApiConfigurationImpl(boolean enabled, + String title, + String description, + String version, + String licenseName, + String licenseUrl, + String serverUrl, + String serverDescription) + { + this.enabled = enabled; + this.title = title; + this.description = description; + this.version = version; + this.licenseName = licenseName; + this.licenseUrl = licenseUrl; + this.serverUrl = serverUrl; + this.serverDescription = serverDescription; + } + + @Override + public boolean enabled() + { + return enabled; + } + + @Override + public String title() + { + return title; + } + + @Override + public String description() + { + return description; + } + + @Override + public String version() + { + return version; + } + + @Override + public String licenseName() + { + return licenseName; + } + + @Override + public String licenseUrl() + { + return licenseUrl; + } + + @Override + public String serverUrl() + { + return serverUrl; + } + + @Override + public String serverDescription() + { + return serverDescription; + } + + /** + * Builder class for {@link OpenApiConfigurationImpl} + */ + public static class Builder Review Comment: We only use a builder on specific cases for the configuration implementation files. This one is not even used. Let's remove it ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/restore/CreateRestoreJobHandler.java: ########## @@ -114,8 +114,7 @@ protected CreateRestoreJobRequestPayload extractParamsOrThrow(RoutingContext con } } - private Future<CreateRestoreJobRequestPayload> validatePayload(CreateRestoreJobRequestPayload - createRestoreJobRequestPayload) + private Future<CreateRestoreJobRequestPayload> validatePayload(CreateRestoreJobRequestPayload createRestoreJobRequestPayload) Review Comment: unrelated, please revert ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/WebJarHandler.java: ########## @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.oas.annotations.Hidden; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.StaticHandler; + +/** + * Handler that serves WebJar static assets + */ +@Hidden +@Singleton +public class WebJarHandler implements Handler<RoutingContext> +{ + private final StaticHandler staticHandler; + + @Inject + public WebJarHandler() + { + this.staticHandler = StaticHandler.create("META-INF/resources") + .setWebRoot("META-INF/resources") + .setCachingEnabled(true) + .setMaxAgeSeconds(86400); // Cache for 1 day Review Comment: should these be configurable? ########## server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java: ########## @@ -54,7 +54,9 @@ PeriodicTask healthCheckPeriodicTask(SidecarConfiguration configuration, VertxRoute sidecarHealthRoute(RouteBuilder.Factory factory) { return factory.builderForUnauthorizedRoute() - .handler(context -> context.json(ApiModule.OK_STATUS)) + .handler(context -> { Review Comment: unrelated, please revert ########## server/src/main/java/org/apache/cassandra/sidecar/docs/OpenApiDocumentationGenerator.java: ########## @@ -0,0 +1,1343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.docs; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.yaml.OpenApiConfigurationImpl; + +/** + * Utility class for generating OpenAPI documentation files + */ +public class OpenApiDocumentationGenerator +{ + private static final String HTML_TEMPLATE = Review Comment: should this be a resource instead? Any reason to hardcode it? ########## server/src/main/java/org/apache/cassandra/sidecar/docs/OpenApiDocumentationGenerator.java: ########## @@ -0,0 +1,1343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.docs; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.yaml.OpenApiConfigurationImpl; + +/** + * Utility class for generating OpenAPI documentation files + */ +public class OpenApiDocumentationGenerator +{ + private static final String HTML_TEMPLATE = + "<!DOCTYPE html>\n" + + "<html lang=\"en\">\n" + + "<head>\n" + + " <meta charset=\"UTF-8\">\n" + + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + + " <title>Cassandra Sidecar API Documentation</title>\n" + + " <link rel=\"stylesheet\" type=\"text/css\" href=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css\" />\n" + + " <style>\n" + + " html {\n" + + " box-sizing: border-box;\n" + + " overflow: -moz-scrollbars-vertical;\n" + + " overflow-y: scroll;\n" + + " }\n" + + " *, *:before, *:after {\n" + + " box-sizing: inherit;\n" + + " }\n" + + " body {\n" + + " margin:0;\n" + + " background: #fafafa;\n" + + " }\n" + + " .swagger-ui .topbar { display: none; }\n" + + " .swagger-ui .info { margin: 50px 0; }\n" + + " .swagger-ui .info hgroup.main { margin: 0 0 20px 0; }\n" + + " .swagger-ui .info h1 { color: #3b4151; }\n" + + " </style>\n" + + "</head>\n" + + "<body>\n" + + " <div id=\"swagger-ui\"></div>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js\"></script>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js\"></script>\n" + + " <script>\n" + + " const spec = %s;\n" + + " window.onload = function() {\n" + + " SwaggerUIBundle({\n" + + " spec: spec,\n" + + " dom_id: '#swagger-ui',\n" + + " deepLinking: true,\n" + + " presets: [\n" + + " SwaggerUIBundle.presets.apis,\n" + + " SwaggerUIStandalonePreset\n" + + " ],\n" + + " plugins: [\n" + + " SwaggerUIBundle.plugins.DownloadUrl\n" + + " ],\n" + + " layout: \"StandaloneLayout\",\n" + + " defaultModelsExpandDepth: 1,\n" + + " defaultModelExpandDepth: 1\n" + + " });\n" + + " };\n" + + " </script>\n" + + "</body>\n" + + "</html>"; + + /** + * Generates OpenAPI documentation files + * + * @param args command line arguments: outputDir + */ + public static void main(String[] args) throws IOException + { + if (args.length < 1) + { + throw new IllegalArgumentException("Usage: OpenApiDocumentationGenerator <output-directory>"); + } + + String outputDir = args[0]; + Path outputPath = Paths.get(outputDir); + + // Create output directory if it doesn't exist + Files.createDirectories(outputPath); + + // Generate OpenAPI specification + OpenApiConfiguration config = new OpenApiConfigurationImpl(); + var openApi = createOpenApiFromConfig(config); + + // Scan for annotated handler classes + openApi = scanForAnnotations(openApi); + + // Generate JSON file + String jsonSpec = Json.pretty(openApi); + Path jsonFile = outputPath.resolve("openapi.json"); + Files.write(jsonFile, jsonSpec.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", jsonFile.toAbsolutePath()); + + // Generate YAML file + String yamlSpec = Yaml.pretty(openApi); + Path yamlFile = outputPath.resolve("openapi.yaml"); + Files.write(yamlFile, yamlSpec.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", yamlFile.toAbsolutePath()); + + // Generate HTML file with embedded specification + String htmlContent = HTML_TEMPLATE.replace("%s", jsonSpec); + Path htmlFile = outputPath.resolve("api-docs.html"); + Files.write(htmlFile, htmlContent.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", htmlFile.toAbsolutePath()); + + System.out.printf("OpenAPI documentation generated successfully!%n"); + System.out.printf("Open %s in your browser to view the documentation.%n", htmlFile.toAbsolutePath()); + } + + /** + * Creates an OpenAPI configuration from the given configuration + */ + private static OpenAPI createOpenApiFromConfig(OpenApiConfiguration config) + { + OpenAPI openApi = new OpenAPI(); + + // Set basic info + io.swagger.v3.oas.models.info.Info info = new io.swagger.v3.oas.models.info.Info(); + info.setTitle(config.title()); + info.setDescription(config.description()); + info.setVersion(config.version()); + + // Set license info + io.swagger.v3.oas.models.info.License license = new io.swagger.v3.oas.models.info.License(); Review Comment: ditto ########## server/src/main/java/org/apache/cassandra/sidecar/docs/OpenApiDocumentationGenerator.java: ########## @@ -0,0 +1,1343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.docs; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.yaml.OpenApiConfigurationImpl; + +/** + * Utility class for generating OpenAPI documentation files + */ +public class OpenApiDocumentationGenerator +{ + private static final String HTML_TEMPLATE = + "<!DOCTYPE html>\n" + + "<html lang=\"en\">\n" + + "<head>\n" + + " <meta charset=\"UTF-8\">\n" + + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + + " <title>Cassandra Sidecar API Documentation</title>\n" + + " <link rel=\"stylesheet\" type=\"text/css\" href=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css\" />\n" + + " <style>\n" + + " html {\n" + + " box-sizing: border-box;\n" + + " overflow: -moz-scrollbars-vertical;\n" + + " overflow-y: scroll;\n" + + " }\n" + + " *, *:before, *:after {\n" + + " box-sizing: inherit;\n" + + " }\n" + + " body {\n" + + " margin:0;\n" + + " background: #fafafa;\n" + + " }\n" + + " .swagger-ui .topbar { display: none; }\n" + + " .swagger-ui .info { margin: 50px 0; }\n" + + " .swagger-ui .info hgroup.main { margin: 0 0 20px 0; }\n" + + " .swagger-ui .info h1 { color: #3b4151; }\n" + + " </style>\n" + + "</head>\n" + + "<body>\n" + + " <div id=\"swagger-ui\"></div>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js\"></script>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js\"></script>\n" + + " <script>\n" + + " const spec = %s;\n" + + " window.onload = function() {\n" + + " SwaggerUIBundle({\n" + + " spec: spec,\n" + + " dom_id: '#swagger-ui',\n" + + " deepLinking: true,\n" + + " presets: [\n" + + " SwaggerUIBundle.presets.apis,\n" + + " SwaggerUIStandalonePreset\n" + + " ],\n" + + " plugins: [\n" + + " SwaggerUIBundle.plugins.DownloadUrl\n" + + " ],\n" + + " layout: \"StandaloneLayout\",\n" + + " defaultModelsExpandDepth: 1,\n" + + " defaultModelExpandDepth: 1\n" + + " });\n" + + " };\n" + + " </script>\n" + + "</body>\n" + + "</html>"; + + /** + * Generates OpenAPI documentation files + * + * @param args command line arguments: outputDir + */ + public static void main(String[] args) throws IOException + { + if (args.length < 1) + { + throw new IllegalArgumentException("Usage: OpenApiDocumentationGenerator <output-directory>"); + } + + String outputDir = args[0]; + Path outputPath = Paths.get(outputDir); + + // Create output directory if it doesn't exist + Files.createDirectories(outputPath); + + // Generate OpenAPI specification + OpenApiConfiguration config = new OpenApiConfigurationImpl(); Review Comment: hmm, this is basically using the hardcoded values, and it never reads from sidecar.yaml. I am also wondering why we even need it in sidecar.yaml ########## server/src/main/java/org/apache/cassandra/sidecar/docs/OpenApiDocumentationGenerator.java: ########## @@ -0,0 +1,1343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.docs; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.cassandra.sidecar.config.OpenApiConfiguration; +import org.apache.cassandra.sidecar.config.yaml.OpenApiConfigurationImpl; + +/** + * Utility class for generating OpenAPI documentation files + */ +public class OpenApiDocumentationGenerator +{ + private static final String HTML_TEMPLATE = + "<!DOCTYPE html>\n" + + "<html lang=\"en\">\n" + + "<head>\n" + + " <meta charset=\"UTF-8\">\n" + + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + + " <title>Cassandra Sidecar API Documentation</title>\n" + + " <link rel=\"stylesheet\" type=\"text/css\" href=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css\" />\n" + + " <style>\n" + + " html {\n" + + " box-sizing: border-box;\n" + + " overflow: -moz-scrollbars-vertical;\n" + + " overflow-y: scroll;\n" + + " }\n" + + " *, *:before, *:after {\n" + + " box-sizing: inherit;\n" + + " }\n" + + " body {\n" + + " margin:0;\n" + + " background: #fafafa;\n" + + " }\n" + + " .swagger-ui .topbar { display: none; }\n" + + " .swagger-ui .info { margin: 50px 0; }\n" + + " .swagger-ui .info hgroup.main { margin: 0 0 20px 0; }\n" + + " .swagger-ui .info h1 { color: #3b4151; }\n" + + " </style>\n" + + "</head>\n" + + "<body>\n" + + " <div id=\"swagger-ui\"></div>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js\"></script>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js\"></script>\n" + + " <script>\n" + + " const spec = %s;\n" + + " window.onload = function() {\n" + + " SwaggerUIBundle({\n" + + " spec: spec,\n" + + " dom_id: '#swagger-ui',\n" + + " deepLinking: true,\n" + + " presets: [\n" + + " SwaggerUIBundle.presets.apis,\n" + + " SwaggerUIStandalonePreset\n" + + " ],\n" + + " plugins: [\n" + + " SwaggerUIBundle.plugins.DownloadUrl\n" + + " ],\n" + + " layout: \"StandaloneLayout\",\n" + + " defaultModelsExpandDepth: 1,\n" + + " defaultModelExpandDepth: 1\n" + + " });\n" + + " };\n" + + " </script>\n" + + "</body>\n" + + "</html>"; + + /** + * Generates OpenAPI documentation files + * + * @param args command line arguments: outputDir + */ + public static void main(String[] args) throws IOException + { + if (args.length < 1) + { + throw new IllegalArgumentException("Usage: OpenApiDocumentationGenerator <output-directory>"); + } + + String outputDir = args[0]; + Path outputPath = Paths.get(outputDir); + + // Create output directory if it doesn't exist + Files.createDirectories(outputPath); + + // Generate OpenAPI specification + OpenApiConfiguration config = new OpenApiConfigurationImpl(); + var openApi = createOpenApiFromConfig(config); + + // Scan for annotated handler classes + openApi = scanForAnnotations(openApi); + + // Generate JSON file + String jsonSpec = Json.pretty(openApi); + Path jsonFile = outputPath.resolve("openapi.json"); + Files.write(jsonFile, jsonSpec.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", jsonFile.toAbsolutePath()); + + // Generate YAML file + String yamlSpec = Yaml.pretty(openApi); + Path yamlFile = outputPath.resolve("openapi.yaml"); + Files.write(yamlFile, yamlSpec.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", yamlFile.toAbsolutePath()); + + // Generate HTML file with embedded specification + String htmlContent = HTML_TEMPLATE.replace("%s", jsonSpec); + Path htmlFile = outputPath.resolve("api-docs.html"); + Files.write(htmlFile, htmlContent.getBytes(StandardCharsets.UTF_8)); + System.out.printf("Generated: %s%n", htmlFile.toAbsolutePath()); + + System.out.printf("OpenAPI documentation generated successfully!%n"); + System.out.printf("Open %s in your browser to view the documentation.%n", htmlFile.toAbsolutePath()); + } + + /** + * Creates an OpenAPI configuration from the given configuration + */ + private static OpenAPI createOpenApiFromConfig(OpenApiConfiguration config) + { + OpenAPI openApi = new OpenAPI(); + + // Set basic info + io.swagger.v3.oas.models.info.Info info = new io.swagger.v3.oas.models.info.Info(); + info.setTitle(config.title()); + info.setDescription(config.description()); + info.setVersion(config.version()); + + // Set license info + io.swagger.v3.oas.models.info.License license = new io.swagger.v3.oas.models.info.License(); + license.setName(config.licenseName()); + license.setUrl(config.licenseUrl()); + info.setLicense(license); + + openApi.setInfo(info); + + // Set server info + io.swagger.v3.oas.models.servers.Server server = new io.swagger.v3.oas.models.servers.Server(); Review Comment: ditto ########## server/build.gradle: ########## @@ -20,6 +20,18 @@ import org.apache.tools.ant.taskdefs.condition.Os import java.nio.file.Paths +buildscript { + repositories { + gradlePluginPortal() + } + dependencies { + classpath("io.smallrye.openapi:io.smallrye.openapi.gradle.plugin:4.1.1") // Replace with the desired version + classpath("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") { + version { strictly("2.14.2") } + } + } +} + Review Comment: can we just do ? ``` id("io.smallrye.openapi") version "4.1.1" ``` ########## server/src/main/java/org/apache/cassandra/sidecar/cluster/locator/CachedLocalTokenRanges.java: ########## @@ -205,22 +205,26 @@ private synchronized Map<Integer, Set<TokenRange>> getCacheOrReload(Metadata met if (isClusterTheSame && localTokenRangesCache != null && localTokenRangesCache.containsKey(ks.getName())) { // we don't need to rebuild if already cached - perKeyspaceBuilder.put(ks.getName(), localTokenRangesCache.get(ks.getName())); + Map<Integer, Set<TokenRange>> cachedRanges = localTokenRangesCache.get(ks.getName()); Review Comment: unrelated, please open a new jira for this issue ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiUIHandler.java: ########## @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.oas.annotations.Hidden; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler that serves the Swagger UI for API documentation + */ +@Hidden +@Singleton +public class OpenApiUIHandler implements Handler<RoutingContext> +{ + private static final String SWAGGER_UI_HTML = Review Comment: can this be moved to a resource instead of hardcoding it into the java file? ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Singleton; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler that serves the OpenAPI specification + * + * This handler serves the OpenAPI specification generated by the Gradle generateOpenApiSpec task + * which processes JAX-RS/MicroProfile annotations to create comprehensive API documentation. + */ +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiHandler.class); + + @Override + public void handle(RoutingContext context) + { + try + { + // Determine format from request path or Accept header + boolean isYaml = isYamlRequest(context); + String openApiContent = loadGeneratedOpenApiSpec(isYaml); + String contentType = isYaml ? "application/yaml" : "application/json"; + + context.response() + .putHeader("Content-Type", contentType) + .end(openApiContent); + } + catch (Exception e) + { + LOGGER.warn("Failed to load generated OpenAPI specification, falling back to basic config", e); Review Comment: maybe we should tell where the spec is for the operator to go look for that path ########## server/build.gradle: ########## @@ -126,6 +144,17 @@ dependencies { // DataHub client is used to convert and report Cassandra schema on a periodic basis implementation(group: 'io.acryl', name: 'datahub-client', version: '0.15.0-3') + + // OpenAPI support + implementation('org.eclipse.microprofile.openapi:microprofile-openapi-api:3.1.1') + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") Review Comment: I have concerns about the usage of this dependency for potential licensing issues ########## server/build.gradle: ########## @@ -240,3 +254,62 @@ checkstyleContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } spotbugsContainerTest.onlyIf { "true" != System.getenv("skipContainerTest") } check.dependsOn containerTest, integrationTest, jacocoTestReport + +// Task to generate OpenAPI documentation +tasks.register('generateOpenApiDocs', JavaExec) { + group = 'documentation' + description = 'Generates OpenAPI documentation files (JSON, YAML, HTML)' + + dependsOn compileJava + classpath = sourceSets.main.runtimeClasspath + mainClass = 'org.apache.cassandra.sidecar.docs.OpenApiDocumentationGenerator' + + def outputDir = layout.buildDirectory.dir('docs/openapi').get().asFile + args outputDir.absolutePath + + outputs.dir outputDir + + doFirst { + if (!outputDir.exists()) { + outputDir.mkdirs() + } Review Comment: the check is redundant. mkdirs will perform the check itself ```suggestion outputDir.mkdirs() ``` ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiUIHandler.java: ########## @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.swagger.v3.oas.annotations.Hidden; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler that serves the Swagger UI for API documentation + */ +@Hidden +@Singleton +public class OpenApiUIHandler implements Handler<RoutingContext> +{ + private static final String SWAGGER_UI_HTML = + "<!DOCTYPE html>\n" + + "<html lang=\"en\">\n" + + "<head>\n" + + " <meta charset=\"UTF-8\">\n" + + " <title>Cassandra Sidecar API Documentation</title>\n" + + " <link rel=\"stylesheet\" type=\"text/css\" href=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css\" />\n" + + " <style>\n" + + " html {\n" + + " box-sizing: border-box;\n" + + " overflow: -moz-scrollbars-vertical;\n" + + " overflow-y: scroll;\n" + + " }\n" + + " *, *:before, *:after {\n" + + " box-sizing: inherit;\n" + + " }\n" + + " body {\n" + + " margin:0;\n" + + " background: #fafafa;\n" + + " }\n" + + " </style>\n" + + "</head>\n" + + "<body>\n" + + " <div id=\"swagger-ui\"></div>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js\"></script>\n" + + " <script src=\"https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js\"></script>\n" + + " <script>\n" + + " window.onload = function() {\n" + + " const ui = SwaggerUIBundle({\n" + + " url: '/openapi.json',\n" + + " dom_id: '#swagger-ui',\n" + + " deepLinking: true,\n" + + " presets: [\n" + + " SwaggerUIBundle.presets.apis,\n" + + " SwaggerUIStandalonePreset\n" + + " ],\n" + + " plugins: [\n" + + " SwaggerUIBundle.plugins.DownloadUrl\n" + + " ],\n" + + " layout: \"StandaloneLayout\"\n" + + " });\n" + + " };\n" + + " </script>\n" + + "</body>\n" + + "</html>"; + + @Inject + public OpenApiUIHandler() + { + } Review Comment: this constructor is not necessary ########## server/build.gradle: ########## @@ -126,6 +144,17 @@ dependencies { // DataHub client is used to convert and report Cassandra schema on a periodic basis implementation(group: 'io.acryl', name: 'datahub-client', version: '0.15.0-3') + + // OpenAPI support + implementation('org.eclipse.microprofile.openapi:microprofile-openapi-api:3.1.1') + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation('io.swagger.core.v3:swagger-core:2.2.21') Review Comment: can we define the version in gradle.settings? ########## server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java: ########## @@ -135,6 +225,14 @@ VertxRoute cassandraKeyspaceSchemaRoute(RouteBuilder.Factory factory, } @Deprecated + @GET + @Path(ApiEndpointsV1.DEPRECATED_KEYSPACE_SCHEMA_ROUTE) + @Operation(summary = "Get keyspace schema (deprecated)", Review Comment: deprecated at the spec level too? ########## server/build.gradle: ########## @@ -126,6 +144,17 @@ dependencies { // DataHub client is used to convert and report Cassandra schema on a periodic basis implementation(group: 'io.acryl', name: 'datahub-client', version: '0.15.0-3') + + // OpenAPI support + implementation('org.eclipse.microprofile.openapi:microprofile-openapi-api:3.1.1') + implementation("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") + implementation('io.swagger.core.v3:swagger-core:2.2.21') + implementation('io.swagger.core.v3:swagger-annotations:2.2.21') Review Comment: also looking at this dependency version and it looks like there are CVEs associated with this version. ########## conf/sidecar.yaml: ########## @@ -398,3 +398,14 @@ live_migration: - glob:${DATA_FILE_DIR}/*/*/snapshots # Excludes snapshot directories in data folder to copy to destination migration_map: # Map of source and destination Cassandra instances # localhost1: localhost4 # This entry says that localhost1 will be migrated to localhost4 + +# OpenAPI documentation configuration +openapi: Review Comment: it looks like this change is unnecessary. ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/OpenApiHandler.java: ########## @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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.apache.cassandra.sidecar.handlers; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Singleton; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler that serves the OpenAPI specification + * + * This handler serves the OpenAPI specification generated by the Gradle generateOpenApiSpec task + * which processes JAX-RS/MicroProfile annotations to create comprehensive API documentation. + */ +@Singleton +public class OpenApiHandler implements Handler<RoutingContext> +{ + private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiHandler.class); + + @Override + public void handle(RoutingContext context) + { + try + { + // Determine format from request path or Accept header + boolean isYaml = isYamlRequest(context); + String openApiContent = loadGeneratedOpenApiSpec(isYaml); + String contentType = isYaml ? "application/yaml" : "application/json"; + + context.response() + .putHeader("Content-Type", contentType) + .end(openApiContent); + } + catch (Exception e) + { + LOGGER.warn("Failed to load generated OpenAPI specification, falling back to basic config", e); + context.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()) + .putHeader("Content-Type", "application/json") + .end(); Review Comment: NIT ```suggestion throw wrapHttpException(HttpResponseStatus.NOT_FOUND, "Unable to load generated OpenAPI specification"); ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For additional commands, e-mail: pr-h...@cassandra.apache.org