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