1 package org.apache.maven.verifier;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.security.NoSuchAlgorithmException;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34 import org.apache.maven.AbstractMavenComponent;
35 import org.apache.maven.MavenConstants;
36 import org.apache.maven.jelly.MavenJellyContext;
37 import org.apache.maven.project.Project;
38 import org.apache.maven.repository.Artifact;
39 import org.apache.maven.util.BootstrapDownloadMeter;
40 import org.apache.maven.util.ConsoleDownloadMeter;
41 import org.apache.maven.wagon.ConnectionException;
42 import org.apache.maven.wagon.ResourceDoesNotExistException;
43 import org.apache.maven.wagon.TransferFailedException;
44 import org.apache.maven.wagon.Wagon;
45 import org.apache.maven.wagon.authorization.AuthorizationException;
46 import org.apache.maven.wagon.events.TransferListener;
47 import org.apache.maven.wagon.observers.ChecksumObserver;
48 import org.apache.maven.wagon.providers.file.FileWagon;
49 import org.apache.maven.wagon.providers.http.HttpWagon;
50 import org.apache.maven.wagon.providers.ssh.jsch.SftpWagon;
51 import org.apache.maven.wagon.proxy.ProxyInfo;
52 import org.apache.maven.wagon.repository.Repository;
53 import org.codehaus.plexus.util.FileUtils;
54 import org.codehaus.plexus.util.StringUtils;
55
56 /**
57 * Make sure that everything that is required for the project to build
58 * successfully is present. We will start by looking at the dependencies
59 * and make sure they are all here before trying to compile.
60 *
61 * @author <a href="mailto:jason@zenplex.com">Jason van Zyl</a>
62 * @author <a href="mailto:vmassol@apache.org">Vincent Massol</a>
63 *
64 *
65 * @todo Separate out the local settings verifier because this only needs to be run
66 * once a session, but is currently being run during project verification so
67 * this is a big waste in the reactor for example.
68 */
69 public class DependencyVerifier
70 extends AbstractMavenComponent
71 {
72 /** LOGGER for debug output */
73 private static final Log LOGGER = LogFactory.getLog( DependencyVerifier.class );
74
75 /** List of failed deps. */
76 private List failedDependencies;
77
78 /** Local Repository verifier. */
79 private LocalSettingsVerifier localRepositoryVerifier;
80
81 private static Set resolvedArtifacts = new HashSet();
82
83 private ProxyInfo proxyInfo = null;
84
85 private TransferListener listener = null;
86
87 /**
88 * Default ctor.
89 * @param project the project to verify
90 */
91 public DependencyVerifier( Project project )
92 {
93 super( project );
94 failedDependencies = new ArrayList();
95 localRepositoryVerifier = new LocalSettingsVerifier( project );
96 MavenJellyContext context = getProject().getContext();
97
98 if ( context.getProxyHost() != null )
99 {
100 proxyInfo = new ProxyInfo();
101 proxyInfo.setHost( context.getProxyHost() );
102 try
103 {
104 proxyInfo.setPort( Integer.valueOf( context.getProxyPort() ).intValue() );
105 }
106 catch ( NumberFormatException e )
107 {
108 LOGGER.warn( "Ignoring invalid proxy port: '" + context.getProxyPort() + "'" );
109 }
110 proxyInfo.setUserName( context.getProxyUserName() );
111 proxyInfo.setPassword( context.getProxyPassword() );
112 proxyInfo.setNonProxyHosts( (String) context.getVariable( MavenConstants.PROXY_NONPROXYHOSTS ) );
113 proxyInfo.setNtlmHost( (String) context.getVariable( MavenConstants.PROXY_NTLM_HOST ) );
114 proxyInfo.setNtlmDomain( (String) context.getVariable( MavenConstants.PROXY_NTLM_DOMAIN ) );
115 }
116 String meterType = (String) context.getVariable( MavenConstants.DOWNLOAD_METER );
117 if ( meterType == null )
118 {
119 meterType = "console";
120 }
121
122 if ( "bootstrap".equals( meterType ) )
123 {
124 listener = new BootstrapDownloadMeter();
125 }
126 else if ( "console".equals( meterType ) )
127 {
128 listener = new ConsoleDownloadMeter();
129 }
130 }
131
132 /**
133 * Execute the verification process.
134 *
135 * @throws RepoConfigException If an error occurs while verifying basic maven settings.
136 * @throws UnsatisfiedDependencyException If there are unsatisfied dependencies.
137 * @throws ChecksumVerificationException if the download checksum doesn't match the calculated
138 */
139 public void verify()
140 throws RepoConfigException, UnsatisfiedDependencyException, ChecksumVerificationException
141 {
142 localRepositoryVerifier.verifyLocalRepository();
143 satisfyDependencies();
144 }
145
146 /**
147 * Clear the failed dependencies. Required when reusing the
148 * project verifier.
149 */
150 private void clearFailedDependencies()
151 {
152 failedDependencies.clear();
153 }
154
155 /**
156 * Check to see that all dependencies are present and if they are
157 * not then download them.
158 *
159 * @throws UnsatisfiedDependencyException If there are unsatisfied dependencies.
160 * @throws ChecksumVerificationException If checksums don't match.
161 */
162 private void satisfyDependencies()
163 throws UnsatisfiedDependencyException, ChecksumVerificationException
164 {
165
166 boolean remoteRepoEnabled = getProject().getContext().getRemoteRepositoryEnabled().booleanValue();
167
168
169 boolean online = getProject().getContext().getOnline().booleanValue();
170
171 if ( !remoteRepoEnabled )
172 {
173 LOGGER.warn( getMessage( "remote.repository.disabled.warning" ) );
174 }
175
176 clearFailedDependencies();
177
178 for ( Iterator i = getProject().getArtifacts().iterator(); i.hasNext(); )
179 {
180 Artifact artifact = (Artifact) i.next();
181
182 String path = artifact.getUrlPath();
183 if ( resolvedArtifacts.contains( path ) )
184 {
185 if ( LOGGER.isDebugEnabled() )
186 LOGGER.debug( "(previously resolved: " + path + ")" );
187 continue;
188 }
189 resolvedArtifacts.add( path );
190
191
192 if ( !artifact.exists() )
193 {
194 if ( LOGGER.isDebugEnabled() )
195 LOGGER.debug( "Artifact [" + path + "] not found in local repository" );
196 failedDependencies.add( artifact );
197 }
198 else if ( artifact.isSnapshot() && !Artifact.OVERRIDE_PATH.equals( artifact.getOverrideType() ) )
199 {
200
201
202
203
204
205
206
207
208 if ( online )
209 {
210 failedDependencies.add( artifact );
211 }
212 else
213 {
214 LOGGER.warn( getMessage( "offline.snapshot.warning", artifact.getName() ) );
215 }
216 }
217 }
218
219
220
221 if ( !failedDependencies.isEmpty() && remoteRepoEnabled && online )
222 {
223 getDependencies();
224 }
225
226
227
228
229
230
231 if ( !failedDependencies.isEmpty() )
232 {
233 throw new UnsatisfiedDependencyException( createUnsatisfiedDependenciesMessage() );
234 }
235 }
236
237 /**
238 * Create a message for the user stating the dependencies that are unsatisfied.
239 *
240 * @return The unsatisfied dependency message.
241 */
242 private String createUnsatisfiedDependenciesMessage()
243 {
244 StringBuffer message = new StringBuffer();
245
246 if ( failedDependencies.size() == 1 )
247 {
248 message.append( getMessage( "single.unsatisfied.dependency.error" ) );
249 }
250 else
251 {
252 message.append( getMessage( "multiple.unsatisfied.dependency.error" ) );
253 }
254
255 message.append( "\n" );
256
257 for ( Iterator i = failedDependencies.iterator(); i.hasNext(); )
258 {
259 Artifact artifact = (Artifact) i.next();
260 message.append( "- " + artifact.getDescription() );
261
262 String overrideType = artifact.getOverrideType();
263 if ( overrideType != Artifact.OVERRIDE_NONE )
264 {
265 if ( Artifact.OVERRIDE_VERSION.equals( overrideType ) )
266 {
267 message.append( "; version override doesn't exist: " + artifact.getDependency().getVersion() );
268 }
269 else if ( Artifact.OVERRIDE_PATH.equals( overrideType ) )
270 {
271 message.append( "; path override doesn't exist: " + artifact.getPath() );
272 }
273 }
274
275 String url = artifact.getDependency().getUrl();
276 if ( StringUtils.isNotEmpty( url ) )
277 {
278
279 message.append( " (" ).append( "try downloading from " ).append( url ).append( ")" );
280 }
281 message.append( "\n" );
282 }
283
284 return message.toString();
285 }
286
287 /**
288 * Try and retrieve the dependencies from the remote repository in
289 * order to satisfy the dependencies of the project.
290 * @throws ChecksumVerificationException If checksums don't match.
291 */
292 private void getDependencies()
293 throws ChecksumVerificationException
294 {
295 if ( LOGGER.isDebugEnabled() )
296 LOGGER.debug( "Getting failed dependencies: " + failedDependencies );
297
298
299 if ( failedDependencies.size() > 0 )
300 {
301 LOGGER.info( getMessage( "satisfy.project.message", getProject().getName() ) );
302 }
303
304 Artifact artifact;
305 Iterator i = failedDependencies.iterator();
306 while ( i.hasNext() )
307 {
308 artifact = (Artifact) i.next();
309
310
311
312
313
314
315 String overrideType = artifact.getOverrideType();
316 if ( Artifact.OVERRIDE_PATH.equals( overrideType ) )
317 {
318 continue;
319 }
320
321
322
323
324 File directory = artifact.getFile().getParentFile();
325
326 if ( !directory.exists() )
327 {
328 directory.mkdirs();
329 }
330
331 if ( getRemoteArtifact( artifact ) )
332 {
333
334
335 i.remove();
336 }
337 else
338 {
339 if ( artifact.exists() )
340 {
341
342
343 i.remove();
344 }
345
346
347
348
349 }
350 }
351 }
352
353 /**
354 * Retrieve a <code>remoteFile</code> from the maven remote repositories
355 * and store it at <code>localFile</code>
356 * @param artifact the artifact to retrieve from the repositories.
357 * @return true if the retrieval succeeds, false otherwise.
358 * @throws ChecksumVerificationException If checksums don't match.
359 */
360 private boolean getRemoteArtifact( Artifact artifact )
361 throws ChecksumVerificationException
362 {
363 boolean artifactFound = false;
364
365 int count = 0;
366
367 Iterator i = getProject().getContext().getMavenRepoRemote().iterator();
368 while(i.hasNext())
369 {
370 String remoteRepo = (String) i.next();
371
372 if ( artifact.isSnapshot() && artifact.exists() )
373 {
374 LOGGER.info( getMessage( "update.message" ) + " " + artifact.getDescription() + " from " + remoteRepo );
375 }
376 else
377 {
378 LOGGER
379 .info( getMessage( "download.message" ) + " " + artifact.getDescription() + " from " + remoteRepo );
380 }
381
382
383 Repository repository = new Repository( "repo" + count++, remoteRepo.trim() );
384
385 final Wagon wagon = new DefaultWagonFactory().getWagon( repository.getProtocol() );
386
387 if ( listener != null )
388 {
389 wagon.addTransferListener( listener );
390 }
391
392 ChecksumObserver md5ChecksumObserver = null;
393 ChecksumObserver sha1ChecksumObserver = null;
394 try
395 {
396 md5ChecksumObserver = new ChecksumObserver( "MD5" );
397 wagon.addTransferListener( md5ChecksumObserver );
398
399 sha1ChecksumObserver = new ChecksumObserver( "SHA-1" );
400 wagon.addTransferListener( sha1ChecksumObserver );
401 }
402 catch ( NoSuchAlgorithmException e )
403 {
404 throw new ChecksumVerificationException( "Unable to add checksum methods: " + e.getMessage(), e );
405 }
406
407 File destination = artifact.getFile();
408 String remotePath = artifact.getUrlPath();
409 File temp = new File( destination + ".tmp" );
410 temp.deleteOnExit();
411 boolean downloaded = false;
412
413 try
414 {
415 wagon.connect( repository, proxyInfo );
416
417 boolean firstRun = true;
418 boolean retry = true;
419
420
421
422
423 while ( firstRun || retry )
424 {
425
426 retry = false;
427
428 downloaded = wagon.getIfNewer( remotePath, temp, destination.lastModified() );
429 if ( !downloaded && firstRun )
430 {
431 LOGGER.info( getMessage( "skip.download.message" ) );
432 }
433
434 if ( downloaded )
435 {
436
437 if ( listener != null )
438 {
439 wagon.removeTransferListener( listener );
440 }
441
442
443 try
444 {
445 verifyChecksum( md5ChecksumObserver, destination, temp, remotePath, ".md5", wagon );
446 }
447 catch ( ChecksumVerificationException e )
448 {
449
450
451
452
453 if ( firstRun )
454 {
455 LOGGER.warn( "*** CHECKSUM FAILED - " + e.getMessage() + " - RETRYING" );
456 retry = true;
457 }
458 else
459 {
460 throw new ChecksumVerificationException( e.getMessage(), e.getCause() );
461 }
462 }
463 catch ( ResourceDoesNotExistException md5TryException )
464 {
465 LOGGER.debug( "MD5 not found, trying SHA1", md5TryException );
466
467
468
469 try
470 {
471 verifyChecksum( sha1ChecksumObserver, destination, temp, remotePath, ".sha1", wagon );
472 }
473 catch ( ChecksumVerificationException e )
474 {
475
476
477 if ( firstRun )
478 {
479 retry = true;
480 }
481 else
482 {
483 throw new ChecksumVerificationException( e.getMessage(), e.getCause() );
484 }
485 }
486 catch ( ResourceDoesNotExistException sha1TryException )
487 {
488
489 throw new ChecksumVerificationException( "Error retrieving checksum file for "
490 + remotePath, sha1TryException );
491 }
492 }
493 }
494
495
496
497 artifactFound = true;
498
499 if ( !artifact.isSnapshot() )
500 {
501 break;
502 }
503
504
505 if ( listener != null )
506 {
507 wagon.addTransferListener( listener );
508 }
509
510
511 firstRun = false;
512 }
513
514 }
515 catch ( ResourceDoesNotExistException e )
516 {
517
518
519
520
521
522 LOGGER.debug( "File not found on one of the repos", e );
523 }
524 catch ( Exception e )
525 {
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544 LOGGER.warn( "Error retrieving artifact from [" + repository.getUrl() + "]: " + e );
545 LOGGER.debug( "Error details", e );
546 }
547 finally
548 {
549 try
550 {
551 wagon.disconnect();
552 }
553 catch ( ConnectionException e )
554 {
555 LOGGER.debug( "Error disconnecting wagon", e );
556 }
557 }
558
559 if ( !temp.exists() && downloaded )
560 {
561 LOGGER.debug( "Downloaded file does not exist: " + temp );
562 artifactFound = false;
563 }
564
565
566
567
568
569
570
571 if ( !temp.renameTo( destination ) && downloaded )
572 {
573 try
574 {
575 FileUtils.copyFile( temp, destination );
576 temp.delete();
577 }
578 catch ( IOException e )
579 {
580 LOGGER.debug( "Error copying temporary file to the final destination: " + e.getMessage() );
581 artifactFound = false;
582 }
583 }
584
585
586 if ( artifactFound )
587 {
588 break;
589 }
590 }
591
592 return artifactFound;
593 }
594
595 /**
596 * Creates Wagons. Replace it with a IoC container?
597 */
598 private static class DefaultWagonFactory
599 {
600
601 private final Map map = new HashMap();
602
603 public DefaultWagonFactory()
604 {
605 map.put( "http", HttpWagon.class );
606 map.put( "https", HttpWagon.class );
607 map.put( "sftp", SftpWagon.class );
608 map.put( "file", FileWagon.class );
609 }
610
611 public final Wagon getWagon( final String protocol )
612 {
613
614 Wagon ret;
615 final Class aClass = (Class) map.get( protocol );
616 if ( aClass == null )
617 {
618 LOGGER.info( "Unknown protocol: `" + protocol + "'. Trying file wagon" );
619 ret = new FileWagon();
620 }
621 else
622 {
623 try
624 {
625 ret = (Wagon) aClass.newInstance();
626 }
627 catch ( final Exception e )
628 {
629 throw new RuntimeException( e );
630 }
631 }
632
633 return ret;
634 }
635 }
636
637
638
639
640
641 /**
642 * Rules for verifying the checksum.
643 *
644 * We attempt to download artifacts and their accompanying md5 checksum
645 * files.
646 */
647 private void verifyChecksum( ChecksumObserver checksumObserver, File destination, File tempDestination,
648 String remotePath, String checksumFileExtension, Wagon wagon )
649 throws ResourceDoesNotExistException, TransferFailedException, AuthorizationException,
650 ChecksumVerificationException
651 {
652 try
653 {
654
655 String actualChecksum = checksumObserver.getActualChecksum();
656
657 File tempChecksumFile = new File( tempDestination + checksumFileExtension + ".tmp" );
658 tempChecksumFile.deleteOnExit();
659 wagon.get( remotePath + checksumFileExtension, tempChecksumFile );
660
661 String expectedChecksum = FileUtils.fileRead( tempChecksumFile );
662
663
664 expectedChecksum = expectedChecksum.trim();
665
666
667 if ( expectedChecksum.startsWith( "MD5" ) )
668 {
669 int lastSpacePos = expectedChecksum.lastIndexOf( ' ' );
670 expectedChecksum = expectedChecksum.substring( lastSpacePos + 1 );
671 }
672 else
673 {
674
675 int spacePos = expectedChecksum.indexOf( ' ' );
676
677 if ( spacePos != -1 )
678 {
679 expectedChecksum = expectedChecksum.substring( 0, spacePos );
680 }
681 }
682 if ( expectedChecksum.equals( actualChecksum ) )
683 {
684 File checksumFile = new File( destination + checksumFileExtension );
685 if ( checksumFile.exists() )
686 {
687 checksumFile.delete();
688 }
689 FileUtils.copyFile( tempChecksumFile, checksumFile );
690 }
691 else
692 {
693 throw new ChecksumVerificationException( "Checksum failed on download: local = '" + actualChecksum
694 + "'; remote = '" + expectedChecksum + "'" );
695 }
696 }
697 catch ( IOException e )
698 {
699 throw new ChecksumVerificationException( "Invalid checksum file", e );
700 }
701 }
702
703 }