You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/11 22:16:45 UTC
(maven-plugin-tools) 03/10: MPLUGIN-421: Migrate Javadoc extraction to JavaParser
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch maven-plugin-tools-3.x
in repository https://gitbox.apache.org/repos/asf/maven-plugin-tools.git
commit 61bb93664af9ef44b7fc1c8da1dc1dfc7bf07a6c
Author: Vaclav Haisman <vh...@gmail.com>
AuthorDate: Fri Aug 7 22:22:04 2026 +0200
MPLUGIN-421: Migrate Javadoc extraction to JavaParser
(cherry picked from commit 8f9d9e966102eaa196ff9cb18d1a7a158e9f8261)
---
.../JavaAnnotationsMojoDescriptorExtractor.java | 460 ++++++--------
.../extractor/annotations/JavaSourceModel.java | 65 +-
.../converter/JavaClassConverterContext.java | 674 +++++++++++----------
.../converter/JavaClassConverterContextTest.java | 25 +-
4 files changed, 599 insertions(+), 625 deletions(-)
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
index b44f3c37..34357c85 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
@@ -23,9 +23,9 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -42,14 +42,16 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
-import com.thoughtworks.qdox.JavaProjectBuilder;
-import com.thoughtworks.qdox.library.SortedClassLibraryBuilder;
-import com.thoughtworks.qdox.model.DocletTag;
-import com.thoughtworks.qdox.model.JavaAnnotatedElement;
-import com.thoughtworks.qdox.model.JavaClass;
-import com.thoughtworks.qdox.model.JavaField;
-import com.thoughtworks.qdox.model.JavaMember;
-import com.thoughtworks.qdox.model.JavaMethod;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.body.MethodDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc;
+import com.github.javaparser.ast.type.ClassOrInterfaceType;
+import com.github.javaparser.javadoc.Javadoc;
+import com.github.javaparser.javadoc.JavadocBlockTag;
+import com.github.javaparser.resolution.UnsolvedSymbolException;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.apache.maven.plugin.descriptor.InvalidParameterException;
@@ -206,9 +208,6 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
request.setRequiredJavaVersion(requiredJavaVersion);
}
}
- JavaProjectBuilder builder = scanJavadoc(request, mojoAnnotatedClasses.values());
- Map<String, JavaClass> javaClassesMap = discoverClasses(builder);
-
final JavadocLinkGenerator linkGenerator;
if (request.getInternalJavadocBaseUrl() != null
|| (request.getExternalJavadocBaseUrls() != null
@@ -222,7 +221,12 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
linkGenerator = null;
}
- populateDataFromJavadoc(builder, mojoAnnotatedClasses, javaClassesMap, linkGenerator);
+ try (JavaSourceModel sourceModel = scanJavadoc(request, mojoAnnotatedClasses.values())) {
+ Map<String, TypeDeclaration<?>> javaClassesMap = discoverClasses(sourceModel);
+ populateDataFromJavadoc(sourceModel, mojoAnnotatedClasses, javaClassesMap, linkGenerator);
+ } catch (IOException e) {
+ throw new ExtractionException("Could not parse Java sources: " + e.getMessage(), e);
+ }
return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor());
}
@@ -242,9 +246,8 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
return result;
}
- private JavaProjectBuilder scanJavadoc(
- PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
- throws ExtractionException {
+ private JavaSourceModel scanJavadoc(PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
+ throws ExtractionException, IOException {
// found artifact from reactors to scan sources
// we currently only scan sources from reactors
List<MavenProject> mavenProjects = new ArrayList<>();
@@ -252,9 +255,10 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
// if we need to scan sources from external artifacts
Set<Artifact> externalArtifacts = new HashSet<>();
- JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder());
- builder.setEncoding(request.getEncoding());
- extendJavaProjectBuilder(request, builder, request.getProject());
+ Charset encoding =
+ request.getEncoding() == null ? StandardCharsets.UTF_8 : Charset.forName(request.getEncoding());
+ JavaSourceModel sourceModel = new JavaSourceModel(encoding);
+ extendJavaSourceModel(request, sourceModel, request.getProject());
for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) {
if (Objects.equals(
@@ -282,17 +286,18 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
for (Artifact artifact : externalArtifacts) {
// parameter for test-sources too ?? olamy I need that for it test only
if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) {
- extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "test-sources");
+ extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "test-sources");
} else {
- extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "sources");
+ extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "sources");
}
}
for (MavenProject mavenProject : mavenProjects) {
- extendJavaProjectBuilder(request, builder, mavenProject);
+ extendJavaSourceModel(request, sourceModel, mavenProject);
}
- return builder;
+ sourceModel.parse();
+ return sourceModel;
}
private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) {
@@ -303,281 +308,191 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
* from sources scan to get @since and @deprecated and description of classes and fields.
*/
protected void populateDataFromJavadoc(
- JavaProjectBuilder javaProjectBuilder,
+ JavaSourceModel sourceModel,
Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
- Map<String, JavaClass> javaClassesMap,
+ Map<String, TypeDeclaration<?>> javaClassesMap,
JavadocLinkGenerator linkGenerator) {
-
for (Map.Entry<String, MojoAnnotatedClass> entry : mojoAnnotatedClasses.entrySet()) {
- JavaClass javaClass = javaClassesMap.get(entry.getKey());
+ TypeDeclaration<?> javaClass = javaClassesMap.get(entry.getKey());
if (javaClass == null) {
continue;
}
- // populate class-level content
- MojoAnnotationContent mojoAnnotationContent = entry.getValue().getMojo();
- if (mojoAnnotationContent != null) {
+ MojoAnnotationContent mojo = entry.getValue().getMojo();
+ if (mojo != null) {
JavaClassConverterContext context = new JavaClassConverterContext(
- javaClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, javaClass.getLineNumber());
- mojoAnnotationContent.setDescription(getDescriptionFromElement(javaClass, context));
-
- DocletTag since = findInClassHierarchy(javaClass, "since");
- if (since != null) {
- mojoAnnotationContent.setSince(getRawValueFromTaglet(since, context));
- }
-
- DocletTag deprecated = findInClassHierarchy(javaClass, "deprecated");
- if (deprecated != null) {
- mojoAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
- }
+ javaClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber(javaClass));
+ mojo.setDescription(getDescriptionFromElement(javaClass, context));
+ findInClassHierarchy(sourceModel, javaClass, "since")
+ .ifPresent(tag -> mojo.setSince(getRawValueFromTaglet(tag, context)));
+ findInClassHierarchy(sourceModel, javaClass, "deprecated")
+ .ifPresent(tag -> mojo.setDeprecated(getRawValueFromTaglet(tag, context)));
}
- Map<String, JavaAnnotatedElement> fieldsMap = extractFieldsAnnotations(javaClass, javaClassesMap);
- Map<String, JavaAnnotatedElement> methodsMap = extractMethodsAnnotations(javaClass, javaClassesMap);
-
- // populate parameters
+ Map<String, SourceMember> fields = extractFields(sourceModel, javaClass);
+ Map<String, SourceMember> methods = extractMethods(sourceModel, javaClass);
Map<String, ParameterAnnotationContent> parameters =
- getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses);
- parameters = new TreeMap<>(parameters);
+ new TreeMap<>(getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses));
for (Map.Entry<String, ParameterAnnotationContent> parameter : parameters.entrySet()) {
- JavaAnnotatedElement element;
- if (parameter.getValue().isAnnotationOnMethod()) {
- element = methodsMap.get(parameter.getKey());
- } else {
- element = fieldsMap.get(parameter.getKey());
- }
-
- if (element == null) {
- continue;
- }
-
- JavaClassConverterContext context = new JavaClassConverterContext(
- javaClass, ((JavaMember) element).getDeclaringClass(),
- javaProjectBuilder, mojoAnnotatedClasses,
- linkGenerator, element.getLineNumber());
- ParameterAnnotationContent parameterAnnotationContent = parameter.getValue();
- parameterAnnotationContent.setDescription(getDescriptionFromElement(element, context));
-
- DocletTag deprecated = element.getTagByName("deprecated");
- if (deprecated != null) {
- parameterAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
- }
-
- DocletTag since = element.getTagByName("since");
- if (since != null) {
- parameterAnnotationContent.setSince(getRawValueFromTaglet(since, context));
+ SourceMember member = parameter.getValue().isAnnotationOnMethod()
+ ? methods.get(parameter.getKey())
+ : fields.get(parameter.getKey());
+ if (member != null) {
+ populateMemberJavadoc(
+ javaClass, member, parameter.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator);
}
}
- // populate components
- Map<String, ComponentAnnotationContent> components =
- entry.getValue().getComponents();
- for (Map.Entry<String, ComponentAnnotationContent> component : components.entrySet()) {
- JavaAnnotatedElement element = fieldsMap.get(component.getKey());
- if (element == null) {
- continue;
- }
-
- JavaClassConverterContext context = new JavaClassConverterContext(
- javaClass, ((JavaMember) element).getDeclaringClass(),
- javaProjectBuilder, mojoAnnotatedClasses,
- linkGenerator, javaClass.getLineNumber());
- ComponentAnnotationContent componentAnnotationContent = component.getValue();
- componentAnnotationContent.setDescription(getDescriptionFromElement(element, context));
-
- DocletTag deprecated = element.getTagByName("deprecated");
- if (deprecated != null) {
- componentAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
- }
-
- DocletTag since = element.getTagByName("since");
- if (since != null) {
- componentAnnotationContent.setSince(getRawValueFromTaglet(since, context));
+ for (Map.Entry<String, ComponentAnnotationContent> component :
+ entry.getValue().getComponents().entrySet()) {
+ SourceMember member = fields.get(component.getKey());
+ if (member != null) {
+ populateMemberJavadoc(
+ javaClass, member, component.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator);
}
}
}
}
- /**
- * Returns the XHTML description from the given element.
- * This may refer to either goal, parameter or component.
- * @param element the element for which to generate the description
- * @param context the context with which to call the converter
- * @return the generated description
- */
- String getDescriptionFromElement(JavaAnnotatedElement element, JavaClassConverterContext context) {
+ private void populateMemberJavadoc(
+ TypeDeclaration<?> mojoClass,
+ SourceMember member,
+ Object annotation,
+ JavaSourceModel sourceModel,
+ Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
+ JavadocLinkGenerator linkGenerator) {
+ JavaClassConverterContext context = new JavaClassConverterContext(
+ mojoClass,
+ member.declaringClass,
+ member.element,
+ sourceModel,
+ mojoAnnotatedClasses,
+ linkGenerator,
+ lineNumber(member.element));
+ String description = getDescriptionFromElement(member.javadocElement, context);
+ Optional<JavadocBlockTag> deprecated = getTag(member.javadocElement, "deprecated");
+ Optional<JavadocBlockTag> since = getTag(member.javadocElement, "since");
+ if (annotation instanceof ParameterAnnotationContent) {
+ ParameterAnnotationContent parameter = (ParameterAnnotationContent) annotation;
+ parameter.setDescription(description);
+ deprecated.ifPresent(tag -> parameter.setDeprecated(getRawValueFromTaglet(tag, context)));
+ since.ifPresent(tag -> parameter.setSince(getRawValueFromTaglet(tag, context)));
+ } else {
+ ComponentAnnotationContent component = (ComponentAnnotationContent) annotation;
+ component.setDescription(description);
+ deprecated.ifPresent(tag -> component.setDeprecated(getRawValueFromTaglet(tag, context)));
+ since.ifPresent(tag -> component.setSince(getRawValueFromTaglet(tag, context)));
+ }
+ }
- String comment = element.getComment();
- if (comment == null) {
+ String getDescriptionFromElement(NodeWithJavadoc<?> element, JavaClassConverterContext context) {
+ Optional<Javadoc> javadoc = element.getJavadoc();
+ if (!javadoc.isPresent()) {
return null;
}
- StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(comment, context));
- for (DocletTag docletTag : element.getTags()) {
- // also consider see block tags
- if ("see".equals(docletTag.getName())) {
- description.append(javadocBlockTagsToHtmlConverter.convert(docletTag, context));
- }
- }
+ StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(
+ javadoc.get().getDescription().toText(), context));
+ javadoc.get().getBlockTags().stream()
+ .filter(tag -> "see".equals(tag.getTagName()))
+ .forEach(tag -> description.append(javadocBlockTagsToHtmlConverter.convert(
+ tag.getTagName(), tag.getContent().toText(), context)));
return description.toString();
}
- String getRawValueFromTaglet(DocletTag docletTag, ConverterContext context) {
- // just resolve inline tags and convert to XHTML
- return javadocInlineTagsToHtmlConverter.convert(docletTag.getValue(), context);
+ String getRawValueFromTaglet(JavadocBlockTag tag, ConverterContext context) {
+ return javadocInlineTagsToHtmlConverter.convert(tag.getContent().toText(), context);
}
- /**
- * @param javaClass not null
- * @param tagName not null
- * @return docletTag instance
- */
- private DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) {
- try {
- DocletTag tag = javaClass.getTagByName(tagName);
-
- if (tag == null) {
- JavaClass superClass = javaClass.getSuperJavaClass();
-
- if (superClass != null) {
- tag = findInClassHierarchy(superClass, tagName);
- }
- }
-
+ private Optional<JavadocBlockTag> findInClassHierarchy(
+ JavaSourceModel sourceModel, TypeDeclaration<?> javaClass, String tagName) {
+ Optional<JavadocBlockTag> tag = getTag(javaClass, tagName);
+ if (tag.isPresent()) {
return tag;
- } catch (NoClassDefFoundError e) {
- if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
- return null;
- }
- String str;
- try {
- str = javaClass.getFullyQualifiedName();
- } catch (Throwable t) {
- str = javaClass.getValue();
- }
- LOGGER.warn("Failed extracting tag '" + tagName + "' from class " + str);
- throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
+ return getSuperSourceClass(sourceModel, javaClass)
+ .flatMap(parent -> findInClassHierarchy(sourceModel, parent, tagName));
}
- /**
- * extract fields that are either parameters or components.
- *
- * @param javaClass not null
- * @return map with Mojo parameters names as keys
- */
- private Map<String, JavaAnnotatedElement> extractFieldsAnnotations(
- JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
- try {
- Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();
-
- // we have to add the parent fields first, so that they will be overwritten by the local fields if
- // that actually happens...
- JavaClass superClass = javaClass.getSuperJavaClass();
-
- if (superClass != null) {
- if (!superClass.getFields().isEmpty()) {
- rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
- }
- // maybe sources comes from scan of sources artifact
- superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
- if (superClass != null && !superClass.getFields().isEmpty()) {
- rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
- }
- } else {
-
- rawParams = new TreeMap<>();
- }
-
- for (JavaField field : javaClass.getFields()) {
- rawParams.put(field.getName(), field);
- }
+ private static Optional<JavadocBlockTag> getTag(NodeWithJavadoc<?> element, String tagName) {
+ return element.getJavadoc()
+ .flatMap(javadoc -> javadoc.getBlockTags().stream()
+ .filter(tag -> tagName.equals(tag.getTagName()))
+ .findFirst());
+ }
- return rawParams;
- } catch (NoClassDefFoundError e) {
- LOGGER.warn("Failed extracting parameters from " + javaClass);
- throw e;
+ private Map<String, SourceMember> extractFields(JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
+ Map<String, SourceMember> result = getSuperSourceClass(sourceModel, javaClass)
+ .map(parent -> extractFields(sourceModel, parent))
+ .orElseGet(TreeMap::new);
+ for (FieldDeclaration field : javaClass.getFields()) {
+ field.getVariables()
+ .forEach(variable ->
+ result.put(variable.getNameAsString(), new SourceMember(javaClass, field, field)));
}
+ return result;
}
- /**
- * extract methods that are parameters.
- *
- * @param javaClass not null
- * @return map with Mojo parameters names as keys
- */
- private Map<String, JavaAnnotatedElement> extractMethodsAnnotations(
- JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
- try {
- Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();
-
- // we have to add the parent methods first, so that they will be overwritten by the local methods if
- // that actually happens...
- JavaClass superClass = javaClass.getSuperJavaClass();
-
- if (superClass != null) {
- if (!superClass.getMethods().isEmpty()) {
- rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
- }
- // maybe sources comes from scan of sources artifact
- superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
- if (superClass != null && !superClass.getMethods().isEmpty()) {
- rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
- }
- } else {
-
- rawParams = new TreeMap<>();
- }
-
- for (JavaMethod method : javaClass.getMethods()) {
- if (isPublicSetterMethod(method)) {
- rawParams.put(
- StringUtils.lowercaseFirstLetter(method.getName().substring(3)), method);
- }
- }
-
- return rawParams;
- } catch (NoClassDefFoundError e) {
- if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
- return new TreeMap<>();
- }
- String str;
- try {
- str = javaClass.getFullyQualifiedName();
- } catch (Throwable t) {
- str = javaClass.getValue();
+ private Map<String, SourceMember> extractMethods(JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
+ Map<String, SourceMember> result = getSuperSourceClass(sourceModel, javaClass)
+ .map(parent -> extractMethods(sourceModel, parent))
+ .orElseGet(TreeMap::new);
+ for (MethodDeclaration method : javaClass.getMethods()) {
+ if (isPublicSetterMethod(method)) {
+ result.put(
+ StringUtils.lowercaseFirstLetter(
+ method.getNameAsString().substring(3)),
+ new SourceMember(javaClass, method, method));
}
- LOGGER.warn("Failed extracting methods from " + str);
- throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
+ return result;
}
- private boolean isPublicSetterMethod(JavaMethod method) {
+ private static boolean isPublicSetterMethod(MethodDeclaration method) {
return method.isPublic()
&& !method.isStatic()
- && method.getName().length() > 3
- && (method.getName().startsWith("add") || method.getName().startsWith("set"))
- && "void".equals(method.getReturnType().getValue())
+ && method.getNameAsString().length() > 3
+ && (method.getNameAsString().startsWith("add")
+ || method.getNameAsString().startsWith("set"))
+ && method.getType().isVoidType()
&& method.getParameters().size() == 1;
}
- protected Map<String, JavaClass> discoverClasses(JavaProjectBuilder builder) {
- Collection<JavaClass> javaClasses = builder.getClasses();
-
- if (javaClasses == null || javaClasses.size() < 1) {
- return Collections.emptyMap();
+ private Optional<TypeDeclaration<?>> getSuperSourceClass(
+ JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
+ if (!(javaClass instanceof ClassOrInterfaceDeclaration)
+ || ((ClassOrInterfaceDeclaration) javaClass).isInterface()) {
+ return Optional.empty();
}
-
- Map<String, JavaClass> javaClassMap = new HashMap<>(javaClasses.size());
-
- for (JavaClass javaClass : javaClasses) {
- javaClassMap.put(javaClass.getFullyQualifiedName(), javaClass);
+ Optional<ClassOrInterfaceType> superClass = ((ClassOrInterfaceDeclaration) javaClass)
+ .getExtendedTypes().stream().findFirst();
+ if (!superClass.isPresent()) {
+ return Optional.empty();
+ }
+ try {
+ return superClass
+ .get()
+ .resolve()
+ .asReferenceType()
+ .getTypeDeclaration()
+ .flatMap(sourceModel::getType);
+ } catch (UnsolvedSymbolException e) {
+ // Sources artifacts need not contain the sources of every superclass. In that case there is no
+ // source Javadoc to inherit, just as when no sources artifact is available at all.
+ LOGGER.debug("Could not resolve source superclass {}", superClass.get(), e);
+ return Optional.empty();
}
+ }
- return javaClassMap;
+ protected Map<String, TypeDeclaration<?>> discoverClasses(JavaSourceModel sourceModel) {
+ Map<String, TypeDeclaration<?>> result = new HashMap<>();
+ for (TypeDeclaration<?> type : sourceModel.getTypes()) {
+ type.getFullyQualifiedName().ifPresent(name -> result.put(name, type));
+ }
+ return result;
}
- protected void extendJavaProjectBuilderWithSourcesJar(
- JavaProjectBuilder builder, Artifact artifact, PluginToolsRequest request, String classifier)
- throws ExtractionException {
+ protected void extendJavaSourceModelWithSourcesJar(
+ JavaSourceModel sourceModel, Artifact artifact, PluginToolsRequest request, String classifier)
+ throws ExtractionException, IOException {
try {
org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact(
artifact.getGroupId(),
@@ -621,17 +536,17 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
unArchiver.setDestDirectory(extractDirectory);
unArchiver.extract();
- extendJavaProjectBuilder(builder, Arrays.asList(extractDirectory), request.getDependencies());
+ extendJavaSourceModel(sourceModel, Arrays.asList(extractDirectory), request.getDependencies());
} else if (sourcesArtifact.getFile().isDirectory()) {
- extendJavaProjectBuilder(builder, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
+ extendJavaSourceModel(sourceModel, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
}
} catch (ArchiverException | NoSuchArchiverException e) {
throw new ExtractionException(e.getMessage(), e);
}
}
- private void extendJavaProjectBuilder(
- PluginToolsRequest request, JavaProjectBuilder builder, final MavenProject project) {
+ private void extendJavaSourceModel(
+ PluginToolsRequest request, JavaSourceModel sourceModel, final MavenProject project) throws IOException {
List<File> sources = new ArrayList<>();
for (String source : project.getCompileSourceRoots()) {
@@ -651,25 +566,32 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
sources.add(generatedPlugin);
}
- extendJavaProjectBuilder(builder, sources, project.getArtifacts());
+ extendJavaSourceModel(sourceModel, sources, project.getArtifacts());
}
- private void extendJavaProjectBuilder(
- JavaProjectBuilder builder, List<File> sourceDirectories, Set<Artifact> artifacts) {
-
- // Build isolated Classloader with only the artifacts of the project (none of this plugin)
- List<URL> urls = new ArrayList<>(artifacts.size());
+ private void extendJavaSourceModel(
+ JavaSourceModel sourceModel, List<File> sourceDirectories, Set<Artifact> artifacts) throws IOException {
for (Artifact artifact : artifacts) {
- try {
- urls.add(artifact.getFile().toURI().toURL());
- } catch (MalformedURLException e) {
- // noop
- }
+ sourceModel.addClassPathEntry(artifact.getFile());
}
- builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()));
-
for (File source : sourceDirectories) {
- builder.addSourceTree(source);
+ sourceModel.addSourceDirectory(source);
+ }
+ }
+
+ private static int lineNumber(Node node) {
+ return node.getBegin().map(position -> position.line).orElse(0);
+ }
+
+ private static final class SourceMember {
+ private final TypeDeclaration<?> declaringClass;
+ private final Node element;
+ private final NodeWithJavadoc<?> javadocElement;
+
+ private SourceMember(TypeDeclaration<?> declaringClass, Node element, NodeWithJavadoc<?> javadocElement) {
+ this.declaringClass = declaringClass;
+ this.element = element;
+ this.javadocElement = javadocElement;
}
}
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java
index 1d6562c7..ac1e8a49 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaSourceModel.java
@@ -56,7 +56,7 @@ import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeS
import com.github.javaparser.utils.SourceRoot;
/** Source declarations and type resolution used while extracting Javadocs. */
-final class JavaSourceModel implements Closeable {
+public final class JavaSourceModel implements Closeable {
private final Charset encoding;
private final Set<Path> sourceDirectories = new LinkedHashSet<>();
@@ -64,29 +64,30 @@ final class JavaSourceModel implements Closeable {
private final Map<String, TypeDeclaration<?>> types = new LinkedHashMap<>();
private final Map<String, String> modulesByExportedPackage = new LinkedHashMap<>();
private final Set<String> packages = new LinkedHashSet<>();
+ private final Set<String> internalPackages = new LinkedHashSet<>();
private ParserConfiguration parserConfiguration;
private CombinedTypeSolver typeSolver;
private URLClassLoader classPathLoader;
private boolean parsed;
- JavaSourceModel(Charset encoding) {
+ public JavaSourceModel(Charset encoding) {
this.encoding = encoding;
}
- void addSourceDirectory(File directory) throws IOException {
+ public void addSourceDirectory(File directory) throws IOException {
if (directory != null && directory.isDirectory()) {
sourceDirectories.add(directory.toPath().toRealPath());
}
}
- void addClassPathEntry(File entry) throws IOException {
+ public void addClassPathEntry(File entry) throws IOException {
if (entry != null && entry.exists()) {
classPathEntries.add(entry.toPath().toRealPath());
}
}
- void parse() throws IOException {
+ public void parse() throws IOException {
if (parsed) {
return;
}
@@ -132,8 +133,9 @@ final class JavaSourceModel implements Closeable {
.collect(Collectors.joining(System.lineSeparator()));
throw new IOException("Unable to parse " + path + System.lineSeparator() + problems);
}
- CompilationUnit unit = result.getResult().orElseThrow(
- () -> new IOException("Parser returned no compilation unit for " + result.getSourcePath()));
+ CompilationUnit unit = result.getResult()
+ .orElseThrow(() ->
+ new IOException("Parser returned no compilation unit for " + result.getSourcePath()));
index(unit);
}
}
@@ -156,57 +158,72 @@ final class JavaSourceModel implements Closeable {
private void index(CompilationUnit unit) {
unit.getPackageDeclaration()
.map(declaration -> declaration.getName().asString())
- .ifPresent(packages::add);
+ .ifPresent(name -> {
+ packages.add(name);
+ internalPackages.add(name);
+ });
for (TypeDeclaration<?> type : unit.findAll(TypeDeclaration.class)) {
type.getFullyQualifiedName().ifPresent(name -> types.putIfAbsent(name, type));
}
- unit.getModule().ifPresent(module -> module.getDirectives().stream()
- .filter(ModuleExportsDirective.class::isInstance)
- .map(ModuleExportsDirective.class::cast)
- .forEach(exports -> modulesByExportedPackage.putIfAbsent(
- exports.getName().asString(), module.getName().asString())));
+ unit.getModule()
+ .ifPresent(module -> module.getDirectives().stream()
+ .filter(ModuleExportsDirective.class::isInstance)
+ .map(ModuleExportsDirective.class::cast)
+ .forEach(exports -> modulesByExportedPackage.putIfAbsent(
+ exports.getName().asString(), module.getName().asString())));
}
- Collection<TypeDeclaration<?>> getTypes() {
+ public Collection<TypeDeclaration<?>> getTypes() {
return Collections.unmodifiableCollection(types.values());
}
- Optional<TypeDeclaration<?>> getType(String fullyQualifiedName) {
+ public Optional<TypeDeclaration<?>> getType(String fullyQualifiedName) {
return Optional.ofNullable(types.get(fullyQualifiedName));
}
- Optional<TypeDeclaration<?>> getType(ResolvedReferenceTypeDeclaration declaration) {
+ public Optional<TypeDeclaration<?>> getType(ResolvedReferenceTypeDeclaration declaration) {
return getType(declaration.getQualifiedName());
}
- Optional<String> getModuleName(String packageName) {
+ public Optional<String> getModuleName(String packageName) {
return Optional.ofNullable(modulesByExportedPackage.get(packageName));
}
- boolean hasPackage(String packageName) {
+ public boolean hasPackage(String packageName) {
return packages.contains(packageName) || Package.getPackage(packageName) != null;
}
- boolean isInternal(ResolvedReferenceTypeDeclaration declaration) {
+ public boolean isInternal(ResolvedReferenceTypeDeclaration declaration) {
return types.containsKey(declaration.getQualifiedName());
}
- Optional<ResolvedReferenceTypeDeclaration> resolveType(String fullyQualifiedName) {
+ public boolean isInternalPackage(String packageName) {
+ return internalPackages.contains(packageName);
+ }
+
+ public Optional<ResolvedReferenceTypeDeclaration> resolveType(String fullyQualifiedName) {
ensureParsed();
SymbolReference<ResolvedReferenceTypeDeclaration> reference = typeSolver.tryToSolveType(fullyQualifiedName);
return reference.isSolved() ? Optional.of(reference.getCorrespondingDeclaration()) : Optional.empty();
}
- TypeSolver getTypeSolver() {
+ public TypeSolver getTypeSolver() {
ensureParsed();
return typeSolver;
}
- String getLocation(Node node, int fallbackLine) {
- int line = node.getBegin().map(position -> position.line).orElse(fallbackLine);
+ public String getLocation(Node node, int fallbackLine) {
+ int line = fallbackLine > 0
+ ? fallbackLine
+ : node.getBegin().map(position -> position.line).orElse(0);
return node.findCompilationUnit()
.flatMap(CompilationUnit::getStorage)
- .map(storage -> storage.getPath().toString() + ":" + line)
+ .map(storage -> java.nio.file.Paths.get("")
+ .toAbsolutePath()
+ .toUri()
+ .relativize(storage.getPath().toUri())
+ .toString()
+ + ":" + line)
.orElse("unknown:" + line);
}
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java
index b039d5b4..bc6bf04f 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContext.java
@@ -20,8 +20,6 @@ package org.apache.maven.tools.plugin.extractor.annotations.converter;
import java.net.URI;
import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -30,121 +28,101 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
-import com.thoughtworks.qdox.JavaProjectBuilder;
-import com.thoughtworks.qdox.builder.TypeAssembler;
-import com.thoughtworks.qdox.library.ClassNameLibrary;
-import com.thoughtworks.qdox.model.JavaClass;
-import com.thoughtworks.qdox.model.JavaField;
-import com.thoughtworks.qdox.model.JavaModule;
-import com.thoughtworks.qdox.model.JavaPackage;
-import com.thoughtworks.qdox.model.JavaType;
-import com.thoughtworks.qdox.parser.structs.TypeDef;
-import com.thoughtworks.qdox.type.TypeResolver;
+import com.github.javaparser.ast.ImportDeclaration;
+import com.github.javaparser.ast.Node;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration;
+import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
+import com.github.javaparser.resolution.types.ResolvedReferenceType;
+import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel;
import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
import org.apache.maven.tools.plugin.javadoc.JavadocReference;
-/** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */
+/** {@link ConverterContext} backed by JavaParser source declarations and symbol resolution. */
public class JavaClassConverterContext implements ConverterContext {
- final JavaClass mojoClass; // this is the mojo's class
+ /** The Mojo class whose documentation is being generated. */
+ private final TypeDeclaration<?> mojoClass;
- final JavaClass declaringClass; // this may be a super class of the mojo's class
+ /** The class declaring the converted Javadoc, possibly a superclass of the Mojo class. */
+ private final TypeDeclaration<?> declaringClass;
- final JavaProjectBuilder javaProjectBuilder;
+ private final Node locationNode;
+ private final JavaSourceModel sourceModel;
+ private final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
- final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
+ /** The link generator, or {@code null} when no Javadoc site is configured. */
+ private final JavadocLinkGenerator linkGenerator;
- final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured
-
- final int lineNumber;
-
- final Optional<JavaModule> javaModule;
-
- final Map<String, Object> attributes;
+ private final int lineNumber;
+ private final Map<String, Object> attributes = new HashMap<>();
public JavaClassConverterContext(
- JavaClass mojoClass,
- JavaProjectBuilder javaProjectBuilder,
+ TypeDeclaration<?> mojoClass,
+ JavaSourceModel sourceModel,
Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
JavadocLinkGenerator linkGenerator,
int lineNumber) {
- this(mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber);
+ this(mojoClass, mojoClass, mojoClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber);
}
public JavaClassConverterContext(
- JavaClass mojoClass,
- JavaClass declaringClass,
- JavaProjectBuilder javaProjectBuilder,
+ TypeDeclaration<?> mojoClass,
+ TypeDeclaration<?> declaringClass,
+ Node locationNode,
+ JavaSourceModel sourceModel,
Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
JavadocLinkGenerator linkGenerator,
int lineNumber) {
this.mojoClass = mojoClass;
this.declaringClass = declaringClass;
- this.javaProjectBuilder = javaProjectBuilder;
+ this.locationNode = locationNode;
+ this.sourceModel = sourceModel;
this.mojoAnnotatedClasses = mojoAnnotatedClasses;
this.linkGenerator = linkGenerator;
this.lineNumber = lineNumber;
- this.attributes = new HashMap<>();
-
- javaModule = mojoClass.getJavaClassLibrary().getJavaModules().stream()
- .filter(m -> m.getDescriptor().getExports().stream()
- .anyMatch(e -> e.getSource().getName().equals(getPackageName())))
- .findFirst();
}
@Override
public Optional<String> getModuleName() {
- // https://github.com/paul-hammant/qdox/issues/113, module name is not exposed
- return javaModule.map(JavaModule::getName);
+ return sourceModel.getModuleName(getPackageName());
}
@Override
public String getPackageName() {
- return mojoClass.getPackageName();
+ return resolve(mojoClass).getPackageName();
}
@Override
public String getLocation() {
- try {
- URL url = declaringClass.getSource().getURL();
- if (url == null) // url is not always available, just emit FQCN in that case
- {
- return declaringClass.getPackageName() + declaringClass.getSimpleName() + ":" + lineNumber;
- }
- return Paths.get("").toUri().relativize(url.toURI()) + ":" + lineNumber;
- } catch (URISyntaxException e) {
- return declaringClass.getSource().getURL() + ":" + lineNumber;
- }
+ return sourceModel.getLocation(locationNode, lineNumber);
}
- /**
- * @param reference
- * @return true in case either the current context class or any of its super classes are referenced
- */
+ /** Returns whether {@code reference} identifies the Mojo class or one of its ancestors. */
@Override
public boolean isReferencedBy(FullyQualifiedJavadocReference reference) {
- JavaClass javaClassInHierarchy = this.mojoClass;
- while (javaClassInHierarchy != null) {
- if (isClassReferencedByReference(javaClassInHierarchy, reference)) {
- return true;
- }
- // check implemented interfaces
- for (JavaClass implementedInterfaces : javaClassInHierarchy.getInterfaces()) {
- if (isClassReferencedByReference(implementedInterfaces, reference)) {
- return true;
- }
- }
- javaClassInHierarchy = javaClassInHierarchy.getSuperJavaClass();
+ ResolvedReferenceTypeDeclaration declaration = resolve(mojoClass);
+ if (isClassReferencedByReference(declaration, reference)) {
+ return true;
}
- return false;
+ return declaration.getAllAncestors().stream()
+ .map(ResolvedReferenceType::getTypeDeclaration)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .anyMatch(type -> isClassReferencedByReference(type, reference));
}
- private static boolean isClassReferencedByReference(JavaClass javaClass, FullyQualifiedJavadocReference reference) {
- return javaClass.getPackageName().equals(reference.getPackageName().orElse(""))
- && javaClass.getSimpleName().equals(reference.getClassName().orElse(""));
+ private static boolean isClassReferencedByReference(
+ ResolvedReferenceTypeDeclaration declaration, FullyQualifiedJavadocReference reference) {
+ return declaration.getPackageName().equals(reference.getPackageName().orElse(""))
+ && declaration.getClassName().equals(reference.getClassName().orElse(""));
}
@Override
@@ -157,8 +135,8 @@ public class JavaClassConverterContext implements ConverterContext {
try {
if (isReferencedBy(reference)
&& MemberType.FIELD == reference.getMemberType().orElse(null)) {
- // link to current goal's parameters
- return new URI(null, null, reference.getMember().orElse(null)); // just an anchor if same context
+ // Fields in the current Mojo link to parameter anchors on the same page.
+ return new URI(null, null, reference.getMember().orElse(null));
}
Optional<String> fqClassName = reference.getFullyQualifiedClassName();
if (fqClassName.isPresent()) {
@@ -167,7 +145,7 @@ public class JavaClassConverterContext implements ConverterContext {
&& mojoAnnotatedClass.getMojo() != null
&& (!reference.getLabel().isPresent()
|| MemberType.FIELD == reference.getMemberType().orElse(null))) {
- // link to other mojo (only for fields = parameters or without member)
+ // Fields and whole-class references to another Mojo link to that Mojo's page.
return new URI(
null,
"./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
@@ -175,7 +153,7 @@ public class JavaClassConverterContext implements ConverterContext {
}
}
} catch (URISyntaxException e) {
- throw new IllegalStateException("Error constructing a valid URL", e); // should not happen
+ throw new IllegalStateException("Error constructing a valid URL", e);
}
if (linkGenerator == null) {
throw new IllegalStateException("No Javadoc Sites given to create URLs to");
@@ -185,296 +163,352 @@ public class JavaClassConverterContext implements ConverterContext {
@Override
public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) {
- Optional<FullyQualifiedJavadocReference> resolvedName;
- // is it already fully qualified?
+ Optional<FullyQualifiedJavadocReference> resolved;
+ // First try the reference exactly as written, which handles fully qualified names.
if (reference.getPackageNameClassName().isPresent()) {
- resolvedName = resolveMember(
- reference.getPackageNameClassName().get(), reference.getMember(), reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
+ String name = reference.getPackageNameClassName().get();
+ resolved = resolveNamedReference(name, reference.getMember(), reference.getLabel());
+ if (resolved.isPresent()) {
+ return resolved.get();
}
}
- // is it a member only?
+
+ // Search member-only references in the current class or interface first, then its ancestors.
if (reference.getMember().isPresent()
&& !reference.getPackageNameClassName().isPresent()) {
- // search order for not fully qualified names:
- // 1. The current class or interface (only for members)
- resolvedName = resolveMember(declaringClass, reference.getMember(), reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
- // 2. Any enclosing classes and interfaces searching the closest first (only members)
- for (JavaClass nestedClass : declaringClass.getNestedClasses()) {
- resolvedName = resolveMember(nestedClass, reference.getMember(), reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
+ resolved = resolveMember(resolve(declaringClass), reference.getMember(), reference.getLabel(), true);
+ if (resolved.isPresent()) {
+ return resolved.get();
}
- // 3. Any superclasses and superinterfaces, searching the closest first. (only members)
- JavaClass superClass = declaringClass.getSuperJavaClass();
- while (superClass != null) {
- resolvedName = resolveMember(superClass, reference.getMember(), reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
- superClass = superClass.getSuperJavaClass();
- }
- } else {
- String packageNameClassName = reference.getPackageNameClassName().get();
- // 4. The current package
- resolvedName = resolveMember(
- declaringClass.getPackageName() + "." + packageNameClassName,
- reference.getMember(),
- reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
- // 5. Any imported packages, classes, and interfaces, searching in the order of the import statement.
- List<String> importNames = new ArrayList<>();
- importNames.add("java.lang.*"); // default import
- importNames.addAll(declaringClass.getSource().getImports());
- for (String importName : importNames) {
- if (importName.endsWith(".*")) {
- resolvedName = resolveMember(
- importName.replace("*", packageNameClassName), reference.getMember(), reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
- } else {
- if (importName.endsWith(packageNameClassName)) {
- resolvedName = resolveMember(importName, reference.getMember(), reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
- } else {
- // ends with prefix of reference (nested class name)
- int firstDotIndex = packageNameClassName.indexOf(".");
- if (firstDotIndex > 0
- && importName.endsWith(packageNameClassName.substring(0, firstDotIndex))) {
- resolvedName = resolveMember(
- importName,
- packageNameClassName.substring(firstDotIndex + 1),
- reference.getMember(),
- reference.getLabel());
- if (resolvedName.isPresent()) {
- return resolvedName.get();
- }
- }
- }
+ } else if (reference.getPackageNameClassName().isPresent()) {
+ // Resolve simple and nested type names using Javadoc's lookup order.
+ String name = reference.getPackageNameClassName().get();
+ for (String candidate : classNameCandidates(name)) {
+ resolved = resolveNamedReference(candidate, reference.getMember(), reference.getLabel());
+ if (resolved.isPresent()) {
+ return resolved.get();
}
}
}
throw new IllegalArgumentException("Could not resolve javadoc reference " + reference);
}
- @Override
- public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
- String fqcn = reference
- .getFullyQualifiedClassName()
- .orElseThrow(() ->
- new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
- String fieldName = reference
- .getMember()
- .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
- JavaClass javaClass = javaProjectBuilder.getClassByName(fqcn);
- JavaField javaField = javaClass.getFieldByName(fieldName);
- if (javaField == null) {
- throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
+ private Optional<FullyQualifiedJavadocReference> resolveNamedReference(
+ String name, Optional<String> member, Optional<String> label) {
+ Optional<ResolvedReferenceTypeDeclaration> type = sourceModel.resolveType(name);
+ if (type.isPresent()) {
+ return resolveMember(type.get(), member, label, true);
}
- if (!javaField.isStatic()) {
- throw new IllegalArgumentException("Field with name " + fieldName + " in class " + fqcn + " is not static");
+ // Package references cannot contain members.
+ if (!member.isPresent() && sourceModel.hasPackage(name)) {
+ return Optional.of(new FullyQualifiedJavadocReference(name, label, !sourceModel.isInternalPackage(name)));
}
- return javaField.getInitializationExpression();
+ return Optional.empty();
}
- @Override
- public URI getInternalJavadocSiteBaseUrl() {
- return linkGenerator.getInternalJavadocSiteBaseUrl();
- }
-
- private Optional<FullyQualifiedJavadocReference> resolveMember(
- String fullyQualifiedPackageNameClassName, Optional<String> member, Optional<String> label) {
- return resolveMember(fullyQualifiedPackageNameClassName, "", member, label);
+ private List<String> classNameCandidates(String name) {
+ List<String> candidates = new ArrayList<>();
+ ResolvedReferenceTypeDeclaration declaration = resolve(declaringClass);
+ // Search order: current package, implicit java.lang import, then explicit imports in declaration order.
+ candidates.add(declaration.getPackageName() + "." + name);
+ candidates.add("java.lang." + name);
+ List<ImportDeclaration> imports = declaringClass
+ .findCompilationUnit()
+ .<List<ImportDeclaration>>map(unit -> new ArrayList<>(unit.getImports()))
+ .orElseGet(Collections::emptyList);
+ for (ImportDeclaration importDeclaration : imports) {
+ if (importDeclaration.isStatic()) {
+ continue;
+ }
+ String importName = importDeclaration.getNameAsString();
+ if (importDeclaration.isAsterisk()) {
+ candidates.add(importName + "." + name);
+ } else if (name.equals(simpleName(importName))) {
+ candidates.add(importName);
+ } else if (name.startsWith(simpleName(importName) + ".")) {
+ // An imported outer type may prefix a nested-type reference.
+ candidates.add(
+ importName + name.substring(simpleName(importName).length()));
+ }
+ }
+ return candidates;
}
private Optional<FullyQualifiedJavadocReference> resolveMember(
- String fullyQualifiedPackageNameClassName,
- String nestedClassName,
+ ResolvedReferenceTypeDeclaration type,
Optional<String> member,
- Optional<String> label) {
- JavaClass javaClass = javaProjectBuilder.getClassByName(fullyQualifiedPackageNameClassName);
- if (!isClassFound(javaClass)) {
- JavaPackage javaPackage = javaProjectBuilder.getPackageByName(fullyQualifiedPackageNameClassName);
- if (javaPackage == null || !nestedClassName.isEmpty()) {
- // is it a nested class?
- int lastIndexOfDot = fullyQualifiedPackageNameClassName.lastIndexOf('.');
- if (lastIndexOfDot > 0) {
- String newNestedClassName = nestedClassName;
- if (!newNestedClassName.isEmpty()) {
- newNestedClassName += '.';
- }
- newNestedClassName += fullyQualifiedPackageNameClassName.substring(lastIndexOfDot + 1);
- return resolveMember(
- fullyQualifiedPackageNameClassName.substring(0, lastIndexOfDot),
- newNestedClassName,
- member,
- label);
- }
- return Optional.empty();
- } else {
- // reference to java package never has a member
- return Optional.of(
- new FullyQualifiedJavadocReference(javaPackage.getName(), label, isExternal(javaPackage)));
- }
- } else {
- if (!nestedClassName.isEmpty()) {
- javaClass = javaClass.getNestedClassByName(nestedClassName);
- if (javaClass == null) {
- return Optional.empty();
- }
- }
+ Optional<String> label,
+ boolean includeAncestors) {
+ if (!member.isPresent()) {
+ return Optional.of(toReference(type, Optional.empty(), Optional.empty(), label));
+ }
+
+ String memberText = member.get();
+ // Resolve ambiguous member text as a field, method, then constructor.
+ Optional<ResolvedFieldDeclaration> field = findField(type, memberText, includeAncestors);
+ if (field.isPresent()) {
+ return Optional.of(toReference(
+ field.get().declaringType().asReferenceType(), member, Optional.of(MemberType.FIELD), label));
+ }
- return resolveMember(javaClass, member, label);
+ String methodName = methodName(memberText);
+ Optional<List<String>> parameterTypes = parameterTypes(memberText);
+ Optional<ResolvedMethodDeclaration> method = findMethod(type, methodName, parameterTypes, includeAncestors);
+ if (method.isPresent()) {
+ return Optional.of(toReference(
+ method.get().declaringType(),
+ Optional.of(canonicalMember(method.get())),
+ Optional.of(MemberType.METHOD),
+ label));
+ }
+
+ if (methodName.equals(type.getName())) {
+ Optional<ResolvedConstructorDeclaration> constructor = findConstructor(type, parameterTypes);
+ if (constructor.isPresent()) {
+ return Optional.of(toReference(
+ type,
+ Optional.of(canonicalMember(constructor.get())),
+ Optional.of(MemberType.CONSTRUCTOR),
+ label));
+ }
}
+ return Optional.empty();
}
- private boolean isExternal(JavaClass javaClass) {
- return isExternal(javaClass.getPackage());
+ private Optional<ResolvedFieldDeclaration> findField(
+ ResolvedReferenceTypeDeclaration type, String name, boolean includeAncestors) {
+ List<ResolvedFieldDeclaration> fields = includeAncestors ? type.getAllFields() : type.getDeclaredFields();
+ return fields.stream().filter(field -> field.getName().equals(name)).findFirst();
}
- private boolean isExternal(JavaPackage javaPackage) {
- return !javaPackage.getJavaClassLibrary().equals(mojoClass.getJavaClassLibrary());
+ private Optional<ResolvedMethodDeclaration> findMethod(
+ ResolvedReferenceTypeDeclaration type,
+ String name,
+ Optional<List<String>> parameterTypes,
+ boolean includeAncestors) {
+ List<ResolvedReferenceTypeDeclaration> hierarchy = new ArrayList<>();
+ hierarchy.add(type);
+ if (includeAncestors) {
+ type.getAllAncestors().stream()
+ .map(ResolvedReferenceType::getTypeDeclaration)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .forEach(hierarchy::add);
+ }
+ for (ResolvedReferenceTypeDeclaration declaration : hierarchy) {
+ Optional<ResolvedMethodDeclaration> method = declaration.getDeclaredMethods().stream()
+ .filter(candidate -> candidate.getName().equals(name))
+ .filter(candidate -> matches(candidate, parameterTypes))
+ .findFirst();
+ if (method.isPresent()) {
+ return method;
+ }
+ }
+ return Optional.empty();
}
- private Optional<FullyQualifiedJavadocReference> resolveMember(
- JavaClass javaClass, Optional<String> member, Optional<String> label) {
- final Optional<MemberType> memberType;
- Optional<String> resolvedMember = member;
- if (member.isPresent()) {
- // member is either field...
- if (javaClass.getFieldByName(member.get()) == null) {
- // ...is method...
- List<JavaType> parameterTypes = getParameterTypes(member.get());
- String methodName = getMethodName(member.get());
- if (javaClass.getMethodBySignature(methodName, parameterTypes) == null) {
- // ...or is constructor
- if ((!methodName.equals(javaClass.getSimpleName()))
- || (javaClass.getConstructor(parameterTypes) == null)) {
- return Optional.empty();
- } else {
- memberType = Optional.of(MemberType.CONSTRUCTOR);
- }
- } else {
- memberType = Optional.of(MemberType.METHOD);
- }
- // reconstruct member with fully qualified names but leaving out the argument names
- StringBuilder memberBuilder = new StringBuilder(methodName);
- memberBuilder.append("(");
- memberBuilder.append(parameterTypes.stream()
- .map(JavaType::getFullyQualifiedName)
- .collect(Collectors.joining(",")));
- memberBuilder.append(")");
- resolvedMember = Optional.of(memberBuilder.toString());
- } else {
- memberType = Optional.of(MemberType.FIELD);
+ private Optional<ResolvedConstructorDeclaration> findConstructor(
+ ResolvedReferenceTypeDeclaration type, Optional<List<String>> parameterTypes) {
+ return type.getConstructors().stream()
+ .filter(candidate -> matches(candidate, parameterTypes))
+ .findFirst();
+ }
+
+ private boolean matches(ResolvedMethodLikeDeclaration declaration, Optional<List<String>> parameterTypes) {
+ if (!parameterTypes.isPresent()) {
+ return true;
+ }
+ if (declaration.getNumberOfParams() != parameterTypes.get().size()) {
+ return false;
+ }
+ for (int index = 0; index < declaration.getNumberOfParams(); index++) {
+ String actual = declaration.getParam(index).getType().erasure().describe();
+ if (!actual.equals(parameterTypes.get().get(index))) {
+ return false;
}
- } else {
- memberType = Optional.empty();
}
- String className = javaClass
- .getCanonicalName()
- .substring(javaClass.getPackageName().length() + 1);
- return Optional.of(new FullyQualifiedJavadocReference(
- javaClass.getPackageName(),
- Optional.of(className),
- resolvedMember,
- memberType,
- label,
- isExternal(javaClass)));
- }
-
- private static boolean isClassFound(JavaClass javaClass) {
- // this is never null due to using the ClassNameLibrary in the builder
- // but every instance of ClassNameLibrary basically means that the class was not found
- return !(javaClass.getJavaClassLibrary() instanceof ClassNameLibrary);
- }
-
- // https://github.com/paul-hammant/qdox/issues/104
- private List<JavaType> getParameterTypes(String member) {
- List<JavaType> parameterTypes = new ArrayList<>();
- // TypeResolver.byClassName() always resolves types as non existing inner class
- TypeResolver typeResolver = TypeResolver.byClassName(
- declaringClass.getPackageName(),
- declaringClass.getJavaClassLibrary(),
- declaringClass.getSource().getImports());
-
- // method parameters are optionally enclosed by parentheses
- int indexOfOpeningParenthesis = member.indexOf('(');
- int indexOfClosingParenthesis = member.indexOf(')');
- final String signatureArguments;
- if (indexOfOpeningParenthesis >= 0
- && indexOfClosingParenthesis > 0
- && indexOfClosingParenthesis > indexOfOpeningParenthesis) {
- signatureArguments = member.substring(indexOfOpeningParenthesis + 1, indexOfClosingParenthesis);
- } else if (indexOfOpeningParenthesis == -1 && indexOfClosingParenthesis >= 0
- || indexOfOpeningParenthesis >= 0 && indexOfOpeningParenthesis == -1) {
+ return true;
+ }
+
+ private Optional<List<String>> parameterTypes(String member) {
+ int opening = member.indexOf('(');
+ int closing = member.lastIndexOf(')');
+ if (opening < 0 && closing < 0) {
+ // Without parentheses, match the first overload found, mirroring javadoc.
+ return Optional.empty();
+ }
+ if (opening < 0 || closing < opening) {
throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member);
- } else {
- // If any method or constructor is entered as a name with no parentheses, such as getValue,
- // and if there is no field with the same name, then the javadoc command still creates a
- // link to the method. If this method is overloaded, then the javadoc command links to the
- // first method its search encounters, which is unspecified
- // (Source: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#JSWOR654)
- return Collections.emptyList();
}
- for (String parameter : signatureArguments.split(",")) {
- // strip off argument name, only type is relevant
- String canonicalParameter = parameter.trim();
- int spaceIndex = canonicalParameter.indexOf(' ');
- final String typeName;
- if (spaceIndex > 0) {
- typeName = canonicalParameter.substring(0, spaceIndex).trim();
- } else {
- typeName = canonicalParameter;
- }
- if (!typeName.isEmpty()) {
- String rawTypeName = getRawTypeName(typeName);
- // already check here for unresolvable types due to https://github.com/paul-hammant/qdox/issues/111
- if (typeResolver.resolveType(rawTypeName) == null) {
- throw new IllegalArgumentException("Found unresolvable method argument type in " + member);
- }
- TypeDef typeDef = new TypeDef(getRawTypeName(typeName));
- int dimensions = getDimensions(typeName);
- JavaType javaType = TypeAssembler.createUnresolved(typeDef, dimensions, typeResolver);
+ String arguments = member.substring(opening + 1, closing).trim();
+ if (arguments.isEmpty()) {
+ return Optional.of(Collections.emptyList());
+ }
+ List<String> result = new ArrayList<>();
+ for (String argument : splitArguments(arguments)) {
+ result.add(resolveParameterType(stripArgumentName(argument.trim())));
+ }
+ return Optional.of(result);
+ }
- parameterTypes.add(javaType);
+ private static List<String> splitArguments(String arguments) {
+ List<String> result = new ArrayList<>();
+ int genericDepth = 0;
+ int start = 0;
+ for (int index = 0; index < arguments.length(); index++) {
+ char ch = arguments.charAt(index);
+ if (ch == '<') {
+ genericDepth++;
+ } else if (ch == '>') {
+ genericDepth--;
+ } else if (ch == ',' && genericDepth == 0) {
+ result.add(arguments.substring(start, index));
+ start = index + 1;
}
}
- return parameterTypes;
+ result.add(arguments.substring(start));
+ return result;
}
- private static int getDimensions(String type) {
- return (int) type.chars().filter(ch -> ch == '[').count();
+ private static String stripArgumentName(String argument) {
+ int genericDepth = 0;
+ for (int index = argument.length() - 1; index >= 0; index--) {
+ char ch = argument.charAt(index);
+ if (ch == '>') {
+ genericDepth++;
+ } else if (ch == '<') {
+ genericDepth--;
+ } else if (Character.isWhitespace(ch) && genericDepth == 0) {
+ return argument.substring(0, index).trim();
+ }
+ }
+ return argument;
}
- private static String getRawTypeName(String typeName) {
- // strip dimensions
- int indexOfOpeningBracket = typeName.indexOf('[');
- if (indexOfOpeningBracket >= 0) {
- return typeName.substring(0, indexOfOpeningBracket);
+ private String resolveParameterType(String typeName) {
+ String normalized = eraseGenerics(typeName.replace("...", "[]"));
+ int dimensions = 0;
+ while (normalized.endsWith("[]")) {
+ dimensions++;
+ normalized = normalized.substring(0, normalized.length() - 2);
+ }
+ String resolved;
+ if (isPrimitive(normalized)) {
+ resolved = normalized;
} else {
- return typeName;
+ List<String> candidates = new ArrayList<>();
+ candidates.add(normalized);
+ candidates.addAll(classNameCandidates(normalized));
+ resolved = candidates.stream()
+ .filter(candidate -> sourceModel.resolveType(candidate).isPresent())
+ .findFirst()
+ .orElseThrow(
+ () -> new IllegalArgumentException("Found unresolvable method argument type " + typeName));
}
+ StringBuilder result = new StringBuilder(resolved);
+ for (int index = 0; index < dimensions; index++) {
+ result.append("[]");
+ }
+ return result.toString();
}
- private static String getMethodName(String member) {
- // name is separated from arguments either by '(' or spans the full member
- int indexOfOpeningParentheses = member.indexOf('(');
- if (indexOfOpeningParentheses == -1) {
- return member;
- } else {
- return member.substring(0, indexOfOpeningParentheses);
+ private static String eraseGenerics(String value) {
+ StringBuilder result = new StringBuilder();
+ int depth = 0;
+ for (int index = 0; index < value.length(); index++) {
+ char ch = value.charAt(index);
+ if (ch == '<') {
+ depth++;
+ } else if (ch == '>') {
+ depth--;
+ } else if (depth == 0) {
+ result.append(ch);
+ }
+ }
+ return result.toString();
+ }
+
+ private static boolean isPrimitive(String name) {
+ return "boolean".equals(name)
+ || "byte".equals(name)
+ || "char".equals(name)
+ || "double".equals(name)
+ || "float".equals(name)
+ || "int".equals(name)
+ || "long".equals(name)
+ || "short".equals(name);
+ }
+
+ private FullyQualifiedJavadocReference toReference(
+ ResolvedReferenceTypeDeclaration type,
+ Optional<String> member,
+ Optional<MemberType> memberType,
+ Optional<String> label) {
+ return new FullyQualifiedJavadocReference(
+ type.getPackageName(),
+ Optional.of(type.getClassName()),
+ member,
+ memberType,
+ label,
+ !sourceModel.isInternal(type));
+ }
+
+ @Override
+ public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
+ String fqcn = reference
+ .getFullyQualifiedClassName()
+ .orElseThrow(() ->
+ new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
+ String fieldName = reference
+ .getMember()
+ .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
+ TypeDeclaration<?> type = sourceModel
+ .getType(fqcn)
+ .orElseThrow(() -> new IllegalArgumentException("Could not find source class " + fqcn));
+ for (FieldDeclaration field : type.getFields()) {
+ Optional<com.github.javaparser.ast.body.VariableDeclarator> variable = field.getVariables().stream()
+ .filter(candidate -> candidate.getNameAsString().equals(fieldName))
+ .findFirst();
+ if (variable.isPresent()) {
+ if (!field.isStatic()) {
+ throw new IllegalArgumentException(
+ "Field with name " + fieldName + " in class " + fqcn + " is not static");
+ }
+ return variable.get()
+ .getInitializer()
+ .map(initializer -> initializer.toString())
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Field with name " + fieldName + " in class " + fqcn + " has no initializer"));
+ }
}
+ throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
+ }
+
+ @Override
+ public URI getInternalJavadocSiteBaseUrl() {
+ return linkGenerator.getInternalJavadocSiteBaseUrl();
+ }
+
+ private static ResolvedReferenceTypeDeclaration resolve(TypeDeclaration<?> declaration) {
+ return declaration.resolve();
+ }
+
+ private static String methodName(String member) {
+ int opening = member.indexOf('(');
+ return opening < 0 ? member : member.substring(0, opening);
+ }
+
+ private static String canonicalMember(ResolvedMethodLikeDeclaration declaration) {
+ return declaration.getName() + "("
+ + declaration.formalParameterTypes().stream()
+ .map(type -> type.erasure().describe())
+ .collect(Collectors.joining(","))
+ + ")";
+ }
+
+ private static String simpleName(String name) {
+ int separator = name.lastIndexOf('.');
+ return separator < 0 ? name : name.substring(separator + 1);
}
@SuppressWarnings("unchecked")
diff --git a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java
index 579fff05..6b2776d2 100644
--- a/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java
+++ b/maven-plugin-tools-annotations/src/test/java/org/apache/maven/tools/plugin/extractor/annotations/converter/JavaClassConverterContextTest.java
@@ -20,11 +20,11 @@ package org.apache.maven.tools.plugin.extractor.annotations.converter;
import java.io.File;
import java.net.URI;
-import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
-import com.thoughtworks.qdox.JavaProjectBuilder;
-import com.thoughtworks.qdox.model.JavaClass;
+import com.github.javaparser.ast.body.TypeDeclaration;
+import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel;
import org.apache.maven.tools.plugin.extractor.annotations.converter.test.CurrentClass;
import org.apache.maven.tools.plugin.extractor.annotations.converter.test.OtherClass;
import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
@@ -46,23 +46,24 @@ class JavaClassConverterContextTest {
private final String currentPackageName;
- private final JavaProjectBuilder builder;
+ private final JavaSourceModel sourceModel;
- private final JavaClass contextClass;
+ private final TypeDeclaration<?> contextClass;
private JavadocLinkGenerator linkGenerator;
private URI javadocBaseUri;
- public JavaClassConverterContextTest() throws URISyntaxException {
- builder = new JavaProjectBuilder();
- builder.addSourceFolder(new File("src/test/java"));
+ JavaClassConverterContextTest() throws Exception {
+ sourceModel = new JavaSourceModel(StandardCharsets.UTF_8);
+ sourceModel.addSourceDirectory(new File("src/test/java"));
+ sourceModel.parse();
- contextClass = builder.getClassByName(CurrentClass.class.getName());
- currentPackageName = contextClass.getPackageName();
+ contextClass = sourceModel.getType(CurrentClass.class.getName()).orElseThrow(AssertionError::new);
+ currentPackageName = contextClass.resolve().getPackageName();
javadocBaseUri = new URI("http://localhost/apidocs");
linkGenerator = new JavadocLinkGenerator(javadocBaseUri, "11");
- context = new JavaClassConverterContext(contextClass, builder, Collections.emptyMap(), linkGenerator, 10);
+ context = new JavaClassConverterContext(contextClass, sourceModel, Collections.emptyMap(), linkGenerator, 10);
}
@Test
@@ -202,7 +203,7 @@ class JavaClassConverterContextTest {
MojoAnnotatedClass mojoAnnotatedClass = new MojoAnnotatedClass().setMojo(mojoAnnotationContent);
context = new JavaClassConverterContext(
contextClass,
- builder,
+ sourceModel,
Collections.singletonMap(
"org.apache.maven.tools.plugin.extractor.annotations.converter.test.OtherClass",
mojoAnnotatedClass),