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 }