diff --git a/org.restlet.ext.openapi/pom.xml b/org.restlet.ext.openapi/pom.xml
new file mode 100644
index 0000000000..421db26713
--- /dev/null
+++ b/org.restlet.ext.openapi/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ org.restlet
+ org.restlet.parent
+ 2.7.0-SNAPSHOT
+ ../pom.xml
+
+
+ org.restlet.ext.openapi
+ bundle
+ Restlet Framework - OpenAPI extension
+ Support for the OpenAPI specification.
+
+
+
+ io.swagger.core.v3
+ swagger-core
+ ${lib-swagger-version}
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${lib-swagger-version}
+
+
+
+ io.swagger.core.v3
+ swagger-integration
+ ${lib-swagger-version}
+
+
+
+ org.restlet
+ org.restlet
+ ${project.version}
+
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+
\ No newline at end of file
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiAnnotationProcessor.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiAnnotationProcessor.java
new file mode 100644
index 0000000000..ccc8d897fd
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiAnnotationProcessor.java
@@ -0,0 +1,63 @@
+package org.restlet.ext.openapi;
+
+import io.swagger.v3.core.util.AnnotationsUtils;
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.parameters.Parameter;
+
+import java.util.Optional;
+
+class OpenApiAnnotationProcessor {
+
+ static void documentOpenApiDefinition(
+ OpenAPI openAPIDefinition,
+ OpenAPIDefinition openAPIDefinitionAnnotation
+ ) {
+ if (openAPIDefinitionAnnotation.info() != null) {
+ AnnotationsUtils.getInfo(openAPIDefinitionAnnotation.info())
+ .ifPresent(openAPIDefinition::setInfo);
+ }
+ }
+
+ static void documentOperation(
+ Operation operation,
+ io.swagger.v3.oas.annotations.Operation operationAnnotation
+ ) {
+ if (operationAnnotation.summary() != null && !operationAnnotation.summary().isEmpty()) {
+ operation.setSummary(operationAnnotation.summary());
+ }
+
+ if (operationAnnotation.description() != null && !operationAnnotation.description().isEmpty()) {
+ operation.setDescription(operationAnnotation.description());
+ }
+
+ if (operationAnnotation.parameters() != null) {
+ for (io.swagger.v3.oas.annotations.Parameter parameterAnnotation : operationAnnotation.parameters()) {
+ resolveParameterFromAnnotation(parameterAnnotation)
+ .ifPresent(operation::addParametersItem);
+ }
+ }
+ }
+
+ private static Optional resolveParameterFromAnnotation(io.swagger.v3.oas.annotations.Parameter parameterAnnotation) {
+ return switch (parameterAnnotation.in()) {
+ case DEFAULT, PATH -> Optional.empty();
+ case COOKIE, HEADER, QUERY -> {
+ Parameter parameter = new Parameter()
+ .name(parameterAnnotation.name())
+ .description(parameterAnnotation.description() == null
+ ? null
+ : parameterAnnotation.description().isEmpty() ? null : parameterAnnotation.description()
+ )
+ .in(parameterAnnotation.in().name().toLowerCase())
+ .required(parameterAnnotation.required());
+
+ AnnotationsUtils.getSchemaFromAnnotation(parameterAnnotation.schema(), null)
+ .ifPresent(parameter::schema);
+
+ yield Optional.of(parameter);
+ }
+ };
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java
new file mode 100644
index 0000000000..102245467e
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java
@@ -0,0 +1,92 @@
+package org.restlet.ext.openapi;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import io.swagger.v3.oas.integration.OpenApiConfigurationException;
+import io.swagger.v3.oas.integration.SwaggerConfiguration;
+import io.swagger.v3.oas.integration.api.OpenAPIConfiguration;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.engine.resource.VariantInfo;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+import org.restlet.routing.Router;
+
+import java.util.List;
+
+class OpenApiSpecificationRestlet extends Restlet {
+ private static final VariantInfo VARIANT_JSON = new VariantInfo(
+ MediaType.APPLICATION_JSON
+ );
+
+ private static final VariantInfo VARIANT_APPLICATION_YAML = new VariantInfo(
+ MediaType.APPLICATION_YAML
+ );
+
+ private final Router router;
+
+ OpenApiSpecificationRestlet(Router router) {
+ super(router.getContext());
+ this.router = router;
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ super.handle(request, response);
+
+ if (Method.GET.equals(request.getMethod())) {
+ response.setEntity(getOpenApiDefinitionAsRepresentation(request));
+ } else {
+ response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
+ }
+ }
+
+ private Representation getOpenApiDefinitionAsRepresentation(Request request) {
+ List allowedVariants = List.of(VARIANT_APPLICATION_YAML, VARIANT_JSON);
+
+ Variant preferredVariant = getApplication()
+ .getConnegService()
+ .getPreferredVariant(
+ allowedVariants,
+ request,
+ getApplication().getMetadataService()
+ );
+
+ OpenAPIConfiguration oasConfig = new SwaggerConfiguration()
+ .prettyPrint(true);
+
+ try {
+ var context = new RestletOpenApiContextBuilder()
+ .router(router)
+ .openApiConfiguration(oasConfig)
+ .buildContext(true);
+
+ var openApiRead = context.read();
+
+ if (VARIANT_JSON.isCompatible(preferredVariant)) {
+ var openApiAsJson = context.getOutputJsonMapper()
+ .writer(new DefaultPrettyPrinter())
+ .writeValueAsString(openApiRead);
+
+ return new StringRepresentation(openApiAsJson, MediaType.APPLICATION_JSON);
+ } else {
+ var openApiAsYaml = context.getOutputYamlMapper()
+ .writer(new DefaultPrettyPrinter())
+ .writeValueAsString(openApiRead);
+
+ return new StringRepresentation(openApiAsYaml, MediaType.APPLICATION_YAML);
+ }
+ } catch (OpenApiConfigurationException | JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void attach(Router router, String path) {
+ router.attach(path, this);
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/Operations.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/Operations.java
new file mode 100644
index 0000000000..0b1737c853
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/Operations.java
@@ -0,0 +1,20 @@
+package org.restlet.ext.openapi;
+
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.oas.models.responses.ApiResponses;
+
+class Operations {
+ private Operations() {
+ }
+
+ static void addApiResponse(Operation operation, String apiResponseName, ApiResponse apiResponse) {
+ if (operation.getResponses() == null) {
+ operation.responses(
+ new ApiResponses().addApiResponse(apiResponseName, apiResponse)
+ );
+ } else {
+ operation.getResponses().addApiResponse(apiResponseName, apiResponse);
+ }
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/PathItems.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/PathItems.java
new file mode 100644
index 0000000000..a948dc5263
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/PathItems.java
@@ -0,0 +1,28 @@
+package org.restlet.ext.openapi;
+
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import org.restlet.data.Method;
+
+class PathItems {
+ private PathItems() {
+ }
+
+ static void setOperation(PathItem pathItem, Method restletMethod, Operation operation) {
+ if (restletMethod.equals(Method.POST)) {
+ pathItem.setPost(operation);
+ } else if (restletMethod.equals(Method.GET)) {
+ pathItem.setGet(operation);
+ } else if (restletMethod.equals(Method.DELETE)) {
+ pathItem.setDelete(operation);
+ } else if (restletMethod.equals(Method.PUT)) {
+ pathItem.setPut(operation);
+ } else if (restletMethod.equals(Method.PATCH)) {
+ pathItem.setPatch(operation);
+ } else if (restletMethod.equals(Method.OPTIONS)) {
+ pathItem.setOptions(operation);
+ } else {
+ throw new IllegalArgumentException("Unsupported Restlet Method: " + restletMethod.getName());
+ }
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiApplication.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiApplication.java
new file mode 100644
index 0000000000..3f3c169087
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiApplication.java
@@ -0,0 +1,80 @@
+package org.restlet.ext.openapi;
+
+import org.restlet.Application;
+import org.restlet.Restlet;
+import org.restlet.routing.Filter;
+import org.restlet.routing.Router;
+
+public class RestletOpenApiApplication extends Application {
+ /**
+ * Default path for the OpenAPI specification. Can be overridden by overriding the
+ * {@link #getOpenapiSpecificationPath()} method.
+ */
+ private static final String OPENAPI_SPECIFICATION_DEFAULT_PATH = "/openapi";
+
+ /**
+ * Indicates if this application has already been documented or not.
+ */
+ private volatile boolean documented;
+
+ @Override
+ public Restlet getInboundRoot() {
+ Restlet inboundRoot = super.getInboundRoot();
+
+ if (!documented) {
+ synchronized (this) {
+ if (!documented) {
+ Router rootRouter = getNextRouter(inboundRoot);
+
+ if (!documented && rootRouter != null) {
+ attachOpenApiSpecificationRestlet(rootRouter);
+ documented = true;
+ }
+
+ }
+ }
+ }
+
+ return inboundRoot;
+ }
+
+ /**
+ * Path where the OpenAPI specification will be available. By default, it is "/openapi".
+ */
+ protected String getOpenapiSpecificationPath() {
+ return OPENAPI_SPECIFICATION_DEFAULT_PATH;
+ }
+
+ /**
+ * Returns the next router available.
+ *
+ * @param current The current Restlet to inspect.
+ * @return The first router available.
+ */
+ private static Router getNextRouter(Restlet current) {
+ Router result = null;
+ if (current instanceof Router) {
+ result = (Router) current;
+ } else if (current instanceof Filter) {
+ result = getNextRouter(((Filter) current).getNext());
+ }
+
+ return result;
+ }
+
+ private void attachOpenApiSpecificationRestlet(Router router) {
+ getOpenApiSpecificationRestlet(router).attach(router, getOpenapiSpecificationPath());
+ documented = true;
+ }
+
+ /**
+ * The dedicated {@link Restlet} able to generate the Swagger specification formats.
+ *
+ * @return The {@link Restlet} able to generate the Swagger specification formats.
+ */
+ OpenApiSpecificationRestlet getOpenApiSpecificationRestlet(
+ Router router
+ ) {
+ return new OpenApiSpecificationRestlet(router);
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiContext.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiContext.java
new file mode 100644
index 0000000000..1d1a438d48
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiContext.java
@@ -0,0 +1,35 @@
+package org.restlet.ext.openapi;
+
+import io.swagger.v3.oas.integration.GenericOpenApiContext;
+import io.swagger.v3.oas.integration.api.OpenAPIConfiguration;
+import io.swagger.v3.oas.integration.api.OpenApiContext;
+import io.swagger.v3.oas.integration.api.OpenApiReader;
+import org.apache.commons.lang3.StringUtils;
+import org.restlet.routing.Router;
+
+class RestletOpenApiContext extends GenericOpenApiContext implements OpenApiContext {
+ private final Router router;
+
+ RestletOpenApiContext(Router router) {
+ this.router = router;
+ }
+
+ @Override
+ protected OpenApiReader buildReader(OpenAPIConfiguration openApiConfiguration) throws Exception {
+ OpenApiReader reader;
+
+ if (StringUtils.isNotBlank(openApiConfiguration.getReaderClass())) {
+ Class> cls = getClass().getClassLoader().loadClass(openApiConfiguration.getReaderClass());
+ reader = (OpenApiReader) cls.getDeclaredConstructor().newInstance();
+ } else {
+ reader = new RestletOpenApiReader();
+ }
+
+ if (reader instanceof RestletOpenApiReader restletOpenApiReader) {
+ restletOpenApiReader.setRouter(router);
+ }
+
+ reader.setConfiguration(openApiConfiguration);
+ return reader;
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiContextBuilder.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiContextBuilder.java
new file mode 100644
index 0000000000..d93edcfadf
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiContextBuilder.java
@@ -0,0 +1,42 @@
+package org.restlet.ext.openapi;
+
+import io.swagger.v3.oas.integration.GenericOpenApiContextBuilder;
+import io.swagger.v3.oas.integration.OpenApiConfigurationException;
+import io.swagger.v3.oas.integration.OpenApiContextLocator;
+import io.swagger.v3.oas.integration.api.OpenApiContext;
+import org.apache.commons.lang3.StringUtils;
+import org.restlet.routing.Router;
+
+class RestletOpenApiContextBuilder extends GenericOpenApiContextBuilder {
+ private Router router;
+
+ RestletOpenApiContextBuilder router(Router router) {
+ this.router = router;
+ return this;
+ }
+
+ @Override
+ public OpenApiContext buildContext(boolean init) throws OpenApiConfigurationException {
+ if (StringUtils.isBlank(ctxId)) {
+ ctxId = OpenApiContext.OPENAPI_CONTEXT_ID_DEFAULT;
+ }
+
+ OpenApiContext ctx = OpenApiContextLocator.getInstance().getOpenApiContext(ctxId);
+
+ if (ctx == null) {
+ OpenApiContext rootCtx = OpenApiContextLocator.getInstance()
+ .getOpenApiContext(OpenApiContext.OPENAPI_CONTEXT_ID_DEFAULT);
+
+ ctx = new RestletOpenApiContext(router)
+ .id(ctxId)
+ .openApiConfiguration(openApiConfiguration)
+ .parent(rootCtx);
+
+ if (init) {
+ ctx.init();
+ }
+ }
+
+ return ctx;
+ }
+}
diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiReader.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiReader.java
new file mode 100644
index 0000000000..f2b9069676
--- /dev/null
+++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/RestletOpenApiReader.java
@@ -0,0 +1,293 @@
+package org.restlet.ext.openapi;
+
+import io.swagger.v3.core.converter.AnnotatedType;
+import io.swagger.v3.core.converter.ModelConverters;
+import io.swagger.v3.core.converter.ResolvedSchema;
+import io.swagger.v3.core.util.ReflectionUtils;
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.integration.api.OpenAPIConfiguration;
+import io.swagger.v3.oas.integration.api.OpenApiReader;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.Paths;
+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 io.swagger.v3.oas.models.responses.ApiResponse;
+import org.restlet.engine.resource.AnnotationInfo;
+import org.restlet.engine.resource.AnnotationUtils;
+import org.restlet.engine.resource.MethodAnnotationInfo;
+import org.restlet.representation.Variant;
+import org.restlet.resource.Finder;
+import org.restlet.resource.ResourceException;
+import org.restlet.resource.ServerResource;
+import org.restlet.routing.Route;
+import org.restlet.routing.Router;
+import org.restlet.routing.TemplateRoute;
+import org.restlet.service.MetadataService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+class RestletOpenApiReader implements OpenApiReader {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RestletOpenApiReader.class);
+
+ private final MetadataService metadataService = new MetadataService();
+
+ private Router router;
+
+ private OpenAPIConfiguration config;
+
+ private Paths paths = new Paths();
+
+ private OpenAPI openAPI = new OpenAPI();
+
+ private Components components = new Components();
+
+ public void setRouter(Router router) {
+ this.router = router;
+ }
+
+ @Override
+ public void setConfiguration(OpenAPIConfiguration openApiConfiguration) {
+ this.config = openApiConfiguration;
+ }
+
+ @Override
+ public OpenAPI read(Set> classes, Map resources) {
+ return processRouter(router);
+ }
+
+ private OpenAPI processRouter(Router router) {
+ OpenAPIDefinition openAPIDefinitionAnnotation = ReflectionUtils.getAnnotation(
+ router.getApplication().getClass(),
+ OpenAPIDefinition.class
+ );
+
+ if (openAPIDefinitionAnnotation != null) {
+ OpenApiAnnotationProcessor.documentOpenApiDefinition(openAPI, openAPIDefinitionAnnotation);
+ }
+
+ List allRoutes = new ArrayList<>(router.getRoutes());
+ if (router.getDefaultRoute() != null) {
+ allRoutes.add(router.getDefaultRoute());
+ }
+
+ for (Route route : allRoutes) {
+ processRoute(route);
+ }
+
+ openAPI.setComponents(components);
+
+ return openAPI;
+ }
+
+ private void processRoute(Route route) {
+ if (route instanceof TemplateRoute templateRoute) {
+ String path = templateRoute.getTemplate().getPattern();
+
+ if (route.getNext() instanceof Finder finder) {
+ ServerResource serverResource = finder.find(null, null);
+
+ if (serverResource != null) {
+ List pathVariableNames = templateRoute.getTemplate().getVariableNames();
+
+ processServerResource(serverResource, path, pathVariableNames);
+ }
+ }
+ } else {
+ LOGGER.info("Route type ignored: {}", route.getClass());
+ }
+ }
+
+ private void processServerResource(
+ ServerResource serverResource,
+ String operationPath,
+ List pathVariableNames
+ ) {
+ List annotations = serverResource.isAnnotated()
+ ? AnnotationUtils.getInstance().getAnnotations(serverResource.getClass())
+ : null;
+
+ if (annotations == null) {
+ return;
+ }
+
+ for (AnnotationInfo annotationInfo : annotations) {
+ if (annotationInfo instanceof MethodAnnotationInfo methodAnnotationInfo) {
+ PathItem pathItem = Optional.ofNullable(openAPI.getPaths())
+ .map(paths -> paths.get(operationPath))
+ .orElseGet(PathItem::new);
+
+ Operation operation = buildOperationFromRestletMethod(methodAnnotationInfo);
+
+ completePathParameters(operation, pathVariableNames);
+ completeOperation(serverResource, operation, methodAnnotationInfo);
+
+ PathItems.setOperation(pathItem, methodAnnotationInfo.getRestletMethod(), operation);
+
+ paths.addPathItem(operationPath, pathItem);
+ if (openAPI.getPaths() != null) {
+ this.paths.putAll(openAPI.getPaths());
+ }
+
+ openAPI.setPaths(this.paths);
+ }
+ }
+ }
+
+ private void completePathParameters(
+ Operation operation,
+ List pathVariableNames
+ ) {
+ if (pathVariableNames != null) {
+ for (String pathVariableName : pathVariableNames) {
+ operation.addParametersItem(
+ new io.swagger.v3.oas.models.parameters.Parameter()
+ .name(pathVariableName)
+ .in("path")
+ .required(true)
+ );
+ }
+ }
+ }
+
+ private Operation buildOperationFromRestletMethod(
+ final MethodAnnotationInfo methodAnnotationInfo
+ ) {
+ Operation operation = new Operation();
+ operation.setOperationId(methodAnnotationInfo.getJavaMethod().getName());
+ return operation;
+ }
+
+ private void completeOperation(
+ final ServerResource serverResource,
+ final Operation operation,
+ MethodAnnotationInfo methodAnnotationInfo
+ ) {
+ try {
+ var methodOperationAnnotation = ReflectionUtils.getAnnotation(
+ methodAnnotationInfo.getJavaMethod(),
+ io.swagger.v3.oas.annotations.Operation.class
+ );
+
+ if (methodOperationAnnotation != null) {
+ OpenApiAnnotationProcessor.documentOperation(operation, methodOperationAnnotation);
+ }
+
+ completeOperationInput(serverResource, operation, methodAnnotationInfo);
+ completeOperationSuccessfulOutput(serverResource, operation, methodAnnotationInfo);
+ } catch (IOException e) {
+ throw new ResourceException(e);
+ }
+ }
+
+ private void completeOperationInput(
+ ServerResource serverResource,
+ Operation operation,
+ MethodAnnotationInfo methodAnnotationInfo
+ ) throws IOException {
+ Type[] parameterTypes = methodAnnotationInfo.getJavaMethod().getGenericParameterTypes();
+ if (parameterTypes.length == 0) {
+ return;
+ }
+
+ Type firstParameterType = parameterTypes[0];
+
+ List requestVariants = methodAnnotationInfo.getRequestVariants(
+ metadataService,
+ serverResource.getConverterService()
+ );
+
+ if (requestVariants == null || requestVariants.isEmpty()) {
+ return;
+ }
+
+ Variant firstVariant = requestVariants.getFirst();
+
+ processTypeToContent(firstParameterType, List.of(firstVariant))
+ .ifPresent(content -> {
+ operation.requestBody(
+ new io.swagger.v3.oas.models.parameters.RequestBody()
+ .content(content)
+ );
+ });
+ }
+
+ private void completeOperationSuccessfulOutput(
+ ServerResource serverResource,
+ Operation operation,
+ MethodAnnotationInfo methodAnnotationInfo
+ ) throws IOException {
+ List responseVariants = methodAnnotationInfo.getResponseVariants(
+ metadataService,
+ serverResource.getConverterService()
+ );
+
+ Type javaMethodReturnType = methodAnnotationInfo.getJavaMethod().getGenericReturnType();
+
+ boolean shouldIgnoreClass = methodAnnotationInfo.getJavaMethod().getReturnType() == Void.class
+ || methodAnnotationInfo.getJavaMethod().getReturnType() == void.class;
+
+ if (!shouldIgnoreClass) {
+ processMethodReturnType(operation, javaMethodReturnType, responseVariants);
+ }
+ }
+
+ private void processMethodReturnType(
+ Operation operation,
+ Type returnType,
+ List responseVariants
+ ) {
+ Variant firstVariant = responseVariants.getFirst();
+
+ processTypeToContent(returnType, List.of(firstVariant))
+ .ifPresent(content -> Operations.addApiResponse(
+ operation,
+ "200",
+ new ApiResponse().content(content)
+ ));
+ }
+
+ private Optional processTypeToContent(Type type, List variants) {
+ ResolvedSchema resolvedSchema = ModelConverters.getInstance(config.toConfiguration())
+ .resolveAsResolvedSchema(
+ new AnnotatedType(type)
+ .resolveAsRef(true)
+ .components(components)
+ );
+
+ if (resolvedSchema.schema == null) {
+ return Optional.empty();
+ }
+
+ Content content = new Content();
+ MediaType mediaType = new MediaType().schema(resolvedSchema.schema);
+
+ for (Variant variant : variants) {
+ if (variant.getMediaType() == null) {
+ LOGGER.warn("Variant has no media type: {}", variant);
+ continue;
+ }
+
+ content.addMediaType(variant.getMediaType().toString(), mediaType);
+ }
+
+ @SuppressWarnings("rawtypes") // Imposed by the ModelConverters API
+ Map schemaMap = resolvedSchema.referencedSchemas;
+ if (schemaMap != null) {
+ schemaMap.forEach((key, schema) -> components.addSchemas(key, schema));
+ }
+
+ return Optional.of(content);
+ }
+}
diff --git a/pom.xml b/pom.xml
index 9b16b769e9..3b0169772f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
4.3.1
2.0.17
6.2.8
+ 2.2.40
3.1.3.RELEASE
2.4.1
@@ -76,6 +77,7 @@
org.restlet.ext.jaas
org.restlet.ext.jackson
org.restlet.ext.json
+ org.restlet.ext.openapi
org.restlet.ext.slf4j
org.restlet.ext.spring
org.restlet.ext.thymeleaf