1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  package org.apache.maven.tools.plugin.javadoc;
20  
21  import java.io.BufferedReader;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.Reader;
26  import java.net.MalformedURLException;
27  import java.net.SocketTimeoutException;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.util.AbstractMap;
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.Collections;
35  import java.util.EnumMap;
36  import java.util.EnumSet;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Objects;
41  import java.util.Optional;
42  import java.util.function.BiFunction;
43  import java.util.regex.Pattern;
44  
45  import org.apache.http.HttpHeaders;
46  import org.apache.http.HttpHost;
47  import org.apache.http.HttpResponse;
48  import org.apache.http.HttpStatus;
49  import org.apache.http.auth.AuthScope;
50  import org.apache.http.auth.Credentials;
51  import org.apache.http.auth.UsernamePasswordCredentials;
52  import org.apache.http.client.CredentialsProvider;
53  import org.apache.http.client.config.CookieSpecs;
54  import org.apache.http.client.config.RequestConfig;
55  import org.apache.http.client.methods.HttpGet;
56  import org.apache.http.client.protocol.HttpClientContext;
57  import org.apache.http.config.Registry;
58  import org.apache.http.config.RegistryBuilder;
59  import org.apache.http.conn.socket.ConnectionSocketFactory;
60  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
61  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
62  import org.apache.http.impl.client.BasicCredentialsProvider;
63  import org.apache.http.impl.client.CloseableHttpClient;
64  import org.apache.http.impl.client.HttpClientBuilder;
65  import org.apache.http.impl.client.HttpClients;
66  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
67  import org.apache.http.message.BasicHeader;
68  import org.apache.maven.settings.Proxy;
69  import org.apache.maven.settings.Settings;
70  import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
71  import org.apache.maven.wagon.proxy.ProxyInfo;
72  import org.apache.maven.wagon.proxy.ProxyUtils;
73  import org.codehaus.plexus.util.StringUtils;
74  
75  
76  
77  
78  
79  class JavadocSite {
80      private static final String PREFIX_MODULE = "module:";
81  
82      final URI baseUri;
83  
84      final Settings settings;
85  
86      final Map<String, String> containedPackageNamesAndModules; 
87  
88      final boolean requireModuleNameInPath;
89  
90      static final EnumMap<
91                      FullyQualifiedJavadocReference.MemberType, EnumSet<JavadocLinkGenerator.JavadocToolVersionRange>>
92              VERSIONS_PER_TYPE;
93  
94      static {
95          VERSIONS_PER_TYPE = new EnumMap<>(FullyQualifiedJavadocReference.MemberType.class);
96          VERSIONS_PER_TYPE.put(
97                  MemberType.CONSTRUCTOR,
98                  EnumSet.of(
99                          JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
100                         JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9,
101                         JavadocLinkGenerator.JavadocToolVersionRange.JDK10_OR_HIGHER));
102         VERSIONS_PER_TYPE.put(
103                 MemberType.METHOD,
104                 EnumSet.of(
105                         JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
106                         JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9,
107                         JavadocLinkGenerator.JavadocToolVersionRange.JDK10_OR_HIGHER));
108         VERSIONS_PER_TYPE.put(
109                 MemberType.FIELD,
110                 EnumSet.of(
111                         JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
112                         JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9));
113     }
114 
115     JavadocLinkGenerator.JavadocToolVersionRange version; 
116 
117     
118 
119 
120 
121 
122 
123     JavadocSite(final URI url, final Settings settings) throws IOException {
124         Map<String, String> containedPackageNamesAndModules;
125         boolean requireModuleNameInPath = false;
126         try {
127             
128             containedPackageNamesAndModules = getPackageListWithModules(url.resolve("package-list"), settings);
129         } catch (FileNotFoundException e) {
130             try {
131                 
132                 containedPackageNamesAndModules = getPackageListWithModules(url.resolve("element-list"), settings);
133 
134                 Optional<String> firstModuleName = containedPackageNamesAndModules.values().stream()
135                         .filter(StringUtils::isNotBlank)
136                         .findFirst();
137                 if (firstModuleName.isPresent()) {
138                     
139                     try (Reader reader = getReader(
140                             url.resolve(firstModuleName.get() + "/module-summary.html")
141                                     .toURL(),
142                             null)) {
143                         requireModuleNameInPath = true;
144                     } catch (IOException ioe) {
145                         
146                     }
147                 }
148             } catch (FileNotFoundException e2) {
149                 throw new IOException("Found neither 'package-list' nor 'element-list' below url " + url
150                         + ". The given URL does probably not specify the root of a javadoc site or has been generated with"
151                         + " javadoc 1.2 or older.");
152             }
153         }
154         this.containedPackageNamesAndModules = containedPackageNamesAndModules;
155         this.baseUri = url;
156         this.settings = settings;
157         this.version = null;
158         this.requireModuleNameInPath = requireModuleNameInPath;
159     }
160 
161     
162 
163     JavadocSite(final URI url, JavadocLinkGenerator.JavadocToolVersionRange version, boolean requireModuleNameInPath) {
164         Objects.requireNonNull(url);
165         this.baseUri = url;
166         Objects.requireNonNull(version);
167         this.version = version;
168         this.settings = null;
169         this.containedPackageNamesAndModules = Collections.emptyMap();
170         this.requireModuleNameInPath = requireModuleNameInPath;
171     }
172 
173     static Map<String, String> getPackageListWithModules(final URI url, final Settings settings) throws IOException {
174         Map<String, String> containedPackageNamesAndModules = new HashMap<>();
175         try (BufferedReader reader = getReader(url.toURL(), settings)) {
176             String line;
177             String module = null;
178             while ((line = reader.readLine()) != null) {
179                 
180                 if (line.startsWith(PREFIX_MODULE)) {
181                     module = line.substring(PREFIX_MODULE.length());
182                 } else {
183                     containedPackageNamesAndModules.put(line, module);
184                 }
185             }
186             return containedPackageNamesAndModules;
187         }
188     }
189 
190     static boolean findLineContaining(final URI url, final Settings settings, Pattern pattern) throws IOException {
191         try (BufferedReader reader = getReader(url.toURL(), settings)) {
192             return reader.lines().anyMatch(pattern.asPredicate());
193         }
194     }
195 
196     public URI getBaseUri() {
197         return baseUri;
198     }
199 
200     public boolean hasEntryFor(Optional<String> moduleName, Optional<String> packageName) {
201         if (containedPackageNamesAndModules.isEmpty()) {
202             throw new UnsupportedOperationException(
203                     "Operation hasEntryFor(...) is not supported for offline " + "javadoc sites");
204         }
205         if (packageName.isPresent()) {
206             if (moduleName.isPresent()) {
207                 String actualModuleName = containedPackageNamesAndModules.get(packageName.get());
208                 if (!moduleName.get().equals(actualModuleName)) {
209                     return false;
210                 }
211             } else {
212                 if (!containedPackageNamesAndModules.containsKey(packageName.get())) {
213                     return false;
214                 }
215             }
216         } else if (moduleName.isPresent()) {
217             if (!containedPackageNamesAndModules.containsValue(moduleName.get())) {
218                 return false;
219             }
220         } else {
221             throw new IllegalArgumentException("Either module name or package name must be set!");
222         }
223         return true;
224     }
225 
226     
227 
228 
229 
230 
231 
232 
233     public URI createLink(String packageName, String className) {
234         try {
235             if (className.endsWith("[]")) {
236                 
237                 className = className.substring(0, className.length() - 2);
238             }
239             return createLink(baseUri, Optional.empty(), Optional.of(packageName), Optional.of(className));
240         } catch (URISyntaxException e) {
241             throw new IllegalArgumentException("Could not create link for " + packageName + "." + className, e);
242         }
243     }
244 
245     
246 
247 
248 
249 
250 
251 
252     static Map.Entry<String, String> getPackageAndClassName(String binaryName) {
253         
254         int indexOfDollar = binaryName.indexOf('$');
255         int indexOfDotBetweenPackageAndClass;
256         if (indexOfDollar >= 0) {
257             
258             if (Character.isDigit(binaryName.charAt(indexOfDollar + 1))) {
259                 
260                 throw new IllegalArgumentException(
261                         "Can only resolve binary names of member classes, " + "but not local or anonymous classes");
262             }
263             
264             indexOfDotBetweenPackageAndClass = binaryName.lastIndexOf('.', indexOfDollar);
265             
266             binaryName = binaryName.replace('$', '.');
267         } else {
268             indexOfDotBetweenPackageAndClass = binaryName.lastIndexOf('.');
269         }
270         if (indexOfDotBetweenPackageAndClass < 0) {
271             throw new IllegalArgumentException("Resolving primitives is not supported. "
272                     + "Binary name must contain at least one dot: " + binaryName);
273         }
274         if (indexOfDotBetweenPackageAndClass == binaryName.length() - 1) {
275             throw new IllegalArgumentException("Invalid binary name ending with a dot: " + binaryName);
276         }
277         String packageName = binaryName.substring(0, indexOfDotBetweenPackageAndClass);
278         String className = binaryName.substring(indexOfDotBetweenPackageAndClass + 1, binaryName.length());
279         return new AbstractMap.SimpleEntry<>(packageName, className);
280     }
281 
282     
283 
284 
285 
286 
287 
288 
289     public URI createLink(FullyQualifiedJavadocReference javadocReference) throws IllegalArgumentException {
290         final Optional<String> moduleName;
291         if (!requireModuleNameInPath) {
292             moduleName = Optional.empty();
293         } else {
294             moduleName = Optional.ofNullable(javadocReference
295                     .getModuleName()
296                     .orElse(containedPackageNamesAndModules.get(
297                             javadocReference.getPackageName().orElse(null))));
298         }
299         return createLink(javadocReference, baseUri, this::appendMemberAsFragment, moduleName);
300     }
301 
302     static URI createLink(
303             FullyQualifiedJavadocReference javadocReference,
304             URI baseUri,
305             BiFunction<URI, FullyQualifiedJavadocReference, URI> fragmentAppender,
306             Optional<String> resolvedModuleName)
307             throws IllegalArgumentException {
308         try {
309             URI uri = createLink(
310                     baseUri,
311                     javadocReference.getModuleName().isPresent()
312                             ? javadocReference.getModuleName()
313                             : resolvedModuleName,
314                     javadocReference.getPackageName(),
315                     javadocReference.getClassName());
316             return fragmentAppender.apply(uri, javadocReference);
317         } catch (URISyntaxException e) {
318             throw new IllegalArgumentException("Could not create link for " + javadocReference, e);
319         }
320     }
321 
322     static URI createLink(
323             URI baseUri, Optional<String> moduleName, Optional<String> packageName, Optional<String> className)
324             throws URISyntaxException {
325         StringBuilder link = new StringBuilder();
326         if (moduleName.isPresent()) {
327             link.append(moduleName.get() + "/");
328         }
329         if (packageName.isPresent()) {
330             link.append(packageName.get().replace('.', '/'));
331         }
332         if (!className.isPresent()) {
333             if (packageName.isPresent()) {
334                 link.append("/package-summary.html");
335             } else if (moduleName.isPresent()) {
336                 link.append("/module-summary.html");
337             }
338         } else {
339             link.append('/').append(className.get()).append(".html");
340         }
341         return baseUri.resolve(new URI(null, link.toString(), null));
342     }
343 
344     URI appendMemberAsFragment(URI url, FullyQualifiedJavadocReference reference) {
345         try {
346             return appendMemberAsFragment(url, reference.getMember(), reference.getMemberType());
347         } catch (URISyntaxException | IOException e) {
348             throw new IllegalArgumentException("Could not create link for " + reference, e);
349         }
350     }
351 
352     
353     
354 
355 
356 
357 
358 
359 
360 
361 
362 
363 
364 
365 
366 
367 
368 
369 
370 
371 
372 
373     
374     URI appendMemberAsFragment(URI url, Optional<String> optionalMember, Optional<MemberType> optionalMemberType)
375             throws URISyntaxException, IOException {
376         if (!optionalMember.isPresent()) {
377             return url;
378         }
379         MemberType memberType = optionalMemberType.orElse(null);
380         final String member = optionalMember.get();
381         String fragment = member;
382         if (version != null) {
383             fragment = getFragmentForMember(version, member, memberType == MemberType.CONSTRUCTOR);
384         } else {
385             
386             for (JavadocLinkGenerator.JavadocToolVersionRange potentialVersion : VERSIONS_PER_TYPE.get(memberType)) {
387                 fragment = getFragmentForMember(potentialVersion, member, memberType == MemberType.CONSTRUCTOR);
388                 if (findAnchor(url, fragment)) {
389                     
390                     if (memberType == MemberType.CONSTRUCTOR || memberType == MemberType.METHOD) {
391                         version = potentialVersion;
392                     }
393                     break;
394                 }
395             }
396         }
397         return new URI(url.getScheme(), url.getSchemeSpecificPart(), fragment);
398     }
399 
400     
401 
402 
403 
404 
405 
406 
407 
408     static String getFragmentForMember(
409             JavadocLinkGenerator.JavadocToolVersionRange version, String member, boolean isConstructor) {
410         String fragment = member;
411         switch (version) {
412             case JDK7_OR_LOWER:
413                 
414                 fragment = fragment.replace(",", ", ");
415                 break;
416             case JDK8_OR_9:
417                 
418                 fragment = fragment.replace("[]", ":A");
419                 
420                 fragment = fragment.replace('(', '-').replace(')', '-').replace(',', '-');
421                 break;
422             case JDK10_OR_HIGHER:
423                 if (isConstructor) {
424                     int indexOfOpeningParenthesis = fragment.indexOf('(');
425                     if (indexOfOpeningParenthesis >= 0) {
426                         fragment = "<init>" + fragment.substring(indexOfOpeningParenthesis);
427                     } else {
428                         fragment = "<init>";
429                     }
430                 }
431                 break;
432             default:
433                 throw new IllegalArgumentException("No valid version range given");
434         }
435         return fragment;
436     }
437 
438     boolean findAnchor(URI uri, String anchorNameOrId) throws MalformedURLException, IOException {
439         return findLineContaining(uri, settings, getAnchorPattern(anchorNameOrId));
440     }
441 
442     static Pattern getAnchorPattern(String anchorNameOrId) {
443         
444         return Pattern.compile(".*(name|NAME|id)=\\\"" + Pattern.quote(anchorNameOrId) + "\\\"");
445     }
446 
447     
448     
449     
450     
451     
452     
453     
454     public static final int DEFAULT_TIMEOUT = 2000;
455 
456     
457 
458 
459 
460 
461 
462 
463 
464 
465     private static CloseableHttpClient createHttpClient(Settings settings, URL url) {
466         HttpClientBuilder builder = HttpClients.custom();
467 
468         Registry<ConnectionSocketFactory> csfRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
469                 .register("http", PlainConnectionSocketFactory.getSocketFactory())
470                 .register("https", SSLConnectionSocketFactory.getSystemSocketFactory())
471                 .build();
472 
473         builder.setConnectionManager(new PoolingHttpClientConnectionManager(csfRegistry));
474         builder.setDefaultRequestConfig(RequestConfig.custom()
475                 .setSocketTimeout(DEFAULT_TIMEOUT)
476                 .setConnectTimeout(DEFAULT_TIMEOUT)
477                 .setCircularRedirectsAllowed(true)
478                 .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
479                 .build());
480 
481         
482         builder.setUserAgent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
483 
484         
485         builder.setDefaultHeaders(Arrays.asList(new BasicHeader(HttpHeaders.ACCEPT, "*/*")));
486 
487         if (settings != null && settings.getActiveProxy() != null) {
488             Proxy activeProxy = settings.getActiveProxy();
489 
490             ProxyInfo proxyInfo = new ProxyInfo();
491             proxyInfo.setNonProxyHosts(activeProxy.getNonProxyHosts());
492 
493             if (StringUtils.isNotEmpty(activeProxy.getHost())
494                     && (url == null || !ProxyUtils.validateNonProxyHosts(proxyInfo, url.getHost()))) {
495                 HttpHost proxy = new HttpHost(activeProxy.getHost(), activeProxy.getPort());
496                 builder.setProxy(proxy);
497 
498                 if (StringUtils.isNotEmpty(activeProxy.getUsername()) && activeProxy.getPassword() != null) {
499                     Credentials credentials =
500                             new UsernamePasswordCredentials(activeProxy.getUsername(), activeProxy.getPassword());
501 
502                     CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
503                     credentialsProvider.setCredentials(AuthScope.ANY, credentials);
504                     builder.setDefaultCredentialsProvider(credentialsProvider);
505                 }
506             }
507         }
508         return builder.build();
509     }
510 
511     static BufferedReader getReader(URL url, Settings settings) throws IOException {
512         BufferedReader reader = null;
513 
514         if ("file".equals(url.getProtocol())) {
515             
516             reader = new BufferedReader(new InputStreamReader(url.openStream()));
517         } else {
518             
519             final CloseableHttpClient httpClient = createHttpClient(settings, url);
520 
521             final HttpGet httpMethod = new HttpGet(url.toString());
522 
523             HttpResponse response;
524             HttpClientContext httpContext = HttpClientContext.create();
525             try {
526                 response = httpClient.execute(httpMethod, httpContext);
527             } catch (SocketTimeoutException e) {
528                 
529                 response = httpClient.execute(httpMethod, httpContext);
530             }
531 
532             int status = response.getStatusLine().getStatusCode();
533             if (status != HttpStatus.SC_OK) {
534                 throw new FileNotFoundException(
535                         "Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + ".");
536             } else {
537                 int pos = url.getPath().lastIndexOf('/');
538                 List<URI> redirects = httpContext.getRedirectLocations();
539                 if (pos >= 0 && isNotEmpty(redirects)) {
540                     URI location = redirects.get(redirects.size() - 1);
541                     String suffix = url.getPath().substring(pos);
542                     
543                     if (!location.getPath().endsWith(suffix)) {
544                         throw new FileNotFoundException(url.toExternalForm() + " redirects to "
545                                 + location.toURL().toExternalForm() + ".");
546                     }
547                 }
548             }
549 
550             
551             reader = new BufferedReader(
552                     new InputStreamReader(response.getEntity().getContent())) {
553                 @Override
554                 public void close() throws IOException {
555                     super.close();
556 
557                     if (httpMethod != null) {
558                         httpMethod.releaseConnection();
559                     }
560                     if (httpClient != null) {
561                         httpClient.close();
562                     }
563                 }
564             };
565         }
566 
567         return reader;
568     }
569 
570     
571 
572 
573 
574 
575 
576     public static boolean isNotEmpty(final Collection<?> collection) {
577         return collection != null && !collection.isEmpty();
578     }
579 }