001package org.apache.maven.tools.plugin.javadoc;
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.io.BufferedReader;
023
024import java.io.IOException;
025import java.net.URI;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import java.util.regex.Pattern;
034
035import org.apache.maven.settings.Settings;
036import org.codehaus.plexus.languages.java.version.JavaVersion;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * Generates links for elements (packages, classes, fields, constructors, methods) in external
042 * and/or an internal (potentially not yet existing) javadoc site.
043 * The external site must be accessible for it to be considered due to the different fragment formats.
044 */
045public class JavadocLinkGenerator
046{
047    /**
048     * Javadoc tool version ranges whose generated sites expose different link formats.
049     *
050     */
051    public enum JavadocToolVersionRange
052    {
053        JDK7_OR_LOWER( null, JavaVersion.parse( "1.8" ) ),
054        JDK8_OR_9( JavaVersion.parse( "1.8" ), JavaVersion.parse( "10" ) ),
055        JDK10_OR_HIGHER( JavaVersion.parse( "10" ), null );
056        
057        // upper bound is exclusive, lower bound inclusive (null means unlimited)
058        private final JavaVersion lowerBound;
059        private final JavaVersion upperBound;
060        JavadocToolVersionRange( JavaVersion lowerBound, JavaVersion upperBound )
061        {
062            this.lowerBound = lowerBound;
063            this.upperBound = upperBound;
064        }
065
066        static JavadocToolVersionRange findMatch( JavaVersion javadocVersion )
067        {
068            for ( JavadocToolVersionRange range : values() )
069            {
070                if ( ( range.lowerBound == null || javadocVersion.isAtLeast( range.lowerBound ) )
071                     && ( range.upperBound == null || javadocVersion.isBefore( range.upperBound ) ) )
072                {
073                    return range;
074                }
075            }
076            throw new IllegalArgumentException( "Found no matching javadoc tool version range for " + javadocVersion );
077        }
078    }
079
080    private static final Logger LOG = LoggerFactory.getLogger( JavadocLinkGenerator.class );
081    private final List<JavadocSite> externalJavadocSites;
082    private final JavadocSite internalJavadocSite; // may be null
083
084    /**
085     * Constructor for an offline internal site only.
086     * 
087     * @param internalJavadocSiteUrl the url of the javadoc generated website
088     * @param internalJavadocVersion the version of javadoc with which the internal site from
089     *                               {@code internalJavadocSiteUrl} has been generated
090     */
091    public JavadocLinkGenerator( URI internalJavadocSiteUrl,
092                                 String internalJavadocVersion )
093    {
094        this( internalJavadocSiteUrl, internalJavadocVersion, Collections.emptyList(), null );
095    }
096
097    /**
098     * Constructor for online external sites only.
099     * 
100     * @param externalJavadocSiteUrls
101     * @param settings
102     */
103    public JavadocLinkGenerator( List<URI> externalJavadocSiteUrls, Settings settings )
104    {
105        this( null, null, externalJavadocSiteUrls, settings );
106    }
107
108    /**
109     * Constructor for both an internal (offline) and external (online) sites.
110     * 
111     * @param internalJavadocSiteUrl
112     * @param internalJavadocVersion
113     * @param externalJavadocSiteUrls
114     * @param settings
115     */
116    public JavadocLinkGenerator( URI internalJavadocSiteUrl,
117                                 String internalJavadocVersion,
118                                 List<URI> externalJavadocSiteUrls, Settings settings )
119    {
120        if ( internalJavadocSiteUrl != null )
121        {
122            // resolve version
123            JavaVersion javadocVersion = JavaVersion.parse( internalJavadocVersion );
124            internalJavadocSite = new JavadocSite( internalJavadocSiteUrl,
125                                                   JavadocToolVersionRange.findMatch( javadocVersion ),
126                                                   false );
127        }
128        else
129        {
130            internalJavadocSite = null;
131        }
132        if ( externalJavadocSiteUrls != null )
133        {
134            externalJavadocSites = new ArrayList<>( externalJavadocSiteUrls.size() );
135            for ( URI siteUrl : externalJavadocSiteUrls  )
136            {
137                try
138                {
139                    externalJavadocSites.add( new JavadocSite( siteUrl, settings ) );
140                }
141                catch ( IOException e )
142                {
143                    LOG.warn( "Could not use {} as base URL: {}", siteUrl, e.getMessage(), e );
144                }
145            }
146        }
147        else
148        {
149            externalJavadocSites = Collections.emptyList();
150        }
151        if ( internalJavadocSite == null && externalJavadocSites.isEmpty() )
152        {
153            throw new IllegalArgumentException( "Either internal or at least one accessible external javadoc "
154                                                + "URLs must be given!" );
155        }
156    }
157
158    /**
159     * Generates a (deep-)link to a HTML page in any of the sites given to the constructor.
160     * The link is not validated (i.e. might point to a non-existing page).
161     * Only uses the offline site for references returning {@code false} for
162     * {@link FullyQualifiedJavadocReference#isExternal()}.
163     * @param javadocReference
164     * @return the (deep-) link towards a javadoc page
165     * @throws IllegalArgumentException in case no javadoc link could be generated for the given reference
166     * @throws IllegalStateException in case no javadoc source sites have been configured
167     */
168    public URI createLink( FullyQualifiedJavadocReference javadocReference )
169    {
170        if ( !javadocReference.isExternal() && internalJavadocSite != null )
171        {
172            return internalJavadocSite.createLink( javadocReference );
173        }
174        else
175        {
176            JavadocSite javadocSite = externalJavadocSites.stream()
177                .filter( base -> base.hasEntryFor( javadocReference.getModuleName(),
178                                                   javadocReference.getPackageName() ) )
179                .findFirst().orElseThrow( () -> new IllegalArgumentException( "Found no javadoc site for "
180                + javadocReference ) );
181            return javadocSite.createLink( javadocReference );
182        }
183    }
184
185    /**
186     * Generates a (deep-)link to a HTML page in any of the sites given to the constructor.
187     * The link is not validated (i.e. might point to a non-existing page).
188     * Preferably resolves from the online sites if they provide the given package.
189     * @param binaryName a binary name according to 
190     * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1">JLS 13.1</a>
191     * @return the (deep-) link towards a javadoc page
192     * @throws IllegalArgumentException in case no javadoc link could be generated for the given name
193     */
194    public URI createLink( String binaryName )
195    {
196        Map.Entry<String, String> packageAndClassName = JavadocSite.getPackageAndClassName( binaryName );
197        // first check external links, otherwise assume internal link
198        JavadocSite javadocSite = externalJavadocSites.stream()
199                        .filter( base -> base.hasEntryFor( Optional.empty(),
200                                                           Optional.of( packageAndClassName.getKey() ) ) )
201                        .findFirst().orElse( null );
202        if ( javadocSite == null )
203        {
204            if ( internalJavadocSite != null )
205            {
206                javadocSite = internalJavadocSite;
207            }
208            else
209            {
210                throw new IllegalArgumentException( "Found no javadoc site for " + binaryName );
211            }
212        }
213        return javadocSite.createLink( packageAndClassName.getKey(), packageAndClassName.getValue() );
214    }
215
216    public URI getInternalJavadocSiteBaseUrl()
217    {
218        if ( internalJavadocSite == null )
219        {
220            throw new IllegalStateException( "Could not get docroot of internal javadoc as it hasn't been set" );
221        }
222        return internalJavadocSite.getBaseUri();
223    }
224    
225    /**
226     * Checks if a given link is valid. For absolute links uses the underling {@link java.net.HttpURLConnection},
227     * otherwise checks for existence of the file on the filesystem.
228     * 
229     * @param url the url to check
230     * @param baseDirectory the base directory to which relative file URLs refer
231     * @return {@code true} in case the given link is valid otherwise {@code false}
232     */
233    public static boolean isLinkValid( URI url, Path baseDirectory )
234    {
235        if ( url.isAbsolute() )
236        {
237            try ( BufferedReader reader = JavadocSite.getReader( url.toURL(), null ) )
238            {
239                if ( url.getFragment() != null )
240                {
241                    Pattern pattern = JavadocSite.getAnchorPattern( url.getFragment() );
242                    if ( reader.lines().noneMatch( pattern.asPredicate() ) )
243                    {
244                        return false;
245                    }
246                }
247            }
248            catch ( IOException e )
249            {
250                return false;
251            }
252            return true;
253        }
254        else
255        {
256            Path file = baseDirectory.resolve( url.getPath() );
257            boolean exists = Files.exists( file );
258            if ( !exists )
259            {
260                LOG.debug( "Could not find file given through '{}' in resolved path '{}'", url, file );
261            }
262            return exists;
263        }
264    }
265}