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