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 return createLink(javadocReference, baseUri, fragmentAppender, Optional.empty());
307 }
308
309 static URI createLink(
310 FullyQualifiedJavadocReference javadocReference,
311 URI baseUri,
312 BiFunction<URI, FullyQualifiedJavadocReference, URI> fragmentAppender,
313 Optional<String> pathPrefix)
314 throws IllegalArgumentException {
315 try {
316 URI uri = createLink(
317 baseUri,
318 javadocReference.getModuleName(),
319 javadocReference.getPackageName(),
320 javadocReference.getClassName());
321 return fragmentAppender.apply(uri, javadocReference);
322 } catch (URISyntaxException e) {
323 throw new IllegalArgumentException("Could not create link for " + javadocReference, e);
324 }
325 }
326
327 static URI createLink(
328 URI baseUri, Optional<String> moduleName, Optional<String> packageName, Optional<String> className)
329 throws URISyntaxException {
330 StringBuilder link = new StringBuilder();
331 if (moduleName.isPresent()) {
332 link.append(moduleName.get() + "/");
333 }
334 if (packageName.isPresent()) {
335 link.append(packageName.get().replace('.', '/'));
336 }
337 if (!className.isPresent()) {
338 if (packageName.isPresent()) {
339 link.append("/package-summary.html");
340 } else if (moduleName.isPresent()) {
341 link.append("/module-summary.html");
342 }
343 } else {
344 link.append('/').append(className.get()).append(".html");
345 }
346 return baseUri.resolve(new URI(null, link.toString(), null));
347 }
348
349 URI appendMemberAsFragment(URI url, FullyQualifiedJavadocReference reference) {
350 try {
351 return appendMemberAsFragment(url, reference.getMember(), reference.getMemberType());
352 } catch (URISyntaxException | IOException e) {
353 throw new IllegalArgumentException("Could not create link for " + reference, e);
354 }
355 }
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379 URI appendMemberAsFragment(URI url, Optional<String> optionalMember, Optional<MemberType> optionalMemberType)
380 throws URISyntaxException, IOException {
381 if (!optionalMember.isPresent()) {
382 return url;
383 }
384 MemberType memberType = optionalMemberType.orElse(null);
385 final String member = optionalMember.get();
386 String fragment = member;
387 if (version != null) {
388 fragment = getFragmentForMember(version, member, memberType == MemberType.CONSTRUCTOR);
389 } else {
390
391 for (JavadocLinkGenerator.JavadocToolVersionRange potentialVersion : VERSIONS_PER_TYPE.get(memberType)) {
392 fragment = getFragmentForMember(potentialVersion, member, memberType == MemberType.CONSTRUCTOR);
393 if (findAnchor(url, fragment)) {
394
395 if (memberType == MemberType.CONSTRUCTOR || memberType == MemberType.METHOD) {
396 version = potentialVersion;
397 }
398 break;
399 }
400 }
401 }
402 return new URI(url.getScheme(), url.getSchemeSpecificPart(), fragment);
403 }
404
405
406
407
408
409
410
411
412
413 static String getFragmentForMember(
414 JavadocLinkGenerator.JavadocToolVersionRange version, String member, boolean isConstructor) {
415 String fragment = member;
416 switch (version) {
417 case JDK7_OR_LOWER:
418
419 fragment = fragment.replace(",", ", ");
420 break;
421 case JDK8_OR_9:
422
423 fragment = fragment.replace("[]", ":A");
424
425 fragment = fragment.replace('(', '-').replace(')', '-').replace(',', '-');
426 break;
427 case JDK10_OR_HIGHER:
428 if (isConstructor) {
429 int indexOfOpeningParenthesis = fragment.indexOf('(');
430 if (indexOfOpeningParenthesis >= 0) {
431 fragment = "<init>" + fragment.substring(indexOfOpeningParenthesis);
432 } else {
433 fragment = "<init>";
434 }
435 }
436 break;
437 default:
438 throw new IllegalArgumentException("No valid version range given");
439 }
440 return fragment;
441 }
442
443 boolean findAnchor(URI uri, String anchorNameOrId) throws MalformedURLException, IOException {
444 return findLineContaining(uri, settings, getAnchorPattern(anchorNameOrId));
445 }
446
447 static Pattern getAnchorPattern(String anchorNameOrId) {
448
449 return Pattern.compile(".*(name|NAME|id)=\\\"" + Pattern.quote(anchorNameOrId) + "\\\"");
450 }
451
452
453
454
455
456
457
458
459 public static final int DEFAULT_TIMEOUT = 2000;
460
461
462
463
464
465
466
467
468
469
470 private static CloseableHttpClient createHttpClient(Settings settings, URL url) {
471 HttpClientBuilder builder = HttpClients.custom();
472
473 Registry<ConnectionSocketFactory> csfRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
474 .register("http", PlainConnectionSocketFactory.getSocketFactory())
475 .register("https", SSLConnectionSocketFactory.getSystemSocketFactory())
476 .build();
477
478 builder.setConnectionManager(new PoolingHttpClientConnectionManager(csfRegistry));
479 builder.setDefaultRequestConfig(RequestConfig.custom()
480 .setSocketTimeout(DEFAULT_TIMEOUT)
481 .setConnectTimeout(DEFAULT_TIMEOUT)
482 .setCircularRedirectsAllowed(true)
483 .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
484 .build());
485
486
487 builder.setUserAgent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
488
489
490 builder.setDefaultHeaders(Arrays.asList(new BasicHeader(HttpHeaders.ACCEPT, "*/*")));
491
492 if (settings != null && settings.getActiveProxy() != null) {
493 Proxy activeProxy = settings.getActiveProxy();
494
495 ProxyInfo proxyInfo = new ProxyInfo();
496 proxyInfo.setNonProxyHosts(activeProxy.getNonProxyHosts());
497
498 if (StringUtils.isNotEmpty(activeProxy.getHost())
499 && (url == null || !ProxyUtils.validateNonProxyHosts(proxyInfo, url.getHost()))) {
500 HttpHost proxy = new HttpHost(activeProxy.getHost(), activeProxy.getPort());
501 builder.setProxy(proxy);
502
503 if (StringUtils.isNotEmpty(activeProxy.getUsername()) && activeProxy.getPassword() != null) {
504 Credentials credentials =
505 new UsernamePasswordCredentials(activeProxy.getUsername(), activeProxy.getPassword());
506
507 CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
508 credentialsProvider.setCredentials(AuthScope.ANY, credentials);
509 builder.setDefaultCredentialsProvider(credentialsProvider);
510 }
511 }
512 }
513 return builder.build();
514 }
515
516 static BufferedReader getReader(URL url, Settings settings) throws IOException {
517 BufferedReader reader = null;
518
519 if ("file".equals(url.getProtocol())) {
520
521 reader = new BufferedReader(new InputStreamReader(url.openStream()));
522 } else {
523
524 final CloseableHttpClient httpClient = createHttpClient(settings, url);
525
526 final HttpGet httpMethod = new HttpGet(url.toString());
527
528 HttpResponse response;
529 HttpClientContext httpContext = HttpClientContext.create();
530 try {
531 response = httpClient.execute(httpMethod, httpContext);
532 } catch (SocketTimeoutException e) {
533
534 response = httpClient.execute(httpMethod, httpContext);
535 }
536
537 int status = response.getStatusLine().getStatusCode();
538 if (status != HttpStatus.SC_OK) {
539 throw new FileNotFoundException(
540 "Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + ".");
541 } else {
542 int pos = url.getPath().lastIndexOf('/');
543 List<URI> redirects = httpContext.getRedirectLocations();
544 if (pos >= 0 && isNotEmpty(redirects)) {
545 URI location = redirects.get(redirects.size() - 1);
546 String suffix = url.getPath().substring(pos);
547
548 if (!location.getPath().endsWith(suffix)) {
549 throw new FileNotFoundException(url.toExternalForm() + " redirects to "
550 + location.toURL().toExternalForm() + ".");
551 }
552 }
553 }
554
555
556 reader = new BufferedReader(
557 new InputStreamReader(response.getEntity().getContent())) {
558 @Override
559 public void close() throws IOException {
560 super.close();
561
562 if (httpMethod != null) {
563 httpMethod.releaseConnection();
564 }
565 if (httpClient != null) {
566 httpClient.close();
567 }
568 }
569 };
570 }
571
572 return reader;
573 }
574
575
576
577
578
579
580
581 public static boolean isNotEmpty(final Collection<?> collection) {
582 return collection != null && !collection.isEmpty();
583 }
584 }