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.net.URL;
024import java.nio.file.Paths;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.stream.Collectors;
032
033import com.thoughtworks.qdox.JavaProjectBuilder;
034import com.thoughtworks.qdox.builder.TypeAssembler;
035import com.thoughtworks.qdox.library.ClassNameLibrary;
036import com.thoughtworks.qdox.model.JavaClass;
037import com.thoughtworks.qdox.model.JavaField;
038import com.thoughtworks.qdox.model.JavaModule;
039import com.thoughtworks.qdox.model.JavaPackage;
040import com.thoughtworks.qdox.model.JavaType;
041import com.thoughtworks.qdox.parser.structs.TypeDef;
042import com.thoughtworks.qdox.type.TypeResolver;
043import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
044import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
045import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
046import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
047import org.apache.maven.tools.plugin.javadoc.JavadocReference;
048
049/** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */
050public class JavaClassConverterContext implements ConverterContext {
051
052    final JavaClass mojoClass; // this is the mojo's class
053
054    final JavaClass declaringClass; // this may be a super class of the mojo's class
055
056    final JavaProjectBuilder javaProjectBuilder;
057
058    final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
059
060    final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured
061
062    final int lineNumber;
063
064    final Optional<JavaModule> javaModule;
065
066    final Map<String, Object> attributes;
067
068    public JavaClassConverterContext(
069            JavaClass mojoClass,
070            JavaProjectBuilder javaProjectBuilder,
071            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
072            JavadocLinkGenerator linkGenerator,
073            int lineNumber) {
074        this(mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber);
075    }
076
077    public JavaClassConverterContext(
078            JavaClass mojoClass,
079            JavaClass declaringClass,
080            JavaProjectBuilder javaProjectBuilder,
081            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
082            JavadocLinkGenerator linkGenerator,
083            int lineNumber) {
084        this.mojoClass = mojoClass;
085        this.declaringClass = declaringClass;
086        this.javaProjectBuilder = javaProjectBuilder;
087        this.mojoAnnotatedClasses = mojoAnnotatedClasses;
088        this.linkGenerator = linkGenerator;
089        this.lineNumber = lineNumber;
090        this.attributes = new HashMap<>();
091
092        javaModule = mojoClass.getJavaClassLibrary().getJavaModules().stream()
093                .filter(m -> m.getDescriptor().getExports().stream()
094                        .anyMatch(e -> e.getSource().getName().equals(getPackageName())))
095                .findFirst();
096    }
097
098    @Override
099    public Optional<String> getModuleName() {
100        // https://github.com/paul-hammant/qdox/issues/113, module name is not exposed
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) // url is not always available, just emit FQCN in that case
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     * @param reference
125     * @return true in case either the current context class or any of its super classes are referenced
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            // check implemented interfaces
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                // link to current goal's parameters
161                return new URI(null, null, reference.getMember().orElse(null)); // just an anchor if same context
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                    // link to other mojo (only for fields = parameters or without member)
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); // should not happen
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        // is it already fully qualified?
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        // is it a member only?
198        if (reference.getMember().isPresent()
199                && !reference.getPackageNameClassName().isPresent()) {
200            // search order for not fully qualified names:
201            // 1. The current class or interface (only for members)
202            resolvedName = resolveMember(declaringClass, reference.getMember(), reference.getLabel());
203            if (resolvedName.isPresent()) {
204                return resolvedName.get();
205            }
206            // 2. Any enclosing classes and interfaces searching the closest first (only members)
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            // 3. Any superclasses and superinterfaces, searching the closest first. (only members)
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            // 4. The current package
225            resolvedName = resolveMember(
226                    declaringClass.getPackageName() + "." + packageNameClassName,
227                    reference.getMember(),
228                    reference.getLabel());
229            if (resolvedName.isPresent()) {
230                return resolvedName.get();
231            }
232            // 5. Any imported packages, classes, and interfaces, searching in the order of the import statement.
233            List<String> importNames = new ArrayList<>();
234            importNames.add("java.lang.*"); // default import
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                        // ends with prefix of reference (nested class name)
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                // is it a nested class?
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                // reference to java package never has a member
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            // member is either field...
355            if (javaClass.getFieldByName(member.get()) == null) {
356                // ...is method...
357                List<JavaType> parameterTypes = getParameterTypes(member.get());
358                String methodName = getMethodName(member.get());
359                if (javaClass.getMethodBySignature(methodName, parameterTypes) == null) {
360                    // ...or is constructor
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                // reconstruct member with fully qualified names but leaving out the argument names
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        // this is never null due to using the ClassNameLibrary in the builder
398        // but every instance of ClassNameLibrary basically means that the class was not found
399        return !(javaClass.getJavaClassLibrary() instanceof ClassNameLibrary);
400    }
401
402    // https://github.com/paul-hammant/qdox/issues/104
403    private List<JavaType> getParameterTypes(String member) {
404        List<JavaType> parameterTypes = new ArrayList<>();
405        // TypeResolver.byClassName() always resolves types as non existing inner class
406        TypeResolver typeResolver = TypeResolver.byClassName(
407                declaringClass.getPackageName(),
408                declaringClass.getJavaClassLibrary(),
409                declaringClass.getSource().getImports());
410
411        // method parameters are optionally enclosed by parentheses
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            // If any method or constructor is entered as a name with no parentheses, such as getValue,
424            // and if there is no field with the same name, then the javadoc command still creates a
425            // link to the method. If this method is overloaded, then the javadoc command links to the
426            // first method its search encounters, which is unspecified
427            // (Source: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#JSWOR654)
428            return Collections.emptyList();
429        }
430        for (String parameter : signatureArguments.split(",")) {
431            // strip off argument name, only type is relevant
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                // already check here for unresolvable types due to https://github.com/paul-hammant/qdox/issues/111
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        // strip dimensions
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        // name is separated from arguments either by '(' or spans the full member
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}