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
53 changes: 53 additions & 0 deletions org.restlet.ext.openapi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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-version}</version>
</dependency>

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

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

<dependency>
<groupId>org.restlet</groupId>
<artifactId>org.restlet</artifactId>
<version>${project.version}</version>
</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,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<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);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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<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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<RestletOpenApiContext> 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;
}
}
Loading