
io.muserver.rest.OpenApiDocumentor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mu-server Show documentation
Show all versions of mu-server Show documentation
A simple but powerful web server framework
The newest version!
package io.muserver.rest;
import io.muserver.*;
import io.muserver.openapi.*;
import jakarta.ws.rs.ext.ParamConverterProvider;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static io.muserver.Mutils.notNull;
import static io.muserver.openapi.ComponentsObjectBuilder.componentsObject;
import static io.muserver.openapi.PathItemObjectBuilder.pathItemObject;
import static io.muserver.openapi.PathsObjectBuilder.pathsObject;
import static io.muserver.openapi.RequestBodyObjectBuilder.requestBodyObject;
import static io.muserver.openapi.ResponsesObjectBuilder.mergeResponses;
import static io.muserver.openapi.ServerObjectBuilder.serverObject;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
class OpenApiDocumentor implements MuHandler {
private final List roots;
private final String openApiJsonUrl;
private final OpenAPIObject openAPIObject;
private final String openApiHtmlUrl;
private final String openApiHtmlCss;
private final CORSConfig corsConfig;
private final List customSchemas;
private final SchemaObjectCustomizer schemaObjectCustomizer;
private final List paramConverterProviders;
OpenApiDocumentor(List roots, String openApiJsonUrl, String openApiHtmlUrl, OpenAPIObject openAPIObject, String openApiHtmlCss, CORSConfig corsConfig, List customSchemas, SchemaObjectCustomizer schemaObjectCustomizer, List paramConverterProviders) {
this.customSchemas = customSchemas;
this.schemaObjectCustomizer = schemaObjectCustomizer;
this.paramConverterProviders = paramConverterProviders;
notNull("openAPIObject", openAPIObject);
this.corsConfig = corsConfig;
this.roots = roots;
this.openApiJsonUrl = openApiJsonUrl == null ? null : Mutils.trim(openApiJsonUrl, "/");
this.openApiHtmlUrl = openApiHtmlUrl == null ? null : Mutils.trim(openApiHtmlUrl, "/");
this.openAPIObject = openAPIObject;
this.openApiHtmlCss = openApiHtmlCss;
}
@Override
public boolean handle(MuRequest request, MuResponse response) throws Exception {
String relativePath = Mutils.trim(request.relativePath(), "/");
if (request.method() != Method.GET || (!relativePath.equals(openApiJsonUrl) && !relativePath.equals(openApiHtmlUrl))) {
return false;
}
List tags = new ArrayList<>();
Map pathItemBuilders = new LinkedHashMap<>();
for (ResourceClass root : roots) {
addResourceClass(0, "", tags, pathItemBuilders, root);
}
Map pathItems = pathItemBuilders.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().build()));
ComponentsObject components = openAPIObject.components();
if (!customSchemas.isEmpty()) {
ComponentsObjectBuilder componentsObjectBuilder = componentsObject(components);
Map schemas = (components != null && components.schemas() != null) ? components.schemas() : new HashMap<>();
for (SchemaReference customSchema : customSchemas) {
if (!schemas.containsKey(customSchema.id)) {
schemas.put(customSchema.id, customSchema.schema);
}
}
components = componentsObjectBuilder.withSchemas(schemas).build();
}
OpenAPIObjectBuilder api = OpenAPIObjectBuilder.openAPIObject()
.withInfo(openAPIObject.info())
.withExternalDocs(openAPIObject.externalDocs())
.withSecurity(openAPIObject.security())
.withComponents(components)
.withServers(openAPIObject.servers() != null ? openAPIObject.servers() :
request.contextPath().length() > 0 ?
singletonList(
serverObject()
.withUrl(request.contextPath())
.build())
: null
)
.withPaths(pathsObject().withPathItemObjects(pathItems).build())
.withTags(tags);
OpenAPIObject builtApi = api.build();
if (relativePath.equals(openApiJsonUrl)) {
response.contentType(ContentTypes.APPLICATION_JSON);
corsConfig.writeHeadersInternal(request, response, emptySet());
response.headers().set("Access-Control-Allow-Methods", "GET");
try (OutputStreamWriter osw = new OutputStreamWriter(response.outputStream(), StandardCharsets.UTF_8);
BufferedWriter writer = new BufferedWriter(osw, 8192)) {
builtApi.writeJson(writer);
}
} else {
response.contentType(ContentTypes.TEXT_HTML_UTF8);
response.headers().set("X-UA-Compatible", "IE=edge");
try (OutputStreamWriter osw = new OutputStreamWriter(response.outputStream(), StandardCharsets.UTF_8);
BufferedWriter writer = new BufferedWriter(osw, 8192)) {
new HtmlDocumentor(writer, builtApi, openApiHtmlCss, request.uri()).writeHtml();
}
}
return true;
}
private void addResourceClass(int recursiveLevel, String parentResourcePath, List tags, Map pathItems, ResourceClass root) {
if (recursiveLevel == 5) {
return;
}
if (!tags.contains(root.tag)) {
tags.add(root.tag);
}
for (ResourceMethod method : root.resourceMethods) {
if (method.isSubResourceLocator()) {
ResourceClass rc = ResourceClass.forSubResourceLocator(method, method.methodHandle.getReturnType(), null, schemaObjectCustomizer, paramConverterProviders);
String newParentResourcePath = Mutils.join(parentResourcePath, "/", method.resourceClass.pathPattern.pathWithoutRegex);
addResourceClass(recursiveLevel + 1, newParentResourcePath, tags, pathItems, rc);
continue;
}
String path = getPathWithoutRegex(root, method, parentResourcePath);
Map operations;
if (pathItems.containsKey(path)) {
operations = pathItems.get(path).operations();
} else {
operations = new LinkedHashMap<>();
PathItemObjectBuilder pathItem = pathItemObject()
.withOperations(operations);
pathItems.put(path, pathItem);
}
List parameters = method.paramsIncludingLocators().stream()
.filter(p -> p.source.openAPIIn != null && p instanceof ResourceMethodParam.RequestBasedParam)
.map(ResourceMethodParam.RequestBasedParam.class::cast)
.map(p -> p.createDocumentationBuilder().build())
.reduce(new ArrayList<>(), (parameterObjects, parameterObject) -> {
if (parameterObjects.stream().noneMatch(existing -> existing.name().equals(parameterObject.name()) && existing.in().equals(parameterObject.in())))
parameterObjects.add(parameterObject);
return parameterObjects;
}, (list1, list2) -> {
list1.addAll(list2);
return list1;
});
String opIdPath = getPathWithoutRegex(root, method, parentResourcePath).replace("{", "_").replace("}", "_");
String opPath = Mutils.trim(opIdPath, "/").replace("/", "_");
String opKey = method.httpMethod.name().toLowerCase();
OperationObject existing = operations.get(opKey);
if (existing == null) {
existing = method.createOperationBuilder(customSchemas)
.withOperationId(method.httpMethod.name() + "_" + opPath)
.withTags(singletonList(root.tag.name()))
.withParameters(parameters)
.build();
} else {
OperationObject curOO = method.createOperationBuilder(customSchemas).build();
List combinedParams = new ArrayList<>(existing.parameters());
combinedParams.addAll(parameters);
Map mergedContent = new HashMap<>();
if (existing.requestBody() != null && existing.requestBody().content() != null) {
mergedContent.putAll(existing.requestBody().content());
}
if (curOO.requestBody() != null) {
mergedContent.putAll(curOO.requestBody().content());
}
OperationObjectBuilder operationObjectBuilder = OperationObjectBuilder.builderFrom(existing)
.withParameters(combinedParams)
.withResponses(mergeResponses(existing.responses(), curOO.responses()).build())
.withRequestBody(requestBodyObject()
.withRequired(existing.requestBody() != null && existing.requestBody().required() &&
curOO.requestBody() != null && curOO.requestBody().required())
.withDescription(Mutils.coalesce(existing.description(), curOO.description()))
.withContent(mergedContent)
.build());
if (existing.summary() == null && existing.description() == null) {
operationObjectBuilder
.withSummary(curOO.summary())
.withDescription(curOO.description());
}
existing = operationObjectBuilder.build();
}
operations.put(opKey, existing);
}
}
static String getPathWithoutRegex(ResourceClass rc, ResourceMethod rm, String parentResourcePath) {
return "/" + Mutils.trim(Mutils.join(parentResourcePath, "/",
Mutils.join(rc.pathPattern == null ? null : rc.pathPattern.pathWithoutRegex,
"/", rm.pathPattern == null ? null : rm.pathPattern.pathWithoutRegex)), "/");
}
}
class SchemaReference {
final String id;
final Class> type;
final Type genericType;
final SchemaObject schema;
SchemaReference(String id, Class> type, Type genericType, SchemaObject schema) {
this.id = id;
this.type = type;
this.genericType = genericType;
this.schema = schema;
}
static SchemaReference find(List references, Class> type, Type genericType) {
for (SchemaReference reference : references) {
if (reference.type.equals(type)) {
return reference;
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy