Skip to content

Commit 0e17a26

Browse files
authored
Merge pull request #3733 from jooby-project/openapijavadoc
WIP: open-api: parse javadoc
2 parents be0fb2c + b43847d commit 0e17a26

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3108
-236
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ TODO
2828
.interp
2929
tmp
3030
checkstyle
31-
javadoc
3231
*.mv.db
3332
versions
3433
out

modules/jooby-gradle-plugin/src/main/java/io/jooby/gradle/OpenAPITask.java

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import org.gradle.api.tasks.TaskAction;
1414

1515
import edu.umd.cs.findbugs.annotations.Nullable;
16+
17+
import java.io.File;
1618
import java.nio.file.Path;
1719
import java.util.List;
1820
import java.util.Optional;
@@ -44,27 +46,27 @@ public void generate() throws Throwable {
4446

4547
String mainClass = Optional.ofNullable(this.mainClass)
4648
.orElseGet(() -> computeMainClassName(projects));
47-
48-
Path outputDir = classes(getProject(), false);
49-
// Reduce lookup to current project: See https://github.com/jooby-project/jooby/issues/2756
50-
String metaInf =
51-
outputDir
52-
.resolve("META-INF")
53-
.resolve("services")
54-
.resolve("io.jooby.MvcFactory")
55-
.toAbsolutePath()
56-
.toString();
49+
var sources = projects.stream()
50+
.flatMap(project -> {
51+
var sourceSet = sourceSet(project, false);
52+
return sourceSet.stream()
53+
.flatMap(it -> it.getAllSource().getSrcDirs().stream())
54+
.map(File::toPath);
55+
})
56+
.distinct()
57+
.toList(); Path outputDir = classes(getProject(), false);
5758

5859
ClassLoader classLoader = createClassLoader(projects);
5960

6061
getLogger().info("Generating OpenAPI: " + mainClass);
6162
getLogger().debug("Using classloader: " + classLoader);
6263
getLogger().debug("Output directory: " + outputDir);
63-
getLogger().debug("META-INF: " + metaInf);
64+
getLogger().debug("Source directories: " + sources);
6465

65-
OpenAPIGenerator tool = new OpenAPIGenerator(metaInf);
66+
OpenAPIGenerator tool = new OpenAPIGenerator();
6667
tool.setClassLoader(classLoader);
6768
tool.setOutputDir(outputDir);
69+
tool.setSources(sources);
6870
trim(includes).ifPresent(tool::setIncludes);
6971
trim(excludes).ifPresent(tool::setExcludes);
7072

modules/jooby-maven-plugin/src/main/java/io/jooby/maven/OpenAPIMojo.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,21 @@ protected void doExecute(@NonNull List<MavenProject> projects, @NonNull String m
4949
throws Exception {
5050
ClassLoader classLoader = createClassLoader(projects);
5151
Path outputDir = Paths.get(project.getBuild().getOutputDirectory());
52-
// Reduce lookup to current project: See https://github.com/jooby-project/jooby/issues/2756
53-
String metaInf =
54-
outputDir
55-
.resolve("META-INF")
56-
.resolve("services")
57-
.resolve("io.jooby.MvcFactory")
58-
.toAbsolutePath()
59-
.toString();
52+
var sources =
53+
projects.stream()
54+
.map(project -> Paths.get(project.getBuild().getSourceDirectory()))
55+
.distinct()
56+
.toList();
6057

6158
getLog().info("Generating OpenAPI: " + mainClass);
6259
getLog().debug("Using classloader: " + classLoader);
6360
getLog().debug("Output directory: " + outputDir);
64-
getLog().debug("META-INF: " + metaInf);
61+
getLog().debug("Source directories: " + sources);
6562

66-
OpenAPIGenerator tool = new OpenAPIGenerator(metaInf);
63+
OpenAPIGenerator tool = new OpenAPIGenerator();
6764
tool.setClassLoader(classLoader);
6865
tool.setOutputDir(outputDir);
66+
tool.setSources(sources);
6967
trim(includes).ifPresent(tool::setIncludes);
7068
trim(excludes).ifPresent(tool::setExcludes);
7169

modules/jooby-openapi/pom.xml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
<artifactId>swagger-models</artifactId>
6565
</dependency>
6666

67+
<dependency>
68+
<groupId>com.puppycrawl.tools</groupId>
69+
<artifactId>checkstyle</artifactId>
70+
<version>10.26.1</version>
71+
</dependency>
72+
6773
<dependency>
6874
<groupId>commons-codec</groupId>
6975
<artifactId>commons-codec</artifactId>
@@ -134,13 +140,6 @@
134140
<version>1.17.6</version>
135141
<scope>test</scope>
136142
</dependency>
137-
138-
<dependency>
139-
<groupId>com.puppycrawl.tools</groupId>
140-
<artifactId>checkstyle</artifactId>
141-
<version>10.26.1</version>
142-
<scope>test</scope>
143-
</dependency>
144143
</dependencies>
145144

146145
<build>

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
import org.objectweb.asm.Type;
2121
import org.objectweb.asm.tree.*;
2222

23-
import io.jooby.Context;
24-
import io.jooby.MediaType;
25-
import io.jooby.Router;
26-
import io.jooby.Session;
23+
import io.jooby.*;
2724
import io.jooby.annotation.ContextParam;
2825
import io.jooby.annotation.CookieParam;
2926
import io.jooby.annotation.FormParam;
@@ -251,9 +248,18 @@ public static List<OperationExt> parse(
251248
return parse(ctx, prefix, type);
252249
}
253250
}
254-
return Collections.emptyList();
251+
return List.of();
255252
}
256253

254+
/**
255+
* This is the main entrypoint beside there is a public {@link #parse(ParserContext, String,
256+
* Signature, MethodInsnNode)}.
257+
*
258+
* @param ctx
259+
* @param prefix
260+
* @param type
261+
* @return
262+
*/
257263
public static List<OperationExt> parse(ParserContext ctx, String prefix, Type type) {
258264
List<OperationExt> result = new ArrayList<>();
259265
ClassNode classNode = ctx.classNode(type);
@@ -262,7 +268,75 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
262268
ctx.debugHandler(method);
263269
result.addAll(routerMethod(ctx, prefix, classNode, method));
264270
}
265-
result.forEach(it -> it.setController(classNode));
271+
var javaDocParser = ctx.javadoc();
272+
for (OperationExt operationExt : result) {
273+
operationExt.setController(classNode);
274+
try {
275+
var className = operationExt.getControllerName().replace("/", ".");
276+
javaDocParser
277+
.parse(className)
278+
.ifPresent(
279+
doc -> {
280+
operationExt.setPathDescription(doc.getDescription());
281+
operationExt.setPathSummary(doc.getSummary());
282+
doc.getTags().forEach(operationExt::addTag);
283+
if (!doc.getExtensions().isEmpty()) {
284+
operationExt.setPathExtensions(doc.getExtensions());
285+
}
286+
var parameterNames =
287+
Optional.ofNullable(operationExt.getNode().parameters)
288+
.orElse(List.of())
289+
.stream()
290+
.map(p -> p.name)
291+
.toList();
292+
doc.getMethod(operationExt.getOperationId(), parameterNames)
293+
.ifPresent(
294+
methodDoc -> {
295+
operationExt.setSummary(methodDoc.getSummary());
296+
operationExt.setDescription(methodDoc.getDescription());
297+
if (!methodDoc.getExtensions().isEmpty()) {
298+
operationExt.setExtensions(methodDoc.getExtensions());
299+
}
300+
methodDoc.getTags().forEach(operationExt::addTag);
301+
// Parameters
302+
for (var parameterName : parameterNames) {
303+
var paramExt =
304+
operationExt.getParameters().stream()
305+
.filter(p -> p.getName().equals(parameterName))
306+
.findFirst()
307+
.map(ParameterExt.class::cast)
308+
.orElse(null);
309+
var paramDoc = methodDoc.getParameterDoc(parameterName);
310+
if (paramDoc != null) {
311+
if (paramExt == null) {
312+
operationExt.getRequestBody().setDescription(paramDoc);
313+
} else {
314+
paramExt.setDescription(paramDoc);
315+
}
316+
}
317+
}
318+
// return types
319+
var defaultResponse = operationExt.getDefaultResponse();
320+
if (defaultResponse != null) {
321+
defaultResponse.setDescription(methodDoc.getReturnDoc());
322+
}
323+
for (var throwsDoc : methodDoc.getThrows().values()) {
324+
var response =
325+
operationExt.getResponse(
326+
Integer.toString(throwsDoc.getStatusCode().value()));
327+
if (response == null) {
328+
response =
329+
operationExt.addResponse(
330+
Integer.toString(throwsDoc.getStatusCode().value()));
331+
}
332+
response.setDescription(throwsDoc.getText());
333+
}
334+
});
335+
});
336+
} catch (Exception x) {
337+
throw SneakyThrows.propagate(x);
338+
}
339+
}
266340
return result;
267341
}
268342

