001package org.apache.maven.tools.plugin.extractor.annotations.converter;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.net.URL;
025import java.nio.file.Paths;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import java.util.stream.Collectors;
033
034import com.thoughtworks.qdox.JavaProjectBuilder;
035import com.thoughtworks.qdox.builder.TypeAssembler;
036import com.thoughtworks.qdox.library.ClassNameLibrary;
037import com.thoughtworks.qdox.model.JavaClass;
038import com.thoughtworks.qdox.model.JavaField;
039import com.thoughtworks.qdox.model.JavaModule;
040import com.thoughtworks.qdox.model.JavaPackage;
041import com.thoughtworks.qdox.model.JavaType;
042import com.thoughtworks.qdox.parser.structs.TypeDef;
043import com.thoughtworks.qdox.type.TypeResolver;
044import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
045import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
046import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
047import org.apache.maven.tools.plugin.javadoc.JavadocReference;
048import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
049
050/** {@link ConverterContext} based on QDox's {@link JavaClass} and {@link JavaProjectBuilder}. */
051public class JavaClassConverterContext
052    implements ConverterContext
053{
054
055    final JavaClass mojoClass; // this is the mojo's class
056
057    final JavaClass declaringClass; // this may be a super class of the mojo's class
058
059    final JavaProjectBuilder javaProjectBuilder;
060
061    final Map<String, MojoAnnotatedClass> mojoAnnotatedClasses;
062
063    final JavadocLinkGenerator linkGenerator; // may be null in case nothing was configured
064
065    final int lineNumber;
066
067    final Optional<JavaModule> javaModule;
068    
069    final Map<String, Object> attributes;
070
071    public JavaClassConverterContext( JavaClass mojoClass, JavaProjectBuilder javaProjectBuilder,
072                                      Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
073                                      JavadocLinkGenerator linkGenerator, int lineNumber )
074    {
075        this( mojoClass, mojoClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, lineNumber );
076    }
077
078    public JavaClassConverterContext( JavaClass mojoClass, JavaClass declaringClass,
079                                      JavaProjectBuilder javaProjectBuilder,
080                                      Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
081                                      JavadocLinkGenerator linkGenerator, int lineNumber )
082    {
083        this.mojoClass = mojoClass;
084        this.declaringClass = declaringClass;
085        this.javaProjectBuilder = javaProjectBuilder;
086        this.mojoAnnotatedClasses = mojoAnnotatedClasses;
087        this.linkGenerator = linkGenerator;
088        this.lineNumber = lineNumber;
089        this.attributes = new HashMap<>();
090
091        javaModule =
092            mojoClass.getJavaClassLibrary().getJavaModules().stream().filter( 
093                m -> m.getDescriptor().getExports().stream().anyMatch( 
094                    e -> e.getSource().getName().equals( getPackageName() ) 
095                ) )
096            .findFirst();
097    }
098
099    @Override
100    public Optional<String> getModuleName()
101    {
102        // https://github.com/paul-hammant/qdox/issues/113, module name is not exposed
103        return javaModule.map( JavaModule::getName );
104    }
105
106    @Override
107    public String getPackageName()
108    {
109        return mojoClass.getPackageName();
110    }
111
112    @Override
113    public String getLocation()
114    {
115        try
116        {
117            URL url = declaringClass.getSource().getURL();
118            if ( url == null ) // url is not always available, just emit FQCN in that case
119            {
120                return declaringClass.getPackageName() + declaringClass.getSimpleName() + ":" + lineNumber;
121            }
122            return Paths.get( "" ).toUri().relativize( url.toURI() ) + ":" + lineNumber;
123        }
124        catch ( URISyntaxException e )
125        {
126            return declaringClass.getSource().getURL() + ":" + lineNumber;
127        }
128    }
129
130    /**
131     * @param reference
132     * @return true in case either the current context class or any of its super classes are referenced
133     */
134    @Override
135    public boolean isReferencedBy( FullyQualifiedJavadocReference reference )
136    {
137        JavaClass javaClassInHierarchy = this.mojoClass;
138        while ( javaClassInHierarchy != null )
139        {
140            if ( isClassReferencedByReference( javaClassInHierarchy, reference ) )
141            {
142                return true;
143            }
144            // check implemented interfaces
145            for ( JavaClass implementedInterfaces : javaClassInHierarchy.getInterfaces() )
146            {
147                if ( isClassReferencedByReference( implementedInterfaces, reference ) )
148                {
149                    return true;
150                }
151            }
152            javaClassInHierarchy = javaClassInHierarchy.getSuperJavaClass();
153        }
154        return false;
155    }
156
157    private static boolean isClassReferencedByReference( JavaClass javaClass, FullyQualifiedJavadocReference reference )
158    {
159        return javaClass.getPackageName().equals( reference.getPackageName().orElse( "" ) )
160            && javaClass.getSimpleName().equals( reference.getClassName().orElse( "" ) );
161    }
162
163    
164    @Override
165    public boolean canGetUrl()
166    {
167        return linkGenerator != null;
168    }
169
170    @Override
171    public URI getUrl( FullyQualifiedJavadocReference reference )
172    {
173        try
174        {
175            if ( isReferencedBy( reference ) && MemberType.FIELD == reference.getMemberType().orElse( null ) )
176            {
177                // link to current goal's parameters
178                return new URI( null, null, reference.getMember().orElse( null ) ); // just an anchor if same context
179            }
180            Optional<String> fqClassName = reference.getFullyQualifiedClassName();
181            if ( fqClassName.isPresent() )
182            {
183                MojoAnnotatedClass mojoAnnotatedClass = mojoAnnotatedClasses.get( fqClassName.get() );
184                if ( mojoAnnotatedClass != null && mojoAnnotatedClass.getMojo() != null
185                    && ( !reference.getLabel().isPresent()
186                        || MemberType.FIELD == reference.getMemberType().orElse( null ) ) )
187                {
188                    // link to other mojo (only for fields = parameters or without member)
189                    return new URI( null, "./" + mojoAnnotatedClass.getMojo().name() + "-mojo.html",
190                                    reference.getMember().orElse( null ) );
191                }
192            }
193        }
194        catch ( URISyntaxException e )
195        {
196            throw new IllegalStateException( "Error constructing a valid URL", e ); // should not happen
197        }
198        if ( linkGenerator == null )
199        {
200            throw new IllegalStateException( "No Javadoc Sites given to create URLs to" );
201        }
202        return linkGenerator.createLink( reference );
203    }
204
205    @Override
206    public FullyQualifiedJavadocReference resolveReference( JavadocReference reference )
207    {
208        Optional<FullyQualifiedJavadocReference> resolvedName;
209        // is it already fully qualified?
210        if ( reference.getPackageNameClassName().isPresent() )
211        {
212            resolvedName =
213                resolveMember( reference.getPackageNameClassName().get(), reference.getMember(), reference.getLabel() );
214            if ( resolvedName.isPresent() )
215            {
216                return resolvedName.get();
217            }
218        }
219        // is it a member only?
220        if ( reference.getMember().isPresent() && !reference.getPackageNameClassName().isPresent() )
221        {
222            // search order for not fully qualified names:
223            // 1. The current class or interface (only for members)
224            resolvedName = resolveMember( declaringClass, reference.getMember(), reference.getLabel() );
225            if ( resolvedName.isPresent() )
226            {
227                return resolvedName.get();
228            }
229            // 2. Any enclosing classes and interfaces searching the closest first (only members)
230            for ( JavaClass nestedClass : declaringClass.getNestedClasses() )
231            {
232                resolvedName = resolveMember( nestedClass, reference.getMember(), reference.getLabel() );
233                if ( resolvedName.isPresent() )
234                {
235                    return resolvedName.get();
236                }
237            }
238            // 3. Any superclasses and superinterfaces, searching the closest first. (only members)
239            JavaClass superClass = declaringClass.getSuperJavaClass();
240            while ( superClass != null )
241            {
242                resolvedName = resolveMember( superClass, reference.getMember(), reference.getLabel() );
243                if ( resolvedName.isPresent() )
244                {
245                    return resolvedName.get();
246                }
247                superClass = superClass.getSuperJavaClass();
248            }
249        }
250        else
251        {
252            String packageNameClassName = reference.getPackageNameClassName().get();
253            // 4. The current package
254            resolvedName = resolveMember( declaringClass.getPackageName() + "." + packageNameClassName,
255                                          reference.getMember(), reference.getLabel() );
256            if ( resolvedName.isPresent() )
257            {
258                return resolvedName.get();
259            }
260            // 5. Any imported packages, classes, and interfaces, searching in the order of the import statement.
261            List<String> importNames = new ArrayList<>();
262            importNames.add( "java.lang.*" ); // default import
263            importNames.addAll( declaringClass.getSource().getImports() );
264            for ( String importName : importNames )
265            {
266                if ( importName.endsWith( ".*" ) )
267                {
268                    resolvedName = resolveMember( importName.replace( "*", packageNameClassName ),
269                                                  reference.getMember(), reference.getLabel() );
270                    if ( resolvedName.isPresent() )
271                    {
272                        return resolvedName.get();
273                    }
274                }
275                else
276                {
277                    if ( importName.endsWith( packageNameClassName ) )
278                    {
279                        resolvedName = resolveMember( importName, reference.getMember(), reference.getLabel() );
280                        if ( resolvedName.isPresent() )
281                        {
282                            return resolvedName.get();
283                        }
284                    }
285                    else
286                    {
287                        // ends with prefix of reference (nested class name)
288                        int firstDotIndex = packageNameClassName.indexOf( "." );
289                        if ( firstDotIndex > 0
290                            && importName.endsWith( packageNameClassName.substring( 0, firstDotIndex ) ) )
291                        {
292                            resolvedName =
293                                resolveMember( importName, packageNameClassName.substring( firstDotIndex + 1 ),
294                                               reference.getMember(), reference.getLabel() );
295                            if ( resolvedName.isPresent() )
296                            {
297                                return resolvedName.get();
298                            }
299                        }
300                    }
301                }
302            }
303        }
304        throw new IllegalArgumentException( "Could not resolve javadoc reference " + reference );
305    }
306
307    @Override
308    public String getStaticFieldValue( FullyQualifiedJavadocReference reference )
309    {
310        String fqcn = reference.getFullyQualifiedClassName().orElseThrow(
311            () -> new IllegalArgumentException( "Given reference does not specify a fully qualified class name!" ) );
312        String fieldName = reference.getMember().orElseThrow(
313            () -> new IllegalArgumentException( "Given reference does not specify a member!" ) );
314        JavaClass javaClass = javaProjectBuilder.getClassByName( fqcn );
315        JavaField javaField = javaClass.getFieldByName( fieldName );
316        if ( javaField == null )
317        {
318            throw new IllegalArgumentException( "Could not find field with name " + fieldName + " in class " + fqcn );
319        }
320        if ( !javaField.isStatic() )
321        {
322            throw new IllegalArgumentException( "Field with name " + fieldName + " in class " + fqcn
323                                                + " is not static" );
324        }
325        return javaField.getInitializationExpression();
326    }
327
328    @Override
329    public URI getInternalJavadocSiteBaseUrl()
330    {
331        return linkGenerator.getInternalJavadocSiteBaseUrl();
332    }
333
334    private Optional<FullyQualifiedJavadocReference> resolveMember( String fullyQualifiedPackageNameClassName,
335                                                                    Optional<String> member, Optional<String> label )
336    {
337        return resolveMember( fullyQualifiedPackageNameClassName, "", member, label );
338    }
339
340    private Optional<FullyQualifiedJavadocReference> resolveMember( String fullyQualifiedPackageNameClassName,
341                                                                    String nestedClassName, Optional<String> member,
342                                                                    Optional<String> label )
343    {
344        JavaClass javaClass = javaProjectBuilder.getClassByName( fullyQualifiedPackageNameClassName );
345        if ( !isClassFound( javaClass ) )
346        {
347            JavaPackage javaPackage = javaProjectBuilder.getPackageByName( fullyQualifiedPackageNameClassName );
348            if ( javaPackage == null || !nestedClassName.isEmpty() )
349            {
350                // is it a nested class?
351                int lastIndexOfDot = fullyQualifiedPackageNameClassName.lastIndexOf( '.' );
352                if ( lastIndexOfDot > 0 )
353                {
354                    String newNestedClassName = nestedClassName;
355                    if ( !newNestedClassName.isEmpty() )
356                    {
357                        newNestedClassName += '.';
358                    }
359                    newNestedClassName += fullyQualifiedPackageNameClassName.substring( lastIndexOfDot + 1 );
360                    return resolveMember( fullyQualifiedPackageNameClassName.substring( 0, lastIndexOfDot ),
361                                          newNestedClassName, member, label );
362                }
363                return Optional.empty();
364            }
365            else
366            {
367                // reference to java package never has a member
368                return Optional.of( new FullyQualifiedJavadocReference( javaPackage.getName(), label,
369                                                                        isExternal( javaPackage ) ) );
370            }
371        }
372        else
373        {
374            if ( !nestedClassName.isEmpty() )
375            {
376                javaClass = javaClass.getNestedClassByName( nestedClassName );
377                if ( javaClass == null )
378                {
379                    return Optional.empty();
380                }
381            }
382
383            return resolveMember( javaClass, member, label );
384        }
385    }
386
387    private boolean isExternal( JavaClass javaClass )
388    {
389        return isExternal( javaClass.getPackage() );
390    }
391    
392    private boolean isExternal( JavaPackage javaPackage )
393    {
394        return !javaPackage.getJavaClassLibrary().equals( mojoClass.getJavaClassLibrary() );
395    }
396
397    private Optional<FullyQualifiedJavadocReference> resolveMember( JavaClass javaClass, Optional<String> member,
398                                                                    Optional<String> label )
399    {
400        final Optional<MemberType> memberType;
401        Optional<String> resolvedMember = member;
402        if ( member.isPresent() )
403        {
404            // member is either field...
405            if ( javaClass.getFieldByName( member.get() ) == null )
406            {
407                // ...is method...
408                List<JavaType> parameterTypes = getParameterTypes( member.get() );
409                String methodName = getMethodName( member.get() );
410                if ( javaClass.getMethodBySignature( methodName, parameterTypes ) == null )
411                {
412                    // ...or is constructor
413                    if ( ( !methodName.equals( javaClass.getSimpleName() ) )
414                        || ( javaClass.getConstructor( parameterTypes ) == null ) )
415                    {
416                        return Optional.empty();
417                    }
418                    else
419                    {
420                        memberType = Optional.of( MemberType.CONSTRUCTOR );
421                    }
422                }
423                else
424                {
425                    memberType = Optional.of( MemberType.METHOD );
426                }
427                // reconstruct member with fully qualified names but leaving out the argument names
428                StringBuilder memberBuilder = new StringBuilder( methodName );
429                memberBuilder.append( "(" );
430                memberBuilder.append( parameterTypes.stream().map( JavaType::getFullyQualifiedName )
431                                      .collect( Collectors.joining( "," ) ) );
432                memberBuilder.append( ")" );
433                resolvedMember = Optional.of( memberBuilder.toString() );
434            }
435            else
436            {
437                memberType = Optional.of( MemberType.FIELD );
438            }
439        }
440        else
441        {
442            memberType = Optional.empty();
443        }
444        String className = javaClass.getCanonicalName().substring( javaClass.getPackageName().length() + 1 );
445        return Optional.of( new FullyQualifiedJavadocReference( javaClass.getPackageName(), Optional.of( className ),
446                                                                resolvedMember, memberType, label,
447                                                                isExternal( javaClass ) ) );
448    }
449
450    private static boolean isClassFound( JavaClass javaClass )
451    {
452        // this is never null due to using the ClassNameLibrary in the builder
453        // but every instance of ClassNameLibrary basically means that the class was not found
454        return !( javaClass.getJavaClassLibrary() instanceof ClassNameLibrary );
455    }
456
457    // https://github.com/paul-hammant/qdox/issues/104
458    private List<JavaType> getParameterTypes( String member )
459    {
460        List<JavaType> parameterTypes = new ArrayList<>();
461        // TypeResolver.byClassName() always resolves types as non existing inner class
462        TypeResolver typeResolver =
463            TypeResolver.byClassName( declaringClass.getPackageName(), declaringClass.getJavaClassLibrary(),
464                                      declaringClass.getSource().getImports() );
465
466        // method parameters are optionally enclosed by parentheses
467        int indexOfOpeningParenthesis = member.indexOf( '(' );
468        int indexOfClosingParenthesis = member.indexOf( ')' );
469        final String signatureArguments;
470        if ( indexOfOpeningParenthesis >= 0 && indexOfClosingParenthesis > 0
471            && indexOfClosingParenthesis > indexOfOpeningParenthesis )
472        {
473            signatureArguments = member.substring( indexOfOpeningParenthesis + 1, indexOfClosingParenthesis );
474        }
475        else if ( indexOfOpeningParenthesis == -1 && indexOfClosingParenthesis >= 0
476            || indexOfOpeningParenthesis >= 0 && indexOfOpeningParenthesis == -1 )
477        {
478            throw new IllegalArgumentException( "Found opening without closing parentheses or vice versa in "
479                + member );
480        }
481        else
482        {
483            // If any method or constructor is entered as a name with no parentheses, such as getValue,
484            // and if there is no field with the same name, then the javadoc command still creates a
485            // link to the method. If this method is overloaded, then the javadoc command links to the
486            // first method its search encounters, which is unspecified
487            // (Source: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html#JSWOR654)
488            return Collections.emptyList();
489        }
490        for ( String parameter : signatureArguments.split( "," ) )
491        {
492            // strip off argument name, only type is relevant
493            String canonicalParameter = parameter.trim();
494            int spaceIndex = canonicalParameter.indexOf( ' ' );
495            final String typeName;
496            if ( spaceIndex > 0 )
497            {
498                typeName = canonicalParameter.substring( 0, spaceIndex ).trim();
499            }
500            else
501            {
502                typeName = canonicalParameter;
503            }
504            if ( !typeName.isEmpty() )
505            {
506                String rawTypeName = getRawTypeName( typeName );
507                // already check here for unresolvable types due to https://github.com/paul-hammant/qdox/issues/111
508                if ( typeResolver.resolveType( rawTypeName ) == null )
509                {
510                    throw new IllegalArgumentException( "Found unresolvable method argument type in " + member );
511                }
512                TypeDef typeDef = new TypeDef( getRawTypeName( typeName ) );
513                int dimensions = getDimensions( typeName );
514                JavaType javaType = TypeAssembler.createUnresolved( typeDef, dimensions, typeResolver );
515
516                parameterTypes.add( javaType );
517            }
518        }
519        return parameterTypes;
520    }
521
522    private static int getDimensions( String type )
523    {
524        return (int) type.chars().filter( ch -> ch == '[' ).count();
525    }
526
527    private static String getRawTypeName( String typeName )
528    {
529        // strip dimensions
530        int indexOfOpeningBracket = typeName.indexOf( '[' );
531        if ( indexOfOpeningBracket >= 0 )
532        {
533            return typeName.substring( 0, indexOfOpeningBracket );
534        }
535        else
536        {
537            return typeName;
538        }
539    }
540
541    private static String getMethodName( String member )
542    {
543        // name is separated from arguments either by '(' or spans the full member
544        int indexOfOpeningParentheses = member.indexOf( '(' );
545        if ( indexOfOpeningParentheses == -1 )
546        {
547            return member;
548        }
549        else
550        {
551            return member.substring( 0, indexOfOpeningParentheses );
552        }
553    }
554
555    @SuppressWarnings( "unchecked" )
556    @Override
557    public <T> T setAttribute( String name, T value )
558    {
559        return (T) attributes.put( name, value );
560    }
561
562    @SuppressWarnings( "unchecked" )
563    @Override
564    public <T> T getAttribute( String name, Class<T> clazz, T defaultValue )
565    {
566        return (T) attributes.getOrDefault( name, defaultValue );
567    }
568}