Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reflect model/POJO JavaDocs through to Swagger 'description' fields #195

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.sebastian_daschner.jaxrs_analyzer.analysis.classes.ContextClassReader;
import com.sebastian_daschner.jaxrs_analyzer.analysis.classes.JAXRSClassVisitor;
import com.sebastian_daschner.jaxrs_analyzer.analysis.javadoc.JavaDocAnalyzer;
import com.sebastian_daschner.jaxrs_analyzer.analysis.javadoc.JavaDocAnalyzerResults;
import com.sebastian_daschner.jaxrs_analyzer.analysis.results.ResultInterpreter;
import com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.Resources;
Expand Down Expand Up @@ -104,9 +105,9 @@ public Resources analyze(Set<Path> projectClassPaths, Set<Path> projectSourcePat
bytecodeAnalyzer.analyzeBytecode(classResult);
}

javaDocAnalyzer.analyze(projectSourcePaths, classResults);
final JavaDocAnalyzerResults javaDocAnalyzerResults = javaDocAnalyzer.analyze(projectSourcePaths, classResults);

return resultInterpreter.interpret(classResults);
return resultInterpreter.interpret(javaDocAnalyzerResults);
} finally {
lock.unlock();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

/**
Expand All @@ -26,10 +31,12 @@
public class JavaDocAnalyzer {

private final Map<MethodIdentifier, MethodComment> methodComments = new HashMap<>();
private final JavaDocParserVisitor javaDocParserVisitor = new JavaDocParserVisitor(methodComments);

public void analyze(final Set<Path> projectSourcePaths, final Set<ClassResult> classResults) {
public JavaDocAnalyzerResults analyze(final Set<Path> projectSourcePaths, final Set<ClassResult> classResults) {
invokeParser(projectSourcePaths);
combineResults(classResults);
return new JavaDocAnalyzerResults(classResults, javaDocParserVisitor.getClassComments());
}

private void invokeParser(Set<Path> projectSourcePaths) {
Expand All @@ -55,7 +62,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
}
});

files.forEach(path -> parseJavaDoc(path, new JavaDocParserVisitor(methodComments)));
files.forEach(path -> parseJavaDoc(path, javaDocParserVisitor));
}

private static void parseJavaDoc(Path path, JavaDocParserVisitor visitor) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.sebastian_daschner.jaxrs_analyzer.analysis.javadoc;

import com.sebastian_daschner.jaxrs_analyzer.model.javadoc.ClassComment;
import com.sebastian_daschner.jaxrs_analyzer.model.results.ClassResult;

import java.util.Map;
import java.util.Set;

/**
* @author Tristan Perry
*/
public class JavaDocAnalyzerResults {

/**
* Contains a range of class-level data, including the JAX-RS 'endpoint' methods which will be invoked when endpoints are hit.
*/
private final Set<ClassResult> classResults;

/**
* Contains JavaDoc comments/messages for classes and their fields.
*/
private final Map<String, ClassComment> classComments;

public JavaDocAnalyzerResults(Set<ClassResult> classResults, Map<String, ClassComment> classComments) {
this.classResults = classResults;
this.classComments = classComments;
}

public Set<ClassResult> getClassResults() {
return classResults;
}

public Map<String, ClassComment> getClassComments() {
return classComments;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public JavaDocParserVisitor(Map<MethodIdentifier, MethodComment> methodComments)
this.methodComments = methodComments;
}

public Map<String, ClassComment> getClassComments() {
return classComments;
}

@Override
public void visit(PackageDeclaration packageDeclaration, Void arg) {
packageName = packageDeclaration.getNameAsString();
Expand Down Expand Up @@ -99,7 +103,21 @@ private void createFieldComment(Javadoc javadoc, FieldDeclaration field) {
classComment = new ClassComment();
classComments.put(className, classComment);
}
classComment.getFieldComments().add(createMemberParamTag(javadoc.getDescription(), field.getAnnotations().stream()));
classComment.getFieldComments().add(createMemberParamTag(getFieldName(field), javadoc.getDescription(), field.getAnnotations().stream()));
}

/**
* Extracts the field name from FieldDeclaration, using the included variables. I have not seen variables have more than one entry,
* so this best effort approach seems to work fine for the required purpose.
* @param field JavaParser field declaration.
* @return the field name, if extracted.
*/
private String getFieldName(FieldDeclaration field) {
if (field.getVariables().isNonEmpty()) {
return field.getVariables().get(0).getName().getIdentifier();
}

return null;
}

@Override
Expand Down Expand Up @@ -132,15 +150,15 @@ private MemberParameterTag createMethodParameterTag(JavadocBlockTag tag, MethodD
.map(NodeList::stream)
.orElseGet(Stream::empty);

return createMemberParamTag(tag.getContent(), annotations);
return createMemberParamTag(tag.getName().orElse(null), tag.getContent(), annotations);
}

private MemberParameterTag createMemberParamTag(JavadocDescription javadocDescription, Stream<AnnotationExpr> annotationStream) {
private MemberParameterTag createMemberParamTag(String name, JavadocDescription javadocDescription, Stream<AnnotationExpr> annotationStream) {
Map<String, String> annotations = annotationStream
.filter(Expression::isSingleMemberAnnotationExpr)
.collect(Collectors.toMap(a -> a.getName().getIdentifier(),
this::createMemberParamValue));
return new MemberParameterTag(javadocDescription.toText(), annotations);
return new MemberParameterTag(name, javadocDescription.toText(), annotations);
}

private String createMemberParamValue(AnnotationExpr a) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private TypeIdentifier analyzeInternal(final JsonValue jsonValue) {

private TypeIdentifier analyzeInternal(final JsonArray jsonArray) {
final TypeIdentifier containedIdentifier = jsonArray.isEmpty() ? TypeIdentifier.ofType(Types.OBJECT) : analyzeInternal(jsonArray.get(0));
final TypeRepresentation containedRepresentation = typeRepresentations.getOrDefault(containedIdentifier, TypeRepresentation.ofConcrete(containedIdentifier));
final TypeRepresentation containedRepresentation = typeRepresentations.getOrDefault(containedIdentifier, TypeRepresentation.ofConcreteBuilder().identifier(containedIdentifier).build());

final TypeIdentifier existingCollection = findExistingCollection(containedRepresentation);
if (existingCollection != null) {
Expand All @@ -96,7 +96,7 @@ private TypeIdentifier analyzeInternal(final JsonObject jsonObject) {
return existing;

final TypeIdentifier identifier = TypeIdentifier.ofDynamic();
typeRepresentations.put(identifier, TypeRepresentation.ofConcrete(identifier, properties));
typeRepresentations.put(identifier, TypeRepresentation.ofConcreteBuilder().identifier(identifier).properties(properties).build());
return identifier;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,47 @@

package com.sebastian_daschner.jaxrs_analyzer.analysis.results;

import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.getAnnotation;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.getFieldDescriptor;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.getMethodSignature;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.getReturnType;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.isAnnotationPresent;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.isAssignableTo;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.loadClassFromType;
import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.toClassName;
import static com.sebastian_daschner.jaxrs_analyzer.model.Types.COLLECTION;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreType;
import com.sebastian_daschner.jaxrs_analyzer.model.Types;
import com.sebastian_daschner.jaxrs_analyzer.model.javadoc.ClassComment;
import com.sebastian_daschner.jaxrs_analyzer.model.javadoc.MemberParameterTag;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.TypeIdentifier;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.TypeRepresentation;
import com.sebastian_daschner.jaxrs_analyzer.model.rest.TypeRepresentation.ConcreteTypeRepresentationBuilder;
import com.sebastian_daschner.jaxrs_analyzer.utils.Pair;

import org.objectweb.asm.Type;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils.*;
import static com.sebastian_daschner.jaxrs_analyzer.model.Types.COLLECTION;

/**
* Analyzes a class (usually a POJO) for it's properties and methods.
Expand All @@ -51,10 +73,12 @@ class JavaTypeAnalyzer {
* The type representation storage where all analyzed types have to be added. This will be created by the caller.
*/
private final Map<TypeIdentifier, TypeRepresentation> typeRepresentations;
private final Map<String, ClassComment> classComments;
private final Set<String> analyzedTypes;

JavaTypeAnalyzer(final Map<TypeIdentifier, TypeRepresentation> typeRepresentations) {
JavaTypeAnalyzer(final Map<TypeIdentifier, TypeRepresentation> typeRepresentations, final Map<String, ClassComment> classComments) {
this.typeRepresentations = typeRepresentations;
this.classComments = classComments;
analyzedTypes = new HashSet<>();
}

Expand Down Expand Up @@ -93,10 +117,28 @@ private TypeRepresentation analyzeInternal(final TypeIdentifier identifier, fina
if (loadedClass != null && loadedClass.isEnum())
return TypeRepresentation.ofEnum(identifier, Stream.of(loadedClass.getEnumConstants()).map(o -> (Enum<?>) o).map(Enum::name).toArray(String[]::new));

return TypeRepresentation.ofConcrete(identifier, analyzeClass(type, loadedClass));
final Map<String, Pair<String, TypeIdentifier>> classResults = analyzeClass(type, loadedClass);

final Map<String, TypeIdentifier> properties = classResults.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e-> e.getValue().getRight()));
final Map<String, String> propertyDescriptions = classResults.entrySet().stream().filter(e -> e.getValue().getLeft() != null)
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getLeft()));

// build up the concrete type response
final ConcreteTypeRepresentationBuilder builder = TypeRepresentation.ofConcreteBuilder().identifier(identifier).properties(properties);

if (!propertyDescriptions.isEmpty()) {
builder.propertyDescriptions(propertyDescriptions);
}

if (classComments.containsKey(toClassName(type))) {
final ClassComment comment = classComments.get(toClassName(type));
builder.description(comment.getComment());
}

return builder.build();
}

private Map<String, TypeIdentifier> analyzeClass(final String type, final Class<?> clazz) {
private Map<String, Pair<String, TypeIdentifier>> analyzeClass(final String type, final Class<?> clazz) {
if (clazz == null || isJDKType(type))
return Collections.emptyMap();

Expand All @@ -107,20 +149,35 @@ private Map<String, TypeIdentifier> analyzeClass(final String type, final Class<
final List<Field> relevantFields = Stream.of(clazz.getDeclaredFields()).filter(f -> isRelevant(f, value)).collect(Collectors.toList());
final List<Method> relevantGetters = Stream.of(clazz.getDeclaredMethods()).filter(m -> isRelevant(m, value)).collect(Collectors.toList());

final Map<String, TypeIdentifier> properties = new HashMap<>();
final Map<String, Pair<String, TypeIdentifier>> properties = new HashMap<>();

final Stream<Class<?>> allSuperTypes = Stream.concat(Stream.of(clazz.getInterfaces()), Stream.of(clazz.getSuperclass()));
allSuperTypes.filter(Objects::nonNull).map(Type::getDescriptor).map(t -> analyzeClass(t, loadClassFromType(t))).forEach(properties::putAll);

Stream.concat(relevantFields.stream().map(f -> mapField(f, type)), relevantGetters.stream().map(g -> mapGetter(g, type)))
.filter(Objects::nonNull).forEach(p -> {
properties.put(p.getLeft(), TypeIdentifier.ofType(p.getRight()));
properties.put(p.getLeft(), Pair.of(getDescription(type, p.getLeft()), TypeIdentifier.ofType(p.getRight())));
analyze(p.getRight());
});

return properties;
}

private String getDescription(String parentClass, String subFieldName) {
final String className = toClassName(parentClass);
if (classComments.containsKey(className)) {
// look through classComments (obtained from JavaDocAnalyzer, using JavaParser) and see if we have a matching field for this class/type
return classComments.get(className).getFieldComments()
.stream()
.filter(memberParameterTag -> memberParameterTag.getName().equals(subFieldName))
.map(MemberParameterTag::getComment)
.findAny()
.orElse(null);
}

return null;
}

private XmlAccessType getXmlAccessType(final Class<?> clazz) {
Class<?> current = clazz;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.sebastian_daschner.jaxrs_analyzer.analysis.results;

import com.sebastian_daschner.jaxrs_analyzer.analysis.javadoc.JavaDocAnalyzerResults;
import com.sebastian_daschner.jaxrs_analyzer.model.JavaUtils;
import com.sebastian_daschner.jaxrs_analyzer.model.elements.HttpResponse;
import com.sebastian_daschner.jaxrs_analyzer.model.javadoc.ClassComment;
Expand Down Expand Up @@ -51,15 +52,15 @@ public class ResultInterpreter {
*
* @return All REST resources
*/
public Resources interpret(final Set<ClassResult> classResults) {
public Resources interpret(final JavaDocAnalyzerResults javaDocAnalyzerResults) {
resources = new Resources();
resources.setBasePath(PathNormalizer.getApplicationPath(classResults));
resources.setBasePath(PathNormalizer.getApplicationPath(javaDocAnalyzerResults.getClassResults()));

javaTypeAnalyzer = new JavaTypeAnalyzer(resources.getTypeRepresentations());
javaTypeAnalyzer = new JavaTypeAnalyzer(resources.getTypeRepresentations(), javaDocAnalyzerResults.getClassComments());
dynamicTypeAnalyzer = new DynamicTypeAnalyzer(resources.getTypeRepresentations());
stringParameterResolver = new StringParameterResolver(resources.getTypeRepresentations(), javaTypeAnalyzer);

classResults.stream().filter(c -> c.getResourcePath() != null).forEach(this::interpretClassResult);
javaDocAnalyzerResults.getClassResults().stream().filter(c -> c.getResourcePath() != null).forEach(this::interpretClassResult);
resources.consolidateMultiplePaths();

return resources;
Expand Down
Loading