@@ -382,7 +456,7 @@ private static List<ParameterExt> routerArguments(
382456

383457
if (paramType == ParamType.BODY) {
384458
RequestBodyExt body = new RequestBodyExt();
385-
body.setRequired(required);
459+
body.setRequired(true);
386460
body.setJavaType(javaType);
387461
requestBody.accept(body);
388462
} else if (paramType == ParamType.FORM) {

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ModelConverterExt.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.util.Iterator;
1111
import java.util.Set;
1212

13-
import com.fasterxml.jackson.databind.JavaType;
1413
import com.fasterxml.jackson.databind.ObjectMapper;
1514
import io.jooby.FileUpload;
1615
import io.jooby.Jooby;
@@ -37,7 +36,7 @@ public ModelConverterExt(ObjectMapper mapper) {
3736
@Override
3837
public Schema resolve(
3938
AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
40-
JavaType javaType = _mapper.getTypeFactory().constructType(type.getType());
39+
var javaType = _mapper.getTypeFactory().constructType(type.getType());
4140
if (javaType.isCollectionLikeType() || javaType.isArrayType()) {
4241
if (isFile(javaType.getContentType().getRawClass())) {
4342
return new ArraySchema().items(new FileSchema());
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi;
7+
8+
import java.lang.reflect.Type;
9+
import java.util.*;
10+
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import io.swagger.v3.core.converter.*;
15+
import io.swagger.v3.core.util.ReferenceTypeUtils;
16+
import io.swagger.v3.oas.models.media.Schema;
17+
18+
public class ModelConvertersExt extends ModelConverters {
19+
20+
/** Copy of {@link ModelConverterContextImpl} required for access to schemas by class name. */
21+
private static class ModelConverterContextExt implements ModelConverterContext {
22+
private static final Logger LOGGER = LoggerFactory.getLogger(ModelConverterContextExt.class);
23+
24+
private final List<ModelConverter> converters;
25+
private final Map<String, Schema> modelByName;
26+
private final HashMap<AnnotatedType, Schema> modelByType;
27+
private final Set<AnnotatedType> processedTypes;
28+
29+
public ModelConverterContextExt(List<ModelConverter> converters) {
30+
this.converters = converters;
31+
modelByName = new TreeMap<>();
32+
modelByType = new HashMap<>();
33+
processedTypes = new HashSet<>();
34+
}
35+
36+
public ModelConverterContextExt(ModelConverter converter) {
37+
this(new ArrayList<ModelConverter>());
38+
converters.add(converter);
39+
}
40+
41+
@Override
42+
public Iterator<ModelConverter> getConverters() {
43+
return converters.iterator();
44+
}
45+
46+
@Override
47+
public void defineModel(String name, Schema model) {
48+
AnnotatedType aType = null;
49+
defineModel(name, model, aType, null);
50+
}
51+
52+
@Override
53+
public void defineModel(String name, Schema model, Type type, String prevName) {
54+
defineModel(name, model, new AnnotatedType().type(type), prevName);
55+
}
56+
57+
@Override
58+
public void defineModel(String name, Schema model, AnnotatedType type, String prevName) {
59+
if (LOGGER.isTraceEnabled()) {
60+
LOGGER.trace(String.format("defineModel %s %s", name, model));
61+
}
62+
modelByName.put(name, model);
63+
64+
if (prevName != null && !prevName.isBlank() && !prevName.equals(name)) {
65+
modelByName.remove(prevName);
66+
}
67+
68+
if (type != null && type.getType() != null) {
69+
modelByType.put(type, model);
70+
}
71+
}
72+
73+
@Override
74+
public Map<String, Schema> getDefinedModels() {
75+
return Collections.unmodifiableMap(modelByName);
76+
}
77+
78+
@Override
79+
public Schema resolve(AnnotatedType type) {
80+
AnnotatedType aType = ReferenceTypeUtils.unwrapReference(type);
81+
if (aType != null) {
82+
return resolve(aType);
83+
}
84+
85+
if (processedTypes.contains(type)) {
86+
return modelByType.get(type);
87+
} else {
88+
processedTypes.add(type);
89+
}
90+
if (LOGGER.isDebugEnabled()) {
91+
LOGGER.debug(String.format("resolve %s", type.getType()));
92+
}
93+
Iterator<ModelConverter> converters = this.getConverters();
94+
Schema resolved = null;
95+
if (converters.hasNext()) {
96+
ModelConverter converter = converters.next();
97+
LOGGER.trace("trying extension {}", converter);
98+
resolved = converter.resolve(type, this, converters);
99+
}
100+
if (resolved != null) {
101+
modelByType.put(type, resolved);
102+
103+
Schema resolvedImpl = resolved;
104+
if (resolvedImpl.getName() != null) {
105+
modelByName.put(resolvedImpl.getName(), resolved);
106+
}
107+
} else {
108+
processedTypes.remove(type);
109+
}
110+
111+
return resolved;
112+
}
113+
}
114+
115+
public ModelConvertersExt() {
116+
super(false);
117+
}
118+
119+
@Override
120+
public ResolvedSchemaExt readAllAsResolvedSchema(Type type) {
121+
return (ResolvedSchemaExt) super.readAllAsResolvedSchema(type);
122+
}
123+
124+
@Override
125+
public ResolvedSchemaExt readAllAsResolvedSchema(AnnotatedType type) {
126+
return (ResolvedSchemaExt) super.readAllAsResolvedSchema(type);
127+
}
128+
129+
@Override
130+
public ResolvedSchemaExt resolveAsResolvedSchema(AnnotatedType type) {
131+
var context = new ModelConverterContextExt(getConverters());
132+
var resolvedSchema = new ResolvedSchemaExt();
133+
resolvedSchema.schema = context.resolve(type);
134+
resolvedSchema.referencedSchemas = context.getDefinedModels();
135+
resolvedSchema.referencedSchemasByType = new HashMap<>();
136+
context.modelByType.forEach(
137+
(annotatedType, schema) ->
138+
resolvedSchema.referencedSchemasByType.put(annotatedType.getType(), schema));
139+
return resolvedSchema;
140+
}
141+
}

0 commit comments

Comments
 (0)