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