View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.tools.plugin.javadoc;
20  
21  import java.io.BufferedReader;
22  import java.io.IOException;
23  import java.net.URI;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Optional;
31  import java.util.regex.Pattern;
32  
33  import org.apache.maven.settings.Settings;
34  import org.codehaus.plexus.languages.java.version.JavaVersion;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  /**
39   * Generates links for elements (packages, classes, fields, constructors, methods) in external
40   * and/or an internal (potentially not yet existing) javadoc site.
41   * The external site must be accessible for it to be considered due to the different fragment formats.
42   */
43  public class JavadocLinkGenerator {
44      /**
45       * Javadoc tool version ranges whose generated sites expose different link formats.
46       *
47       */
48      public enum JavadocToolVersionRange {
49          JDK7_OR_LOWER(null, JavaVersion.parse("1.8")),
50          JDK8_OR_9(JavaVersion.parse("1.8"), JavaVersion.parse("10")),
51          JDK10_OR_HIGHER(JavaVersion.parse("10"), null);
52  
53          // upper bound is exclusive, lower bound inclusive (null means unlimited)
54          private final JavaVersion lowerBound;
55          private final JavaVersion upperBound;
56  
57          JavadocToolVersionRange(JavaVersion lowerBound, JavaVersion upperBound) {
58              this.lowerBound = lowerBound;
59              this.upperBound = upperBound;
60          }
61  
62          static JavadocToolVersionRange findMatch(JavaVersion javadocVersion) {
63              for (JavadocToolVersionRange range : values()) {
64                  if ((range.lowerBound == null || javadocVersion.isAtLeast(range.lowerBound))
65                          && (range.upperBound == null || javadocVersion.isBefore(range.upperBound))) {
66                      return range;
67                  }
68              }
69              throw new IllegalArgumentException("Found no matching javadoc tool version range for " + javadocVersion);
70          }
71      }
72  
73      private static final Logger LOG = LoggerFactory.getLogger(JavadocLinkGenerator.class);
74      private final List<JavadocSite> externalJavadocSites;
75      private final JavadocSite internalJavadocSite; // may be null
76  
77      /**
78       * Constructor for an offline internal site only.
79       *
80       * @param internalJavadocSiteUrl the url of the javadoc generated website
81       * @param internalJavadocVersion the version of javadoc with which the internal site from
82       *                               {@code internalJavadocSiteUrl} has been generated
83       */
84      public JavadocLinkGenerator(URI internalJavadocSiteUrl, String internalJavadocVersion) {
85          this(internalJavadocSiteUrl, internalJavadocVersion, Collections.emptyList(), null);
86      }
87  
88      /**
89       * Constructor for online external sites only.
90       *
91       * @param externalJavadocSiteUrls
92       * @param settings
93       */
94      public JavadocLinkGenerator(List<URI> externalJavadocSiteUrls, Settings settings) {
95          this(null, null, externalJavadocSiteUrls, settings);
96      }
97  
98      /**
99       * 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 }