diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/ClassDocumentation.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/ClassDocumentation.java index af3bdc5c..d6059a2a 100644 --- a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/ClassDocumentation.java +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/ClassDocumentation.java @@ -169,11 +169,12 @@ public Map getMethodsJavadocByIdentifier() { return methodsJavadocByIdentifier; } + public Optional getSummary() { + return Optional.ofNullable(javadocWrapper).flatMap(JavadocWrapper::getSummary); + } + public Optional getDescription() { - if(javadocWrapper != null) { - return javadocWrapper.getDescription(); - } - return Optional.empty(); + return Optional.ofNullable(javadocWrapper).flatMap(JavadocWrapper::getDescription); } @Override diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocElementParser.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocElementParser.java new file mode 100644 index 00000000..2bb06f48 --- /dev/null +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocElementParser.java @@ -0,0 +1,105 @@ +package io.github.kbuntrock.javadoc; + +import com.github.javaparser.javadoc.description.JavadocDescription; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocInlineTag; +import com.github.javaparser.javadoc.description.JavadocSnippet; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JavadocElementParser { + + public static final Pattern hrefLinkRegex = Pattern.compile("href=\"(.*?)\""); + + public static Optional getSummary(JavadocDescription description, String endOfLineReplacement) { + return processJavadocElements(description, endOfLineReplacement, elements -> elements + .filter(onlyTagsOfType(JavadocTag.SUMMARY)) + .map(JavadocElementParser::toTagContent)); + } + + public static Optional getDescription(JavadocDescription description, String endOfLineReplacement1) { + return processJavadocElements(description, endOfLineReplacement1, elements -> elements + .filter(JavadocElementParser::onlySnippetsAndFormattingTags) + .map(JavadocElementParser::formatDescriptionElement)); + } + + private static Optional processJavadocElements( + JavadocDescription description, + String endOfLineReplacement, + Function, Stream> elementProcessor) { + return Optional.ofNullable(description) + .map(JavadocDescription::getElements) + .map(Collection::stream) + .map(elementProcessor) + .map(s -> s.collect(Collectors.joining())) + .map(s -> s.replaceAll("\r\n", "\n")) + .map(s -> removeNewlinesIfActivated(s, endOfLineReplacement)) + .filter(text -> !text.isEmpty()) + .map(String::trim); + } + + private static String formatDescriptionElement(JavadocDescriptionElement javadocElement) { + if(javadocElement instanceof JavadocInlineTag) { + JavadocInlineTag tag = (JavadocInlineTag) javadocElement; + switch(JavadocTag.fromString(tag.getName())) { + case CODE: + return toInlineCodeMarkdown(tag); + case SEE: + return toEmphasizedMarkdown(tag); + case LINK: + return toHrefMarkdown(tag); + default: + return tag.getContent(); + } + } + + return javadocElement.toText(); + } + + private static String toInlineCodeMarkdown(JavadocInlineTag tag) { + return "`" + tag.getContent().trim() + "`"; + } + + private static String toEmphasizedMarkdown(JavadocInlineTag tag) { + return "*" + tag.getContent().trim() + "*"; + } + + private static String toHrefMarkdown(JavadocInlineTag tag) { + Matcher m = hrefLinkRegex.matcher(tag.getContent()); + String url = null; + if(m.find()) { + url = m.group(1); + } + return String.format("[%s](%s)", url, url); + } + + private static Predicate onlyTagsOfType(JavadocTag tag) { + return e -> (e instanceof JavadocInlineTag && tag.toString().toLowerCase().equals(((JavadocInlineTag) e).getName())); + } + + private static boolean onlySnippets(JavadocDescriptionElement e) { + return e instanceof JavadocSnippet; + } + + private static String toTagContent(JavadocDescriptionElement t) { + return ((JavadocInlineTag) t).getContent().trim(); + } + + private static boolean onlySnippetsAndFormattingTags(JavadocDescriptionElement e) { + return onlySnippets(e) || ( + e instanceof JavadocInlineTag && + JavadocTag.isFormattingTag(((JavadocInlineTag) e).getName())); + } + + private static String removeNewlinesIfActivated(String text, String endOfLineReplacement) { + return endOfLineReplacement != null + ? text.replaceAll("\\r\\n", endOfLineReplacement).replaceAll("\\n", endOfLineReplacement) + : text; + } +} diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocParser.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocParser.java index 332ff886..73a81fb4 100644 --- a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocParser.java +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocParser.java @@ -53,8 +53,7 @@ public JavadocParser(final List filesToScan, final JavadocConfiguration ja charset = Charset.forName(javadocConfiguration.getEncoding()); } else { logger.warn("Encoding " + javadocConfiguration.getEncoding() + " is not supported. UTF-8 will be used instead."); - logger.warn("Supported encoding on this JVM are : " + Charset.availableCharsets().keySet().stream() - .collect(Collectors.joining(", "))); + logger.warn("Supported encoding on this JVM are : " + String.join(", ", Charset.availableCharsets().keySet())); } parserConfiguration.setCharacterEncoding(charset); debugScan = javadocConfiguration.isDebugScan(); @@ -85,10 +84,12 @@ private void printDebug() { logger.debug("-------- PRINT JAVADOC SCAN RESULTS ----------"); for(final ClassDocumentation classDocumentation : javadocMap.values()) { logger.debug("Class documentation for : " + classDocumentation.getCompleteName()); + logger.debug("Summary : " + classDocumentation.getSummary()); logger.debug("Description : " + classDocumentation.getDescription()); if(!classDocumentation.getMethodsJavadocByIdentifier().isEmpty()) { for(final Entry entry : classDocumentation.getMethodsJavadocByIdentifier().entrySet()) { logger.debug("Method doc for : " + entry.getKey()); + logger.debug("Summary : " + entry.getValue().getSummary()); logger.debug("Description : " + entry.getValue().getDescription()); entry.getValue().printParameters(); entry.getValue().printReturn(); diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocTag.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocTag.java new file mode 100644 index 00000000..49ac58f1 --- /dev/null +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocTag.java @@ -0,0 +1,46 @@ +package io.github.kbuntrock.javadoc; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public enum JavadocTag { + // Tags with extractable information (content will be stripped from the general description) + SUMMARY, + AUTHOR, + + // Formatting tags (text content should not be stripped) + CODE, + LINK, + SEE; + + @Override + public String toString() { + return this.name().toLowerCase(Locale.ROOT); + } + + public static JavadocTag fromString(String tagName) { + return JavadocTag.valueOf(tagName.toUpperCase(Locale.ROOT)); + } + + private static final HashSet formattingTags = new HashSet<>(); + + static { + formattingTags.add(CODE); + formattingTags.add(LINK); + formattingTags.add(SEE); + } + + public static boolean isFormattingTag(String tagName) { + try { + JavadocTag tag = fromString(tagName); + return isFormattingTag(tag); + } catch (IllegalArgumentException e) { + return false; + } + } + + public static boolean isFormattingTag(JavadocTag tag) { + return formattingTags.contains(tag); + } +} diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocWrapper.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocWrapper.java index 2bbead0f..d29b0153 100644 --- a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocWrapper.java +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/javadoc/JavadocWrapper.java @@ -70,17 +70,12 @@ public Optional getReturnBlockTag() { return Optional.empty(); } + public Optional getSummary() { + return JavadocElementParser.getSummary(javadoc.getDescription(), endOfLineReplacement); + } + public Optional getDescription() { - if(javadoc.getDescription() != null) { - String desc = javadoc.getDescription().toText(); - if(endOfLineReplacement != null) { - desc = desc.replaceAll("\\r\\n", endOfLineReplacement).replaceAll("\\n", endOfLineReplacement); - } - if(!desc.isEmpty()) { - return Optional.of(desc); - } - } - return Optional.empty(); + return JavadocElementParser.getDescription(javadoc.getDescription(), endOfLineReplacement); } public boolean isInheritTagFound() { @@ -94,15 +89,14 @@ public void printParameters() { Logger.INSTANCE.getLogger().debug(entry.getKey() + " : " + entry.getValue().getContent().toText()); } } - } public void printReturn() { if(blockTagsByType != null) { final Optional returnTag = getReturnBlockTag(); - if(returnTag.isPresent()) { - Logger.INSTANCE.getLogger().debug("Return : " + returnTag.get().getContent().toText()); - } + returnTag.ifPresent(javadocBlockTag -> + Logger.INSTANCE.getLogger().debug("Return : " + javadocBlockTag.getContent().toText())); } } + } diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/YamlWriter.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/YamlWriter.java index 2b3316b8..5bb977ca 100644 --- a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/YamlWriter.java +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/YamlWriter.java @@ -137,7 +137,9 @@ public void write(final File file, final TagLibrary tagLibrary) throws IOExcepti specification.setTags(tagLibrary.getSortedTags().stream() .map(tag -> { if(tagLibrary.hasJavadocMap() && tag.getDescription() == null) { - ClassDocumentation classDocumentation = tagLibrary.getJavadocMap().get(tag.getClazz().getCanonicalName()); + ClassDocumentation classDocumentation = tagLibrary.getJavadocMap() + .computeIfAbsent(tag.getClazz().getCanonicalName(), + k -> new ClassDocumentation(tag.getClazz().getCanonicalName(), tag.getClazz().getSimpleName())); // Even if there is no declared class documentation, we may enhance it with javadoc on interface and/or abstract classes if(classDocumentation == null) { classDocumentation = new ClassDocumentation(tag.getClazz().getCanonicalName(), tag.getClazz().getSimpleName()); @@ -147,16 +149,15 @@ public void write(final File file, final TagLibrary tagLibrary) throws IOExcepti "Class documentation found for tag " + tag.getClazz().getSimpleName() + " ? " + (classDocumentation != null)); classDocumentation.inheritanceEnhancement(tag.getClazz(), ClassDocumentation.EnhancementType.METHODS, tagLibrary.getJavadocMap()); - final Optional description = classDocumentation.getDescription(); - if(description.isPresent()) { - return new TagElement(tag.computeConfiguredName(apiConfiguration), description.get()); - } + return new TagElement( + tag.computeConfiguredName(apiConfiguration), + classDocumentation.getDescription().orElse(null)); } return new TagElement(tag.computeConfiguredName(apiConfiguration), tag.getDescription()); }).collect(Collectors.toList())); - // Write the "paths" section (all url / http verbs combinaison scanned) + // Write the "paths" section (all url / http verbs combination scanned) specification.setPaths(createPaths(tagLibrary)); final Map schemaSection = createSchemaSection(tagLibrary); @@ -226,8 +227,8 @@ private Map> createPaths(final TagLibrary tagLibr final String computedTagName = tag.computeConfiguredName(apiConfiguration); operation.getTags().add(computedTagName); operation.setOperationId(ObjectsUtils.nonNullElse(endpoint.getOperationAnnotationInfo().getOperationId(), - apiConfiguration.getOperationIdHelper().toOperationId(tag.getName(), computedTagName, endpoint.getName())) - ); + apiConfiguration.getOperationIdHelper().toOperationId(tag.getName(), computedTagName, endpoint.getName())) + ); if(apiConfiguration.isLoopbackOperationName()) { operation.setLoopbackOperationName(endpoint.getName()); } @@ -248,12 +249,16 @@ private Map> createPaths(final TagLibrary tagLibr operation.setDescription(endpoint.getOperationAnnotationInfo().getDescription()); } else { if(methodJavadoc != null) { - operation.setDescription(methodJavadoc.getJavadoc().getDescription().toText()); + operation.setDescription(methodJavadoc.getDescription().orElse(null)); } } if(endpoint.getOperationAnnotationInfo().getSummary() != null) { operation.setSummary(endpoint.getOperationAnnotationInfo().getSummary()); + } else { + if(methodJavadoc != null) { + operation.setSummary(methodJavadoc.getSummary().orElse(null)); + } } // Warning on paths @@ -273,7 +278,8 @@ private Map> createPaths(final TagLibrary tagLibr // All parameters which are not in the body for(final ParameterObject parameter : endpoint.getParameters().stream() - .filter(x -> ParameterLocation.BODY != x.getLocation() && ParameterLocation.BODY_PART != x.getLocation()).collect(Collectors.toList())) { + .filter(x -> ParameterLocation.BODY != x.getLocation() && ParameterLocation.BODY_PART != x.getLocation()) + .collect(Collectors.toList())) { final ParameterElement parameterElement = new ParameterElement(); parameterElement.setName(parameter.getName()); parameterElement.setIn(parameter.getLocation().toString().toLowerCase(Locale.ENGLISH)); @@ -290,8 +296,8 @@ private Map> createPaths(final TagLibrary tagLibr } parameterElement.setSchema(schema); - - if(parameter.getJavaClass() == Object.class && parameter.isAllowEmptyValue() && parameter.getLocation() == ParameterLocation.QUERY) { + if(parameter.getJavaClass() == Object.class && parameter.isAllowEmptyValue() + && parameter.getLocation() == ParameterLocation.QUERY) { parameterElement.setSchema(null); } @@ -321,6 +327,9 @@ private Map> createPaths(final TagLibrary tagLibr if(javadocParamWrapper != null) { final Optional desc = javadocParamWrapper.getDescription(); parameterElement.setDescription(desc.get()); + + final Optional summary = javadocParamWrapper.getSummary(); + parameterElement.setSummary(summary.orElse(null)); } } } @@ -342,7 +351,7 @@ private Map> createPaths(final TagLibrary tagLibr final ParameterObject body = bodies.get(0); final Content requestBodyContent = isFormData(bodies) - ? Content.fromMultipartBodies(bodies, tagLibrary) + ? Content.fromMultipartBodies(bodies, tagLibrary) : Content.fromDataObject(body, tagLibrary); if(body.getFormats() != null) { @@ -468,10 +477,11 @@ private static boolean isFormData(List bodies) { /** * Check if a common operation exist and merge it if possible - * @param tag the tag to document - * @param paths the already documented paths in this tag + * + * @param tag the tag to document + * @param paths the already documented paths in this tag * @param operation the current operation to document - * @param response the current operation response (without the default ones) + * @param response the current operation response (without the default ones) */ private void mergeCommonOperations(Tag tag, Map> paths, Operation operation, Response response) { // Check if on operation already exist for this name (GET / POST / ...) and path @@ -482,7 +492,7 @@ private void mergeCommonOperations(Tag tag, Map> // Check if there is a collision in response content type. Object existingContent = existingOperation.getResponses().get(response.getCode()); for(Entry responseContent : response.getContent().entrySet()) { - if(existingContent instanceof Response ) { + if(existingContent instanceof Response) { Response existingResponse = (Response) existingContent; if(existingResponse.getContent().containsKey(responseContent.getKey())) { // There are too many cases: this is uncommon, but it might be a valid case. @@ -506,13 +516,15 @@ private void mergeCommonOperations(Tag tag, Map> } } // Now merging parameters - Map existingParametersByNames = existingOperation.getParameters().stream().collect(Collectors.toMap(ParameterElement::getName, Function.identity())); + Map existingParametersByNames = existingOperation.getParameters().stream() + .collect(Collectors.toMap(ParameterElement::getName, Function.identity())); for(ParameterElement parameter : operation.getParameters()) { ParameterElement existingParameter = existingParametersByNames.get(parameter.getName()); if(existingParameter == null) { existingOperation.getParameters().add(parameter); } else { - if(!existingParameter.getSchema().getType().getNode().toString().equals(parameter.getSchema().getType().getNode().toString())) { + if(!existingParameter.getSchema().getType().getNode().toString() + .equals(parameter.getSchema().getType().getNode().toString())) { Logger.INSTANCE.getLogger().warn("Parameters incoherences detected in path " + operation.getPath()); } } diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/ParameterElement.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/ParameterElement.java index 78f9ae64..d311b3b4 100644 --- a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/ParameterElement.java +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/ParameterElement.java @@ -7,6 +7,8 @@ public class ParameterElement { private String name; @JsonInclude(JsonInclude.Include.NON_NULL) + private String summary; + @JsonInclude(JsonInclude.Include.NON_NULL) private String description; private String in; private boolean required; @@ -55,6 +57,14 @@ public void setSchema(Property schema) { this.schema = schema; } + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + public String getDescription() { return description; } diff --git a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/Schema.java b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/Schema.java index f7170ff8..6dd71bbb 100644 --- a/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/Schema.java +++ b/openapi-maven-plugin/src/main/java/io/github/kbuntrock/yaml/model/Schema.java @@ -24,7 +24,6 @@ import io.github.kbuntrock.utils.OpenApiConstants; import io.github.kbuntrock.utils.OpenApiResolvedType; import io.github.kbuntrock.utils.UnwrappingType; - import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -48,6 +47,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class Schema { + @JsonIgnore + protected String summary; @JsonIgnore protected String description; @JsonIgnore @@ -224,6 +225,9 @@ public Schema(final DataObject wrappedDataObject, if(javadocWrapper != null) { final Optional desc = javadocWrapper.getDescription(); property.setDescription(desc.get()); + + final Optional summary = javadocWrapper.getSummary(); + property.setSummary(summary.orElse(null)); } } @@ -275,6 +279,9 @@ public Schema(final DataObject wrappedDataObject, if(javadocWrapper != null) { final Optional desc = javadocWrapper.getDescription(); property.setDescription(desc.get()); + + final Optional summary = javadocWrapper.getSummary(); + property.setSummary(summary.orElse(null)); } } } @@ -441,6 +448,14 @@ public void setItems(final Schema items) { this.items = items; } + public String getSummary() { + return summary; + } + + public void setSummary(final String summary) { + this.summary = summary; + } + public String getDescription() { return description; } diff --git a/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/jackson/JacksonJsonPropertyController.java b/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/jackson/JacksonJsonPropertyController.java index 7eb8fa5f..d2437435 100644 --- a/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/jackson/JacksonJsonPropertyController.java +++ b/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/jackson/JacksonJsonPropertyController.java @@ -16,10 +16,12 @@ public interface JacksonJsonPropertyController { /** - * {@code GET /users} : get the list of users - * - * @return the list of users. - */ + * {@code GET /users} : get the list of users + * {@see SimpleUserDto} + * {@link https://kbuntrock.github.io/openapi-maven-plugin} + * + * @return the list of users. + */ @RequestMapping(method = RequestMethod.GET) List findAll(); } diff --git a/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/javadoc/inheritance/ChildClassOne.java b/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/javadoc/inheritance/ChildClassOne.java index a580b4cd..331ce914 100644 --- a/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/javadoc/inheritance/ChildClassOne.java +++ b/openapi-maven-plugin/src/test/java/io/github/kbuntrock/resources/endpoint/javadoc/inheritance/ChildClassOne.java @@ -9,6 +9,8 @@ /** * A beautiful description of ChildClassOne + * {@summary The first child} + * Subtitle * * @author Kevin Buntrock */ @@ -40,6 +42,7 @@ public boolean canEncapsulate() { /** * Return the name of this controller + * {@summary name getter} * * @return the name */ diff --git a/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/JavadocParserTest.inheritance_test_one.approved.txt b/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/JavadocParserTest.inheritance_test_one.approved.txt index 278e2ae8..b5c3344f 100644 --- a/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/JavadocParserTest.inheritance_test_one.approved.txt +++ b/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/JavadocParserTest.inheritance_test_one.approved.txt @@ -7,7 +7,10 @@ servers: - url: "" tags: - name: ChildClassOne - description: A beautiful description of ChildClassOne + description: |- + A beautiful description of ChildClassOne + + Subtitle paths: /api/child-class-one/can-encapsulate: get: @@ -71,6 +74,7 @@ paths: - ChildClassOne operationId: getName description: Return the name of this controller + summary: name getter responses: 200: description: the name diff --git a/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/SpringClassAnalyserTest.jacksonJsonProperty.approved.txt b/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/SpringClassAnalyserTest.jacksonJsonProperty.approved.txt index 70ded244..b8eb4776 100644 --- a/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/SpringClassAnalyserTest.jacksonJsonProperty.approved.txt +++ b/openapi-maven-plugin/src/test/resources/io/github/kbuntrock/SpringClassAnalyserTest.jacksonJsonProperty.approved.txt @@ -14,7 +14,10 @@ paths: tags: - JacksonJsonPropertyController operationId: findAll - description: "{@code GET /users} : get the list of users" + description: |- + `GET /users` : get the list of users + *SimpleUserDto* + [https://kbuntrock.github.io/openapi-maven-plugin](https://kbuntrock.github.io/openapi-maven-plugin) responses: 200: description: the list of users.