001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.tools.plugin.extractor.annotations.converter;
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.stream.Collectors;
030
031import com.github.javaparser.ast.ImportDeclaration;
032import com.github.javaparser.ast.Node;
033import com.github.javaparser.ast.body.FieldDeclaration;
034import com.github.javaparser.ast.body.TypeDeclaration;
035import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration;
036import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
037import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
038import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration;
039import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
040import com.github.javaparser.resolution.types.ResolvedReferenceType;
041import org.apache.maven.tools.plugin.extractor.annotations.JavaSourceModel;
042import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
043import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
044import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
045import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
046import org.apache.maven.tools.plugin.javadoc.JavadocReference;
047
048/** {@link ConverterContext} backed by JavaParser source declarations and symbol resolution. */
049public class JavaClassConverterContext implements ConverterContext {
050
051    /** The Mojo class whose documentation is being generated. */
052    private final TypeDeclaration<?> mojoClass;
053
054    /** The class declaring the converted Javadoc, possibly a superclass of the Mojo class. */
055    private final TypeDeclaration<?> declaringClass;
056
057    private final Node locationNode;
058    private final JavaSourceModel sourceModel;
059    private final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
060
061    /** The link generator, or {@code null} when no Javadoc site is configured. */
062    private final JavadocLinkGenerator linkGenerator;
063
064    private final int lineNumber;
065    private final Map<String, Object> attributes = new HashMap<>();
066
067    public JavaClassConverterContext(
068            TypeDeclaration<?> mojoClass,
069            JavaSourceModel sourceModel,
070            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
071            JavadocLinkGenerator linkGenerator,
072            int lineNumber) {
073        this(mojoClass, mojoClass, mojoClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber);
074    }
075
076    public JavaClassConverterContext(
077            TypeDeclaration<?> mojoClass,
078            TypeDeclaration<?> declaringClass,
079            Node locationNode,
080            JavaSourceModel sourceModel,
081            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
082            JavadocLinkGenerator linkGenerator,
083            int lineNumber) {
084        this.mojoClass = mojoClass;
085        this.declaringClass = declaringClass;
086        this.locationNode = locationNode;
087        this.sourceModel = sourceModel;
088        this.mojoAnnotatedClasses = mojoAnnotatedClasses;
089        this.linkGenerator = linkGenerator;
090        this.lineNumber = lineNumber;
091    }
092
093    @Override
094    public Optional<String> getModuleName() {
095        return sourceModel.getModuleName(getPackageName());
096    }
097
098    @Override
099    public String getPackageName() {
100        return resolve(mojoClass).getPackageName();
101    }
102
103    @Override
104    public String getLocation() {
105        return sourceModel.getLocation(locationNode, lineNumber);
106    }
107
108    /** Returns whether {@code reference} identifies the Mojo class or one of its ancestors. */
109    @Override
110    public boolean isReferencedBy(FullyQualifiedJavadocReference reference) {
111        ResolvedReferenceTypeDeclaration declaration = resolve(mojoClass);
112        if (isClassReferencedByReference(declaration, reference)) {
113            return true;
114        }
115        return declaration.getAllAncestors().stream()
116                .map(ResolvedReferenceType::getTypeDeclaration)
117                .filter(Optional::isPresent)
118                .map(Optional::get)
119                .anyMatch(type -> isClassReferencedByReference(type, reference));
120    }
121
122    private static boolean isClassReferencedByReference(
123            ResolvedReferenceTypeDeclaration declaration, FullyQualifiedJavadocReference reference) {
124        return declaration.getPackageName().equals(reference.getPackageName().orElse(""))
125                && declaration.getClassName().equals(reference.getClassName().orElse(""));
126    }
127
128    @Override
129    public boolean canGetUrl() {
130        return linkGenerator != null;
131    }
132
133    @Override
134    public URI getUrl(FullyQualifiedJavadocReference reference) {
135        try {
136            if (isReferencedBy(reference)
137                    && MemberType.FIELD == reference.getMemberType().orElse(null)) {
138                // Fields in the current Mojo link to parameter anchors on the same page.
139                return new URI(null, null, reference.getMember().orElse(null));
140            }
141            Optional<String> fqClassName = reference.getFullyQualifiedClassName();
142            if (fqClassName.isPresent()) {
143                MojoAnnotatedClass mojoAnnotatedClass = mojoAnnotatedClasses.get(fqClassName.get());
144                if (mojoAnnotatedClass != null
145                        && mojoAnnotatedClass.getMojo() != null
146                        && (!reference.getLabel().isPresent()
147                                || MemberType.FIELD == reference.getMemberType().orElse(null))) {
148                    // Fields and whole-class references to another Mojo link to that Mojo's page.
149                    return new URI(
150                            null,
151                            "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
152                            reference.getMember().orElse(null));
153                }
154            }
155        } catch (URISyntaxException e) {
156            throw new IllegalStateException("Error constructing a valid URL", e);
157        }
158        if (linkGenerator == null) {
159            throw new IllegalStateException("No Javadoc Sites given to create URLs to");
160        }
161        return linkGenerator.createLink(reference);
162    }
163
164    @Override
165    public FullyQualifiedJavadocReference resolveReference(JavadocReference reference) {
166        Optional<FullyQualifiedJavadocReference> resolved;
167        // First try the reference exactly as written, which handles fully qualified names.
168        if (reference.getPackageNameClassName().isPresent()) {
169            String name = reference.getPackageNameClassName().get();
170            resolved = resolveNamedReference(name, reference.getMember(), reference.getLabel());
171            if (resolved.isPresent()) {
172                return resolved.get();
173            }
174        }
175
176        // Search member-only references in the current class or interface first, then its ancestors.
177        if (reference.getMember().isPresent()
178                && !reference.getPackageNameClassName().isPresent()) {
179            resolved = resolveMember(resolve(declaringClass), reference.getMember(), reference.getLabel(), true);
180            if (resolved.isPresent()) {
181                return resolved.get();
182            }
183        } else if (reference.getPackageNameClassName().isPresent()) {
184            // Resolve simple and nested type names using Javadoc's lookup order.
185            String name = reference.getPackageNameClassName().get();
186            for (String candidate : classNameCandidates(name)) {
187                resolved = resolveNamedReference(candidate, reference.getMember(), reference.getLabel());
188                if (resolved.isPresent()) {
189                    return resolved.get();
190                }
191            }
192        }
193        throw new IllegalArgumentException("Could not resolve javadoc reference " + reference);
194    }
195
196    private Optional<FullyQualifiedJavadocReference> resolveNamedReference(
197            String name, Optional<String> member, Optional<String> label) {
198        Optional<ResolvedReferenceTypeDeclaration> type = sourceModel.resolveType(name);
199        if (type.isPresent()) {
200            return resolveMember(type.get(), member, label, true);
201        }
202        // Package references cannot contain members.
203        if (!member.isPresent() && sourceModel.hasPackage(name)) {
204            return Optional.of(new FullyQualifiedJavadocReference(name, label, !sourceModel.isInternalPackage(name)));
205        }
206        return Optional.empty();
207    }
208
209    private List<String> classNameCandidates(String name) {
210        List<String> candidates = new ArrayList<>();
211        ResolvedReferenceTypeDeclaration declaration = resolve(declaringClass);
212        // Search order: current package, implicit java.lang import, then explicit imports in declaration order.
213        candidates.add(declaration.getPackageName() + "." + name);
214        candidates.add("java.lang." + name);
215        List<ImportDeclaration> imports = declaringClass
216                .findCompilationUnit()
217                .<List<ImportDeclaration>>map(unit -> new ArrayList<>(unit.getImports()))
218                .orElseGet(Collections::emptyList);
219        for (ImportDeclaration importDeclaration : imports) {
220            if (importDeclaration.isStatic()) {
221                continue;
222            }
223            String importName = importDeclaration.getNameAsString();
224            if (importDeclaration.isAsterisk()) {
225                candidates.add(importName + "." + name);
226            } else if (name.equals(simpleName(importName))) {
227                candidates.add(importName);
228            } else if (name.startsWith(simpleName(importName) + ".")) {
229                // An imported outer type may prefix a nested-type reference.
230                candidates.add(
231                        importName + name.substring(simpleName(importName).length()));
232            }
233        }
234        return candidates;
235    }
236
237    private Optional<FullyQualifiedJavadocReference> resolveMember(
238            ResolvedReferenceTypeDeclaration type,
239            Optional<String> member,
240            Optional<String> label,
241            boolean includeAncestors) {
242        if (!member.isPresent()) {
243            return Optional.of(toReference(type, Optional.empty(), Optional.empty(), label));
244        }
245
246        String memberText = member.get();
247        // Resolve ambiguous member text as a field, method, then constructor.
248        Optional<ResolvedFieldDeclaration> field = findField(type, memberText, includeAncestors);
249        if (field.isPresent()) {
250            return Optional.of(toReference(
251                    field.get().declaringType().asReferenceType(), member, Optional.of(MemberType.FIELD), label));
252        }
253
254        String methodName = methodName(memberText);
255        Optional<List<String>> parameterTypes = parameterTypes(memberText);
256        Optional<ResolvedMethodDeclaration> method = findMethod(type, methodName, parameterTypes, includeAncestors);
257        if (method.isPresent()) {
258            return Optional.of(toReference(
259                    method.get().declaringType(),
260                    Optional.of(canonicalMember(method.get())),
261                    Optional.of(MemberType.METHOD),
262                    label));
263        }
264
265        if (methodName.equals(type.getName())) {
266            Optional<ResolvedConstructorDeclaration> constructor = findConstructor(type, parameterTypes);
267            if (constructor.isPresent()) {
268                return Optional.of(toReference(
269                        type,
270                        Optional.of(canonicalMember(constructor.get())),
271                        Optional.of(MemberType.CONSTRUCTOR),
272                        label));
273            }
274        }
275        return Optional.empty();
276    }
277
278    private Optional<ResolvedFieldDeclaration> findField(
279            ResolvedReferenceTypeDeclaration type, String name, boolean includeAncestors) {
280        List<ResolvedFieldDeclaration> fields = includeAncestors ? type.getAllFields() : type.getDeclaredFields();
281        return fields.stream().filter(field -> field.getName().equals(name)).findFirst();
282    }
283
284    private Optional<ResolvedMethodDeclaration> findMethod(
285            ResolvedReferenceTypeDeclaration type,
286            String name,
287            Optional<List<String>> parameterTypes,
288            boolean includeAncestors) {
289        List<ResolvedReferenceTypeDeclaration> hierarchy = new ArrayList<>();
290        hierarchy.add(type);
291        if (includeAncestors) {
292            type.getAllAncestors().stream()
293                    .map(ResolvedReferenceType::getTypeDeclaration)
294                    .filter(Optional::isPresent)
295                    .map(Optional::get)
296                    .forEach(hierarchy::add);
297        }
298        for (ResolvedReferenceTypeDeclaration declaration : hierarchy) {
299            Optional<ResolvedMethodDeclaration> method = declaration.getDeclaredMethods().stream()
300                    .filter(candidate -> candidate.getName().equals(name))
301                    .filter(candidate -> matches(candidate, parameterTypes))
302                    .findFirst();
303            if (method.isPresent()) {
304                return method;
305            }
306        }
307        return Optional.empty();
308    }
309
310    private Optional<ResolvedConstructorDeclaration> findConstructor(
311            ResolvedReferenceTypeDeclaration type, Optional<List<String>> parameterTypes) {
312        return type.getConstructors().stream()
313                .filter(candidate -> matches(candidate, parameterTypes))
314                .findFirst();
315    }
316
317    private boolean matches(ResolvedMethodLikeDeclaration declaration, Optional<List<String>> parameterTypes) {
318        if (!parameterTypes.isPresent()) {
319            return true;
320        }
321        if (declaration.getNumberOfParams() != parameterTypes.get().size()) {
322            return false;
323        }
324        for (int index = 0; index < declaration.getNumberOfParams(); index++) {
325            String actual = declaration.getParam(index).getType().erasure().describe();
326            if (!actual.equals(parameterTypes.get().get(index))) {
327                return false;
328            }
329        }
330        return true;
331    }
332
333    private Optional<List<String>> parameterTypes(String member) {
334        int opening = member.indexOf('(');
335        int closing = member.lastIndexOf(')');
336        if (opening < 0 && closing < 0) {
337            // Without parentheses, match the first overload found, mirroring javadoc.
338            return Optional.empty();
339        }
340        if (opening < 0 || closing < opening) {
341            throw new IllegalArgumentException("Found opening without closing parentheses or vice versa in " + member);
342        }
343        String arguments = member.substring(opening + 1, closing).trim();
344        if (arguments.isEmpty()) {
345            return Optional.of(Collections.emptyList());
346        }
347        List<String> result = new ArrayList<>();
348        for (String argument : splitArguments(arguments)) {
349            result.add(resolveParameterType(stripArgumentName(argument.trim())));
350        }
351        return Optional.of(result);
352    }
353
354    private static List<String> splitArguments(String arguments) {
355        List<String> result = new ArrayList<>();
356        int genericDepth = 0;
357        int start = 0;
358        for (int index = 0; index < arguments.length(); index++) {
359            char ch = arguments.charAt(index);
360            if (ch == '<') {
361                genericDepth++;
362            } else if (ch == '>') {
363                genericDepth--;
364            } else if (ch == ',' && genericDepth == 0) {
365                result.add(arguments.substring(start, index));
366                start = index + 1;
367            }
368        }
369        result.add(arguments.substring(start));
370        return result;
371    }
372
373    private static String stripArgumentName(String argument) {
374        int genericDepth = 0;
375        for (int index = argument.length() - 1; index >= 0; index--) {
376            char ch = argument.charAt(index);
377            if (ch == '>') {
378                genericDepth++;
379            } else if (ch == '<') {
380                genericDepth--;
381            } else if (Character.isWhitespace(ch) && genericDepth == 0) {
382                return argument.substring(0, index).trim();
383            }
384        }
385        return argument;
386    }
387
388    private String resolveParameterType(String typeName) {
389        String normalized = eraseGenerics(typeName.replace("...", "[]"));
390        int dimensions = 0;
391        while (normalized.endsWith("[]")) {
392            dimensions++;
393            normalized = normalized.substring(0, normalized.length() - 2);
394        }
395        String resolved;
396        if (isPrimitive(normalized)) {
397            resolved = normalized;
398        } else {
399            List<String> candidates = new ArrayList<>();
400            candidates.add(normalized);
401            candidates.addAll(classNameCandidates(normalized));
402            resolved = candidates.stream()
403                    .filter(candidate -> sourceModel.resolveType(candidate).isPresent())
404                    .findFirst()
405                    .orElseThrow(
406                            () -> new IllegalArgumentException("Found unresolvable method argument type " + typeName));
407        }
408        StringBuilder result = new StringBuilder(resolved);
409        for (int index = 0; index < dimensions; index++) {
410            result.append("[]");
411        }
412        return result.toString();
413    }
414
415    private static String eraseGenerics(String value) {
416        StringBuilder result = new StringBuilder();
417        int depth = 0;
418        for (int index = 0; index < value.length(); index++) {
419            char ch = value.charAt(index);
420            if (ch == '<') {
421                depth++;
422            } else if (ch == '>') {
423                depth--;
424            } else if (depth == 0) {
425                result.append(ch);
426            }
427        }
428        return result.toString();
429    }
430
431    private static boolean isPrimitive(String name) {
432        return "boolean".equals(name)
433                || "byte".equals(name)
434                || "char".equals(name)
435                || "double".equals(name)
436                || "float".equals(name)
437                || "int".equals(name)
438                || "long".equals(name)
439                || "short".equals(name);
440    }
441
442    private FullyQualifiedJavadocReference toReference(
443            ResolvedReferenceTypeDeclaration type,
444            Optional<String> member,
445            Optional<MemberType> memberType,
446            Optional<String> label) {
447        return new FullyQualifiedJavadocReference(
448                type.getPackageName(),
449                Optional.of(type.getClassName()),
450                member,
451                memberType,
452                label,
453                !sourceModel.isInternal(type));
454    }
455
456    @Override
457    public String getStaticFieldValue(FullyQualifiedJavadocReference reference) {
458        String fqcn = reference
459                .getFullyQualifiedClassName()
460                .orElseThrow(() ->
461                        new IllegalArgumentException("Given reference does not specify a fully qualified class name!"));
462        String fieldName = reference
463                .getMember()
464                .orElseThrow(() -> new IllegalArgumentException("Given reference does not specify a member!"));
465        TypeDeclaration<?> type = sourceModel
466                .getType(fqcn)
467                .orElseThrow(() -> new IllegalArgumentException("Could not find source class " + fqcn));
468        for (FieldDeclaration field : type.getFields()) {
469            Optional<com.github.javaparser.ast.body.VariableDeclarator> variable = field.getVariables().stream()
470                    .filter(candidate -> candidate.getNameAsString().equals(fieldName))
471                    .findFirst();
472            if (variable.isPresent()) {
473                if (!field.isStatic()) {
474                    throw new IllegalArgumentException(
475                            "Field with name " + fieldName + " in class " + fqcn + " is not static");
476                }
477                return variable.get()
478                        .getInitializer()
479                        .map(initializer -> initializer.toString())
480                        .orElseThrow(() -> new IllegalArgumentException(
481                                "Field with name " + fieldName + " in class " + fqcn + " has no initializer"));
482            }
483        }
484        throw new IllegalArgumentException("Could not find field with name " + fieldName + " in class " + fqcn);
485    }
486
487    @Override
488    public URI getInternalJavadocSiteBaseUrl() {
489        return linkGenerator.getInternalJavadocSiteBaseUrl();
490    }
491
492    private static ResolvedReferenceTypeDeclaration resolve(TypeDeclaration<?> declaration) {
493        return declaration.resolve();
494    }
495
496    private static String methodName(String member) {
497        int opening = member.indexOf('(');
498        return opening < 0 ? member : member.substring(0, opening);
499    }
500
501    private static String canonicalMember(ResolvedMethodLikeDeclaration declaration) {
502        return declaration.getName() + "("
503                + declaration.formalParameterTypes().stream()
504                        .map(type -> type.erasure().describe())
505                        .collect(Collectors.joining(","))
506                + ")";
507    }
508
509    private static String simpleName(String name) {
510        int separator = name.lastIndexOf('.');
511        return separator < 0 ? name : name.substring(separator + 1);
512    }
513
514    @SuppressWarnings("unchecked")
515    @Override
516    public <T> T setAttribute(String name, T value) {
517        return (T) attributes.put(name, value);
518    }
519
520    @SuppressWarnings("unchecked")
521    @Override
522    public <T> T getAttribute(String name, Class<T> clazz, T defaultValue) {
523        return (T) attributes.getOrDefault(name, defaultValue);
524    }
525}