pre-major refactoring + ext api.
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Declares OpenAPI operation metadata for a class-based handler.
|
||||
* Picked up by {@link OpenApiExtension} via {@link FlashApp#register}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Route(method = HttpMethod.GET, path = "/api/blogs")
|
||||
* @ApiOperation(summary = "List all blogs", tags = {"blogs"})
|
||||
* public class ListBlogs extends JacksonHandler { ... }
|
||||
* }</pre>
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ApiOperation {
|
||||
String summary() default "";
|
||||
String description() default "";
|
||||
String[] tags() default {};
|
||||
boolean deprecated() default false;
|
||||
String operationId() default "";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Declares a single parameter (query, path, header, or cookie) for an operation.
|
||||
* Repeatable — place multiple annotations on the same handler class.
|
||||
*
|
||||
* <pre>{@code
|
||||
* @ApiParam(name = "limit", in = "query", type = "integer", description = "Max results (default 20)")
|
||||
* @ApiParam(name = "offset", in = "query", type = "integer", description = "Pagination offset")
|
||||
* public class ListBlogs extends JacksonHandler { ... }
|
||||
* }</pre>
|
||||
*/
|
||||
@Repeatable(ApiParams.class)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ApiParam {
|
||||
String name();
|
||||
/** "query", "path", "header", or "cookie". */
|
||||
String in() default "query";
|
||||
/** OpenAPI primitive type: "string", "integer", "number", "boolean". */
|
||||
String type() default "string";
|
||||
String description() default "";
|
||||
boolean required() default false;
|
||||
String example() default "";
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/** Container for repeated {@link ApiParam} annotations. */
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ApiParams {
|
||||
ApiParam[] value();
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Declares a single response for an operation. Repeatable — use multiple
|
||||
* {@code @ApiResponse} annotations on the same handler to document several status codes.
|
||||
*
|
||||
* <pre>{@code
|
||||
* @ApiResponse(status = 200, description = "Blog created", schema = Blog.class)
|
||||
* @ApiResponse(status = 400, description = "Invalid input")
|
||||
* @ApiResponse(status = 409, description = "Slug already exists")
|
||||
* public class CreateBlog extends JacksonHandler { ... }
|
||||
* }</pre>
|
||||
*/
|
||||
@Repeatable(ApiResponses.class)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ApiResponse {
|
||||
int status();
|
||||
String description() default "";
|
||||
Class<?> schema() default Void.class;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/** Container for repeated {@link ApiResponse} annotations. */
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ApiResponses {
|
||||
ApiResponse[] value();
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import dev.relism.routing.Route;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Accumulates OpenAPI 3.0 operations and builds the spec document as a plain
|
||||
* {@code Map} for Jackson to serialize. Operations are added at registration time
|
||||
* via {@link OpenApiExtension}'s {@link dev.relism.AnnotationProcessor}.
|
||||
*/
|
||||
public class OpenApiBuilder {
|
||||
|
||||
private String title = "API";
|
||||
private String version = "1.0.0";
|
||||
private String description = "";
|
||||
|
||||
private final Map<String, Map<String, Object>> paths = new LinkedHashMap<>();
|
||||
private final Map<String, Map<String, Class<?>>> operationHandlers = new LinkedHashMap<>();
|
||||
private OpenApiSecurityRegistry securityRegistry;
|
||||
|
||||
// ── Configuration ─────────────────────────────────────────────────────────
|
||||
|
||||
public OpenApiBuilder title(String title) { this.title = title; return this; }
|
||||
public OpenApiBuilder version(String version) { this.version = version; return this; }
|
||||
public OpenApiBuilder description(String description) { this.description = description; return this; }
|
||||
|
||||
void setSecurityRegistry(OpenApiSecurityRegistry registry) {
|
||||
this.securityRegistry = registry;
|
||||
}
|
||||
|
||||
// ── Operation registration ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Adds an operation derived from the handler's {@link Route}, {@link ApiOperation},
|
||||
* {@link ApiResponse}, and {@link ApiParam} annotations.
|
||||
*/
|
||||
public void addOperation(Route route, ApiOperation op, Class<?> handlerClass) {
|
||||
String path = normalizePath(route.path());
|
||||
String method = route.method().name().toLowerCase();
|
||||
|
||||
Map<String, Object> pathItem = paths.computeIfAbsent(path, k -> new LinkedHashMap<>());
|
||||
Map<String, Object> operation = new LinkedHashMap<>();
|
||||
|
||||
if (!op.operationId().isEmpty()) operation.put("operationId", op.operationId());
|
||||
if (!op.summary().isEmpty()) operation.put("summary", op.summary());
|
||||
if (!op.description().isEmpty()) operation.put("description", op.description());
|
||||
if (op.tags().length > 0) operation.put("tags", Arrays.asList(op.tags()));
|
||||
if (op.deprecated()) operation.put("deprecated", true);
|
||||
|
||||
buildParameters(operation, handlerClass, route);
|
||||
buildResponses(operation, handlerClass);
|
||||
|
||||
pathItem.put(method, operation);
|
||||
operationHandlers.computeIfAbsent(path, k -> new LinkedHashMap<>()).put(method, handlerClass);
|
||||
}
|
||||
|
||||
// ── Spec build ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the complete OpenAPI 3.0.3 spec as a plain map ready for JSON
|
||||
* serialization. Called on each request to {@code /openapi.json} so that
|
||||
* handlers registered after the extension is installed are included.
|
||||
*/
|
||||
public Map<String, Object> build() {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("title", title);
|
||||
info.put("version", version);
|
||||
if (!description.isEmpty()) info.put("description", description);
|
||||
|
||||
List<OpenApiSecurityContributor> contributors = securityRegistry != null
|
||||
? securityRegistry.contributors() : List.of();
|
||||
|
||||
// Build paths with security injected per-operation (fresh copy each time so
|
||||
// repeated calls don't accumulate duplicate security entries)
|
||||
Map<String, Object> renderedPaths = new LinkedHashMap<>();
|
||||
for (var pathEntry : paths.entrySet()) {
|
||||
Map<String, Object> renderedPathItem = new LinkedHashMap<>();
|
||||
Map<String, Class<?>> handlers = operationHandlers.getOrDefault(pathEntry.getKey(), Map.of());
|
||||
for (var methodEntry : pathEntry.getValue().entrySet()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> original = (Map<String, Object>) methodEntry.getValue();
|
||||
Map<String, Object> op = new LinkedHashMap<>(original); // shallow copy
|
||||
Class<?> handler = handlers.get(methodEntry.getKey());
|
||||
if (handler != null && !contributors.isEmpty()) {
|
||||
List<Map<String, List<String>>> security = buildOperationSecurity(contributors, handler);
|
||||
if (!security.isEmpty()) op.put("security", security);
|
||||
}
|
||||
renderedPathItem.put(methodEntry.getKey(), op);
|
||||
}
|
||||
renderedPaths.put(pathEntry.getKey(), renderedPathItem);
|
||||
}
|
||||
|
||||
Map<String, Object> spec = new LinkedHashMap<>();
|
||||
spec.put("openapi", "3.0.3");
|
||||
spec.put("info", info);
|
||||
spec.put("paths", renderedPaths);
|
||||
|
||||
if (!contributors.isEmpty()) {
|
||||
Map<String, Object> schemes = new LinkedHashMap<>();
|
||||
for (OpenApiSecurityContributor c : contributors) {
|
||||
schemes.put(c.schemeName(), c.schemeDefinition());
|
||||
}
|
||||
spec.put("components", Map.of("securitySchemes", schemes));
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
// ── Internals ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildParameters(Map<String, Object> op, Class<?> cls, Route route) {
|
||||
List<Map<String, Object>> params = new ArrayList<>();
|
||||
|
||||
// Path params from @Route path — add them automatically as required
|
||||
String path = route.path();
|
||||
int i = 0;
|
||||
while (i < path.length()) {
|
||||
int open = path.indexOf('{', i);
|
||||
if (open < 0) break;
|
||||
int close = path.indexOf('}', open);
|
||||
if (close < 0) break;
|
||||
String name = path.substring(open + 1, close);
|
||||
Map<String, Object> p = new LinkedHashMap<>();
|
||||
p.put("name", name);
|
||||
p.put("in", "path");
|
||||
p.put("required", true);
|
||||
p.put("schema", Map.of("type", "string"));
|
||||
params.add(p);
|
||||
i = close + 1;
|
||||
}
|
||||
|
||||
// Explicit @ApiParam annotations
|
||||
ApiParam[] apiParams = cls.getAnnotationsByType(ApiParam.class);
|
||||
for (ApiParam ann : apiParams) {
|
||||
Map<String, Object> p = new LinkedHashMap<>();
|
||||
p.put("name", ann.name());
|
||||
p.put("in", ann.in());
|
||||
p.put("required", ann.required());
|
||||
if (!ann.description().isEmpty()) p.put("description", ann.description());
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", ann.type());
|
||||
if (!ann.example().isEmpty()) schema.put("example", ann.example());
|
||||
p.put("schema", schema);
|
||||
params.add(p);
|
||||
}
|
||||
|
||||
if (!params.isEmpty()) op.put("parameters", params);
|
||||
}
|
||||
|
||||
private void buildResponses(Map<String, Object> op, Class<?> cls) {
|
||||
ApiResponse[] annotations = cls.getAnnotationsByType(ApiResponse.class);
|
||||
Map<String, Object> responses = new LinkedHashMap<>();
|
||||
|
||||
if (annotations.length == 0) {
|
||||
responses.put("200", Map.of("description", "OK"));
|
||||
} else {
|
||||
for (ApiResponse ann : annotations) {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("description", ann.description().isEmpty() ? httpPhrase(ann.status()) : ann.description());
|
||||
if (ann.schema() != Void.class) {
|
||||
r.put("content", Map.of(
|
||||
"application/json", Map.of(
|
||||
"schema", Map.of("$ref", "#/components/schemas/" + ann.schema().getSimpleName()))));
|
||||
}
|
||||
responses.put(String.valueOf(ann.status()), r);
|
||||
}
|
||||
}
|
||||
op.put("responses", responses);
|
||||
}
|
||||
|
||||
/** Converts Flash path params ({id}) to OpenAPI path params ({id}) — already compatible. */
|
||||
private static String normalizePath(String path) {
|
||||
return path.startsWith("/") ? path : "/" + path;
|
||||
}
|
||||
|
||||
private static String httpPhrase(int status) {
|
||||
return switch (status) {
|
||||
case 200 -> "OK";
|
||||
case 201 -> "Created";
|
||||
case 204 -> "No Content";
|
||||
case 400 -> "Bad Request";
|
||||
case 401 -> "Unauthorized";
|
||||
case 403 -> "Forbidden";
|
||||
case 404 -> "Not Found";
|
||||
case 409 -> "Conflict";
|
||||
case 422 -> "Unprocessable Entity";
|
||||
case 500 -> "Internal Server Error";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
private static List<Map<String, List<String>>> buildOperationSecurity(
|
||||
List<OpenApiSecurityContributor> contributors, Class<?> handlerClass) {
|
||||
List<Map<String, List<String>>> security = new ArrayList<>();
|
||||
for (OpenApiSecurityContributor c : contributors) {
|
||||
List<String> scopes = c.requiredFor(handlerClass);
|
||||
if (scopes != null) {
|
||||
security.add(Map.of(c.schemeName(), scopes));
|
||||
}
|
||||
}
|
||||
return security;
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import dev.relism.extension.ExtensionContext;
|
||||
import dev.relism.extension.FlashApp;
|
||||
import dev.relism.extension.FlashExtension;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.routing.Route;
|
||||
|
||||
/**
|
||||
* Generates and serves an OpenAPI 3.0 spec and Swagger UI under a configurable base path.
|
||||
*
|
||||
* <p>Given {@code basePath = "/openapi"} (the default), three routes are registered:
|
||||
* <ul>
|
||||
* <li>{@code GET /openapi.json} — OpenAPI 3.0 spec as JSON</li>
|
||||
* <li>{@code GET /openapi.yaml} — OpenAPI 3.0 spec as YAML</li>
|
||||
* <li>{@code GET /openapi/swagger} — Swagger UI pointing at {@code /openapi.json}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Requires</b> {@code flash-ext-jackson} to be installed first (shares its
|
||||
* {@link ObjectMapper}). The YAML endpoint uses its own {@link YAMLMapper} instance.
|
||||
*
|
||||
* <p>Operations are collected automatically from handlers annotated with
|
||||
* {@link ApiOperation} as they are registered via {@link FlashApp#register}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FlashApp.of(new HttpServer(config))
|
||||
* .install(new JacksonExtension())
|
||||
* .install(new OpenApiExtension("/openapi", "My API", "2.0.0"))
|
||||
* .register(new BlogHandlers.Index())
|
||||
* .start();
|
||||
* }</pre>
|
||||
*/
|
||||
public class OpenApiExtension implements FlashExtension {
|
||||
|
||||
private static final String YAML_CONTENT_TYPE = "application/yaml";
|
||||
|
||||
private final String basePath;
|
||||
private final String title;
|
||||
private final String version;
|
||||
private final String description;
|
||||
|
||||
public OpenApiExtension() {
|
||||
this("/openapi", "API", "1.0.0", "");
|
||||
}
|
||||
|
||||
public OpenApiExtension(String basePath) {
|
||||
this(basePath, "API", "1.0.0", "");
|
||||
}
|
||||
|
||||
public OpenApiExtension(String basePath, String title, String version) {
|
||||
this(basePath, title, version, "");
|
||||
}
|
||||
|
||||
public OpenApiExtension(String basePath, String title, String version, String description) {
|
||||
this.basePath = basePath.endsWith("/") ? basePath.substring(0, basePath.length() - 1) : basePath;
|
||||
this.title = title;
|
||||
this.version = version;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(FlashApp app, ExtensionContext ctx) {
|
||||
ObjectMapper jsonMapper = ctx.require(ObjectMapper.class);
|
||||
YAMLMapper yamlMapper = new YAMLMapper();
|
||||
|
||||
OpenApiBuilder builder = new OpenApiBuilder()
|
||||
.title(title)
|
||||
.version(version)
|
||||
.description(description);
|
||||
|
||||
OpenApiSecurityRegistry secRegistry = new OpenApiSecurityRegistry();
|
||||
ctx.provide(OpenApiSecurityRegistry.class, secRegistry);
|
||||
builder.setSecurityRegistry(secRegistry);
|
||||
|
||||
ctx.provide(OpenApiBuilder.class, builder);
|
||||
|
||||
// Collect operation metadata at handler-registration time (no middleware injected)
|
||||
ctx.addAnnotationProcessor(handlerClass -> {
|
||||
ApiOperation op = handlerClass.getAnnotation(ApiOperation.class);
|
||||
Route route = handlerClass.getAnnotation(Route.class);
|
||||
if (op != null && route != null) {
|
||||
builder.addOperation(route, op, handlerClass);
|
||||
}
|
||||
return java.util.List.of();
|
||||
});
|
||||
|
||||
String jsonPath = basePath + ".json";
|
||||
String yamlPath = basePath + ".yaml";
|
||||
String swaggerPath = basePath + "/swagger";
|
||||
|
||||
// JSON spec
|
||||
app.get(jsonPath, (req, res) -> {
|
||||
res.setContentType(ContentType.JSON);
|
||||
return jsonMapper.writeValueAsString(builder.build());
|
||||
}).with();
|
||||
|
||||
// YAML spec
|
||||
app.get(yamlPath, (req, res) -> {
|
||||
res.type(YAML_CONTENT_TYPE);
|
||||
return yamlMapper.writeValueAsString(builder.build());
|
||||
}).with();
|
||||
|
||||
// Swagger UI — loads from CDN, points at the JSON spec
|
||||
String swaggerHtml = buildSwaggerHtml(jsonPath);
|
||||
app.get(swaggerPath, (req, res) -> {
|
||||
res.setContentType(ContentType.TEXT_HTML);
|
||||
return swaggerHtml;
|
||||
}).with();
|
||||
}
|
||||
|
||||
// ── Swagger UI HTML ───────────────────────────────────────────────────────
|
||||
|
||||
private static String buildSwaggerHtml(String specJsonPath) {
|
||||
return "<!DOCTYPE html>\n" +
|
||||
"<html lang=\"en\">\n" +
|
||||
"<head>\n" +
|
||||
" <meta charset=\"UTF-8\">\n" +
|
||||
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" +
|
||||
" <title>Swagger UI</title>\n" +
|
||||
" <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\">\n" +
|
||||
"</head>\n" +
|
||||
"<body>\n" +
|
||||
"<div id=\"swagger-ui\"></div>\n" +
|
||||
"<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n" +
|
||||
"<script>\n" +
|
||||
"SwaggerUIBundle({\n" +
|
||||
" url: \"" + specJsonPath + "\",\n" +
|
||||
" dom_id: '#swagger-ui',\n" +
|
||||
" deepLinking: true,\n" +
|
||||
" presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],\n" +
|
||||
" layout: \"BaseLayout\"\n" +
|
||||
"});\n" +
|
||||
"</script>\n" +
|
||||
"</body>\n" +
|
||||
"</html>";
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Pluggable security scheme contributor for the OpenAPI spec.
|
||||
*
|
||||
* <p>Extensions that enforce authentication (e.g. {@code flash-ext-oidc}) implement
|
||||
* this interface and register an instance into {@link OpenApiSecurityRegistry} via the
|
||||
* {@link dev.relism.extension.ExtensionContext}. {@link OpenApiExtension} picks it up
|
||||
* at spec-generation time — no coupling between the two extensions at install time.
|
||||
*
|
||||
* <p>Multi-tenant: multiple contributors may coexist. For handlers secured by
|
||||
* {@code @Authenticated}/{@code @RolesAllowed}, each matching contributor adds its
|
||||
* own entry to the operation's {@code security} array (OpenAPI OR semantics).
|
||||
*/
|
||||
public interface OpenApiSecurityContributor {
|
||||
|
||||
/**
|
||||
* Unique scheme name used as a key in {@code components.securitySchemes}
|
||||
* and referenced from each operation's {@code security} array.
|
||||
*/
|
||||
String schemeName();
|
||||
|
||||
/**
|
||||
* The OpenAPI security scheme definition object placed under
|
||||
* {@code components.securitySchemes.<schemeName>}.
|
||||
*
|
||||
* <p>Example for OIDC:
|
||||
* <pre>{@code
|
||||
* Map.of("type", "openIdConnect",
|
||||
* "openIdConnectUrl", "https://idp.example.com/.well-known/openid-configuration")
|
||||
* }</pre>
|
||||
*/
|
||||
Map<String, Object> schemeDefinition();
|
||||
|
||||
/**
|
||||
* Returns the scopes/roles required for the given handler class under this scheme,
|
||||
* or {@code null} if this contributor does not secure the handler.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code null} — handler is not secured by this contributor (skip)</li>
|
||||
* <li>empty list — handler requires authentication, no specific scopes</li>
|
||||
* <li>non-empty list — handler requires these scopes/roles</li>
|
||||
* </ul>
|
||||
*/
|
||||
List<String> requiredFor(Class<?> handlerClass);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package dev.relism.ext.openapi;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Mutable registry of {@link OpenApiSecurityContributor}s.
|
||||
*
|
||||
* <p>Created and provided to the {@link dev.relism.extension.ExtensionContext} by
|
||||
* {@link OpenApiExtension} at install time. Other extensions (e.g. {@code flash-ext-oidc})
|
||||
* retrieve it via {@code ctx.find(OpenApiSecurityRegistry.class)} and register their
|
||||
* contributor — the OpenAPI extension then picks it up lazily at spec-generation time.
|
||||
*
|
||||
* <p>Thread-safe: {@link CopyOnWriteArrayList} allows concurrent reads during spec
|
||||
* generation without blocking registration.
|
||||
*/
|
||||
public final class OpenApiSecurityRegistry {
|
||||
|
||||
private final List<OpenApiSecurityContributor> contributors = new CopyOnWriteArrayList<>();
|
||||
|
||||
/** Registers a contributor. Safe to call concurrently. */
|
||||
public void add(OpenApiSecurityContributor contributor) {
|
||||
contributors.add(contributor);
|
||||
}
|
||||
|
||||
/** Returns an unmodifiable snapshot of all registered contributors. */
|
||||
public List<OpenApiSecurityContributor> contributors() {
|
||||
return Collections.unmodifiableList(contributors);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user