Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions org.restlet.ext.openapi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.restlet</groupId>
<artifactId>org.restlet.parent</artifactId>
<version>2.7.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>org.restlet.ext.openapi</artifactId>
<packaging>bundle</packaging>
<name>Restlet Framework - OpenAPI extension</name>
<description>Support for the OpenAPI specification.</description>

<dependencies>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core</artifactId>
<version>${lib-swagger-core-version}</version>
</dependency>

<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${lib-swagger-core-version}</version>
</dependency>

<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-integration</artifactId>
<version>${lib-swagger-core-version}</version>
</dependency>

<dependency>
<groupId>org.restlet</groupId>
<artifactId>org.restlet</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${lib-junit-version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.restlet</groupId>
<artifactId>org.restlet.ext.jackson</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>${lib-swagger-parser-version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright 2005-2026 Qlik
*
* The contents of this file is subject to the terms of the Apache 2.0 open source license available at
* http://www.opensource.org/licenses/apache-2.0
*
* Restlet is a registered trademark of QlikTech International AB.
*/

package org.restlet.ext.openapi;

import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
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;

public class OpenApiAnnotationProcessor {

public static void documentOpenApiDefinition(
OpenAPI openAPIDefinition,
OpenAPIDefinition openAPIDefinitionAnnotation
) {
if (openAPIDefinitionAnnotation.info() != null) {
AnnotationsUtils.getInfo(openAPIDefinitionAnnotation.info())
.ifPresent(openAPIDefinition::setInfo);
}
}

public 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);
}
}

if (operationAnnotation.responses() != null) {
for (ApiResponse apiResponseAnnotation : operationAnnotation.responses()) {
documentOperationResponse(operation, apiResponseAnnotation);
}
}
}

public static void documentOperationResponse(
Operation operation,
ApiResponse apiResponseAnnotation
) {
var defaultDescription = apiResponseAnnotation.responseCode() + " response";

io.swagger.v3.oas.models.responses.ApiResponse apiResponse =
new io.swagger.v3.oas.models.responses.ApiResponse()
.description(apiResponseAnnotation.description() == null
? defaultDescription
: apiResponseAnnotation.description().isEmpty()
? defaultDescription
: apiResponseAnnotation.description()
);

if (!isContentEmpty(apiResponseAnnotation)) {
AnnotationsUtils.getContent(
apiResponseAnnotation.content(),
null,
null,
null,
null,
null
).ifPresent(apiResponse::content);
}

AnnotationsUtils.getHeaders(apiResponseAnnotation.headers(), null)
.ifPresent(apiResponse::headers);

Operations.addApiResponse(operation, apiResponseAnnotation.responseCode(), apiResponse);
}

private static Optional<Parameter> 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);
}
};
}

private static boolean isContentEmpty(ApiResponse apiResponseAnnotation) {
if (apiResponseAnnotation.content() == null || apiResponseAnnotation.content().length == 0) {
return true;
}

Content content = apiResponseAnnotation.content()[0];

return content.mediaType().isEmpty() &&
content.schema().implementation() == Void.class &&
content.schema().ref().isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Copyright 2005-2026 Qlik
*
* The contents of this file is subject to the terms of the Apache 2.0 open source license available at
* http://www.opensource.org/licenses/apache-2.0
*
* Restlet is a registered trademark of QlikTech International AB.
*/

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 OpenApiApplication extends Application {
/**
* Default path for the OpenAPI specification. Can be overridden by overriding the
* {@link #getOpenApiSpecificationPath()} method.
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright 2005-2026 Qlik
*
* The contents of this file is subject to the terms of the Apache 2.0 open source license available at
* http://www.opensource.org/licenses/apache-2.0
*
* Restlet is a registered trademark of QlikTech International AB.
*/

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;

public 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;

public 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<Variant> 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);
}
}
Loading