1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.tools.plugin.extractor.annotations.converter;
20
21 import java.net.URI;
22 import java.net.URISyntaxException;
23 import java.net.URL;
24 import java.nio.file.Paths;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import java.util.stream.Collectors;
32
33 import com.thoughtworks.qdox.JavaProjectBuilder;
34 import com.thoughtworks.qdox.builder.TypeAssembler;
35 import com.thoughtworks.qdox.library.ClassNameLibrary;
36 import com.thoughtworks.qdox.model.JavaClass;
37 import com.thoughtworks.qdox.model.JavaField;
38 import com.thoughtworks.qdox.model.JavaModule;
39 import com.thoughtworks.qdox.model.JavaPackage;
40 import com.thoughtworks.qdox.model.JavaType;
41 import com.thoughtworks.qdox.parser.structs.TypeDef;
42 import com.thoughtworks.qdox.type.TypeResolver;
43 import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
44 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
45 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
46 import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
47 import org.apache.maven.tools.plugin.javadoc.JavadocReference;
48
49
50 public class JavaClassConverterContext implements ConverterContext {
51
52 final JavaClass mojoClass;
53
54 final JavaClass declaringClass;
55
56 final JavaProjectBuilder javaProjectBuilder;
57
58 final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
59
60 final JavadocLinkGenerator linkGenerator;
61
62 final int lineNumber;
63
64 final Optional<JavaModule> javaModule;
65
66 final Map<String, Object> attributes;
67
68 public JavaClassConverterContext(
69 JavaClass mojoClass,
70 JavaProjectBuilder javaProjectBuilder,
71 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
72 JavadocLinkGenerator linkGenerator,
73 int lineNumber) {
74 this(mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber);
75 }
76
77 public JavaClassConverterContext(
78 JavaClass mojoClass,
79 JavaClass declaringClass,
80 JavaProjectBuilder javaProjectBuilder,
81 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
82 JavadocLinkGenerator linkGenerator,
83 int lineNumber) {
84 this.mojoClass = mojoClass;
85 this.declaringClass = declaringClass;
86 this.javaProjectBuilder = javaProjectBuilder;
87 this.mojoAnnotatedClasses = mojoAnnotatedClasses;
88 this.linkGenerator = linkGenerator;
89 this.lineNumber = lineNumber;
90 this.attributes = new HashMap<>();
91
92 javaModule = mojoClass.getJavaClassLibrary().getJavaModules().stream()
93 .filter(m -> m.getDescriptor().getExports().stream()
94 .anyMatch(e -> e.getSource().getName().equals(getPackageName())))
95 .findFirst();
96 }
97
98 @Override
99 public Optional<String> getModuleName() {
100
101 return javaModule.map(JavaModule::getName);
102 }
103
104 @Override
105 public String getPackageName() {
106 return mojoClass.getPackageName();
107 }
108
109 @Override
110 public String getLocation() {
111 try {
112 URL url = declaringClass.getSource().getURL();
113 if (url == null)
114 {
115 return declaringClass.getPackageName() + declaringClass.getSimpleName() + ":" + lineNumber;
116 }
117 return Paths.get("").toUri().relativize(url.toURI()) + ":" + lineNumber;
118 } catch (URISyntaxException e) {
119 return declaringClass.getSource().getURL() + ":" + lineNumber;
120 }
121 }
122
123
124
125
126
127 @Override
128 public boolean isReferencedBy(FullyQualifiedJavadocReference reference) {
129 JavaClass javaClassInHierarchy = this.mojoClass;
130 while (javaClassInHierarchy != null) {
131 if (isClassReferencedByReference(javaClassInHierarchy, reference)) {
132 return true;
133 }
134
135 for (JavaClass implementedInterfaces : javaClassInHierarchy.getInterfaces()) {
136 if (isClassReferencedByReference(implementedInterfaces, reference)) {
137 return true;
138 }
139 }
140 javaClassInHierarchy = javaClassInHierarchy.getSuperJavaClass();
141 }
142 return false;
143 }
144
145 private static boolean isClassReferencedByReference(JavaClass javaClass, FullyQualifiedJavadocReference reference) {
146 return javaClass.getPackageName().equals(reference.getPackageName().orElse(""))
147 && javaClass.getSimpleName().equals(reference.getClassName().orElse(""));
148 }
149
150 @Override
151 public boolean canGetUrl() {
152 return linkGenerator != null;
153 }
154
155 @Override
156 public URI getUrl(FullyQualifiedJavadocReference reference) {
157 try {
158 if (isReferencedBy(reference)
159 && MemberType.FIELD == reference.getMemberType().orElse(null)) {
160
161 return new URI(null, null, reference.getMember().orElse(null));
162 }
163 Optional<String> fqClassName = reference.getFullyQualifiedClassName();
164 if (fqClassName.isPresent()) {
165 MojoAnnotatedClass mojoAnnotatedClass = mojoAnnotatedClasses.get(fqClassName.get());
166 if (mojoAnnotatedClass != null
167 && mojoAnnotatedClass.getMojo() != null
168 && (!reference.getLabel().isPresent()
169 || MemberType.FIELD == reference.getMemberType().orElse(null))) {
170
171 return new URI(
172 null,
173 "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
174 reference.getMember().orElse(null));
175 }
176 }
177 } catch (URISyntaxException e) {
178 throw new IllegalStateException("Error constructing a valid URL", e);
179 }
180 if (linkGenerator == null) {
181 throw new IllegalStateException("No Javadoc Sites given to create URLs to");
182 }
183 return linkGenerator.createLink(reference);
184 }
185
186 @Override
187 public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) {
188 Optional<FullyQualifiedJavadocReference> resolvedName;
189
190 if (reference.getPackageNameClassName().isPresent()) {
191 resolvedName = resolveMember(
192 reference.getPackageNameClassName().get(), reference.getMember(), reference.getLabel());
193 if (resolvedName.isPresent()) {
194 return resolvedName.get();
195 }
196 }
197
198 if (reference.getMember().isPresent()
199 && !reference.getPackageNameClassName().isPresent()) {
200
201
202 resolvedName = resolveMember(declaringClass, reference.getMember(), reference.getLabel());
203 if (resolvedName.isPresent()) {
204 return resolvedName.get();
205 }
206
207 for (JavaClass nestedClass : declaringClass.getNestedClasses()) {
208 resolvedName = resolveMember(nestedClass, reference.getMember(), reference.getLabel());
209 if (resolvedName.isPresent()) {
210 return resolvedName.get();
211 }
212 }
213
214 JavaClass superClass = declaringClass.getSuperJavaClass();
215 while (superClass != null) {
216 resolvedName = resolveMember(superClass, reference.getMember(), reference.getLabel());
217 if (resolvedName.isPresent()) {
218 return resolvedName.get();
219 }
220 superClass = superClass.getSuperJavaClass();
221 }
222 } else {
223 String packageNameClassName = reference.getPackageNameClassName().get();
224
225 resolvedName = resolveMember(
226 declaringClass.getPackageName() + "." + packageNameClassName,
227 reference.getMember(),
228 reference.getLabel());
229 if (resolvedName.isPresent()) {
230 return resolvedName.get();
231 }
232
233 List<String> importNames = new ArrayList<>();
234 importNames.add("java.lang.*");
235 importNames.addAll(declaringClass.getSource().getImports());
236 for (String importName : importNames) {
237 if (importName.endsWith(".*")) {
238 resolvedName = resolveMember(
239 importName.replace("*", packageNameClassName), reference.getMember(), reference.getLabel());
240 if (resolvedName.isPresent()) {
241 return resolvedName.get();
242 }
243 } else {
244 if (importName.endsWith(packageNameClassName)) {
245 resolvedName = resolveMember(importName, reference.getMember(), reference.getLabel());
246 if (resolvedName.isPresent()) {
247 return resolvedName.get();
248 }
249 } else {
250
251 int firstDotIndex = packageNameClassName.indexOf(".");
252 if (firstDotIndex > 0
253 && importName.endsWith(packageNameClassName.substring(0, firstDotIndex))) {
254 resolvedName = resolveMember(
255 importName,
256 packageNameClassName.substring(firstDotIndex + 1),
257 reference.getMember(),
258 reference.getLabel());
259 if (resolvedName.isPresent()) {
260 return resolvedName.get();
261 }
262 }
263 }
264 }
265 }
266 }
267 throw new IllegalArgumentException("Could not resolve javadoc reference " + reference);
268 }
269
270 @Override
271 public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
272 String fqcn = reference
273 .getFullyQualifiedClassName()
274 .orElseThrow(() ->
275 new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
276 String fieldName = reference
277 .getMember()
278 .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
279 JavaClass javaClass = javaProjectBuilder.getClassByName(fqcn);
280 JavaField javaField = javaClass.getFieldByName(fieldName);
281 if (javaField == null) {
282 throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
283 }
284 if (!javaField.isStatic()) {
285 throw new IllegalArgumentException("Field with name " + fieldName + " in class " + fqcn + " is not static");
286 }
287 return javaField.getInitializationExpression();
288 }
289
290 @Override
291 public URI getInternalJavadocSiteBaseUrl() {
292 return linkGenerator.getInternalJavadocSiteBaseUrl();
293 }
294
295 private Optional<FullyQualifiedJavadocReference> resolveMember(
296 String fullyQualifiedPackageNameClassName, Optional<String> member, Optional<String> label) {
297 return resolveMember(fullyQualifiedPackageNameClassName, "", member, label);
298 }
299
300 private Optional<FullyQualifiedJavadocReference> resolveMember(
301 String fullyQualifiedPackageNameClassName,
302 String nestedClassName,
303 Optional<String> member,
304 Optional<String> label) {
305 JavaClass javaClass = javaProjectBuilder.getClassByName(fullyQualifiedPackageNameClassName);
306 if (!isClassFound(javaClass)) {
307 JavaPackage javaPackage = javaProjectBuilder.getPackageByName(fullyQualifiedPackageNameClassName);
308 if (javaPackage == null || !nestedClassName.isEmpty()) {
309
310 int lastIndexOfDot = fullyQualifiedPackageNameClassName.lastIndexOf('.');
311 if (lastIndexOfDot > 0) {
312 String newNestedClassName = nestedClassName;
313 if (!newNestedClassName.isEmpty()) {
314 newNestedClassName += '.';
315 }
316 newNestedClassName += fullyQualifiedPackageNameClassName.substring(lastIndexOfDot + 1);
317 return resolveMember(
318 fullyQualifiedPackageNameClassName.substring(0, lastIndexOfDot),
319 newNestedClassName,
320 member,
321 label);
322 }
323 return Optional.empty();
324 } else {
325
326 return Optional.of(
327 new FullyQualifiedJavadocReference(javaPackage.getName(), label, isExternal(javaPackage)));
328 }
329 } else {
330 if (!nestedClassName.isEmpty()) {
331 javaClass = javaClass.getNestedClassByName(nestedClassName);
332 if (javaClass == null) {
333 return Optional.empty();
334 }
335 }
336
337 return resolveMember(javaClass, member, label);
338 }
339 }
340
341 private boolean isExternal(JavaClass javaClass) {
342 return isExternal(javaClass.getPackage());
343 }
344
345 private boolean isExternal(JavaPackage javaPackage) {
346 return !javaPackage.getJavaClassLibrary().equals(mojoClass.getJavaClassLibrary());
347 }
348
349 private Optional<FullyQualifiedJavadocReference> resolveMember(
350 JavaClass javaClass, Optional<String> member, Optional<String> label) {
351 final Optional<MemberType> memberType;
352 Optional<String> resolvedMember = member;
353 if (member.isPresent()) {
354
355 if (javaClass.getFieldByName(member.get()) == null) {
356
357 List<JavaType> parameterTypes = getParameterTypes(member.get());
358 String methodName = getMethodName(member.get());
359 if (javaClass.getMethodBySignature(methodName, parameterTypes) == null) {
360
361 if ((!methodName.equals(javaClass.getSimpleName()))
362 || (javaClass.getConstructor(parameterTypes) == null)) {
363 return Optional.empty();
364 } else {
365 memberType = Optional.of(MemberType.CONSTRUCTOR);
366 }
367 } else {
368 memberType = Optional.of(MemberType.METHOD);
369 }
370
371 StringBuilder memberBuilder = new StringBuilder(methodName);
372 memberBuilder.append("(");
373 memberBuilder.append(parameterTypes.stream()
374 .map(JavaType::getFullyQualifiedName)
375 .collect(Collectors.joining(",")));
376 memberBuilder.append(")");
377 resolvedMember = Optional.of(memberBuilder.toString());
378 } else {
379 memberType = Optional.of(MemberType.FIELD);
380 }
381 } else {
382 memberType = Optional.empty();
383 }
384 String className = javaClass
385 .getCanonicalName()
386 .substring(javaClass.getPackageName().length() + 1);
387 return Optional.of(new FullyQualifiedJavadocReference(
388 javaClass.getPackageName(),
389 Optional.of(className),
390 resolvedMember,
391 memberType,
392 label,
393 isExternal(javaClass)));
394 }
395
396 private static boolean isClassFound(JavaClass javaClass) {
397
398
399 return !(javaClass.getJavaClassLibrary() instanceof ClassNameLibrary);
400 }
401
402
403 private List<JavaType> getParameterTypes(String member) {
404 List<JavaType> parameterTypes = new ArrayList<>();
405
406 TypeResolver typeResolver = TypeResolver.byClassName(
407 declaringClass.getPackageName(),
408 declaringClass.getJavaClassLibrary(),
409 declaringClass.getSource().getImports());
410
411
412 int indexOfOpeningParenthesis = member.indexOf('(');
413 int indexOfClosingParenthesis = member.indexOf(')');
414 final String signatureArguments;
415 if (indexOfOpeningParenthesis >= 0
416 && indexOfClosingParenthesis > 0
417 && indexOfClosingParenthesis > indexOfOpeningParenthesis) {
418 signatureArguments = member.substring(indexOfOpeningParenthesis + 1, indexOfClosingParenthesis);
419 } else if (indexOfOpeningParenthesis == -1 && indexOfClosingParenthesis >= 0
420 || indexOfOpeningParenthesis >= 0 && indexOfOpeningParenthesis == -1) {
421 throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member);
422 } else {
423
424
425
426
427
428 return Collections.emptyList();
429 }
430 for (String parameter : signatureArguments.split(",")) {
431
432 String canonicalParameter = parameter.trim();
433 int spaceIndex = canonicalParameter.indexOf(' ');
434 final String typeName;
435 if (spaceIndex > 0) {
436 typeName = canonicalParameter.substring(0, spaceIndex).trim();
437 } else {
438 typeName = canonicalParameter;
439 }
440 if (!typeName.isEmpty()) {
441 String rawTypeName = getRawTypeName(typeName);
442
443 if (typeResolver.resolveType(rawTypeName) == null) {
444 throw new IllegalArgumentException("Found unresolvable method argument type in " + member);
445 }
446 TypeDef typeDef = new TypeDef(getRawTypeName(typeName));
447 int dimensions = getDimensions(typeName);
448 JavaType javaType = TypeAssembler.createUnresolved(typeDef, dimensions, typeResolver);
449
450 parameterTypes.add(javaType);
451 }
452 }
453 return parameterTypes;
454 }
455
456 private static int getDimensions(String type) {
457 return (int) type.chars().filter(ch -> ch == '[').count();
458 }
459
460 private static String getRawTypeName(String typeName) {
461
462 int indexOfOpeningBracket = typeName.indexOf('[');
463 if (indexOfOpeningBracket >= 0) {
464 return typeName.substring(0, indexOfOpeningBracket);
465 } else {
466 return typeName;
467 }
468 }
469
470 private static String getMethodName(String member) {
471
472 int indexOfOpeningParentheses = member.indexOf('(');
473 if (indexOfOpeningParentheses == -1) {
474 return member;
475 } else {
476 return member.substring(0, indexOfOpeningParentheses);
477 }
478 }
479
480 @SuppressWarnings("unchecked")
481 @Override
482 public <T> T setAttribute(String name, T value) {
483 return (T) attributes.put(name, value);
484 }
485
486 @SuppressWarnings("unchecked")
487 @Override
488 public <T> T getAttribute(String name, Class<T> clazz, T defaultValue) {
489 return (T) attributes.getOrDefault(name, defaultValue);
490 }
491 }