View Javadoc

1   package org.apache.maven.verifier;
2   
3   /* ====================================================================
4    *   Licensed to the Apache Software Foundation (ASF) under one or more
5    *   contributor license agreements.  See the NOTICE file distributed with
6    *   this work for additional information regarding copyright ownership.
7    *   The ASF licenses this file to You under the Apache License, Version 2.0
8    *   (the "License"); you may not use this file except in compliance with
9    *   the License.  You may obtain a copy of the License at
10   *
11   *       http://www.apache.org/licenses/LICENSE-2.0
12   *
13   *   Unless required by applicable law or agreed to in writing, software
14   *   distributed under the License is distributed on an "AS IS" BASIS,
15   *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   *   See the License for the specific language governing permissions and
17   *   limitations under the License.
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         // Is the remote repository enabled?
166         boolean remoteRepoEnabled = getProject().getContext().getRemoteRepositoryEnabled().booleanValue();
167 
168         // Is the user online?
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             // The artifact plain doesn't exist so chalk it up as a failed dependency.
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                 // The artifact exists but we need to take into account the user
201                 // being online and whether the artifact is a snapshot. If the user
202                 // is online then snapshots are added to the list of failed dependencies
203                 // so that a newer version can be retrieved if one exists. We make
204                 // an exception when the user is working offline and let them
205                 // take their chances with a strong warning that they could possibly
206                 // be using an out-of-date artifact. We don't want to cripple users
207                 // when working offline.
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         // If we have any failed dependencies then we will attempt to download
220         // them for the user if the remote repository is enabled.
221         if ( !failedDependencies.isEmpty() && remoteRepoEnabled && online )
222         {
223             getDependencies();
224         }
225 
226         // If we still have failed dependencies after we have tried to
227         // satisfy all dependencies then we have a problem. There might
228         // also be a problem if the use of the remote repository has
229         // been disabled and dependencies just aren't present. In any
230         // case we have a problem.
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                 // FIXME: internationalize
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         // if there are failed dependencies try to indicate for which project:
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             // before we try to download a missing dependency we have to verify
311             // that the dependency is not of the type Artifact.OVERRIDE_PATH,
312             // in which case it can not be downloaded. Just skip this iteration.
313             // Since the dependency won't get removed from the failedDependencies list
314             // an error message will be created.
315             String overrideType = artifact.getOverrideType();
316             if ( Artifact.OVERRIDE_PATH.equals( overrideType ) )
317             {
318                 continue;
319             }
320 
321             // The directory structure for the project this dependency belongs to
322             // may not exists so attempt to create the project directory structure
323             // before attempting to download the dependency.
324             File directory = artifact.getFile().getParentFile();
325 
326             if ( !directory.exists() )
327             {
328                 directory.mkdirs();
329             }
330 
331             if ( getRemoteArtifact( artifact ) )
332             {
333                 // The dependency has been successfully downloaded so lets remove
334                 // it from the failed dependency list.
335                 i.remove();
336             }
337             else
338             {
339                 if ( artifact.exists() )
340                 {
341                     // The snapshot jar locally exists and not in remote repository
342                     //LOGGER.info( getMessage( "not.existing.artifact.in.repo", artifact.getUrlPath() ) );
343                     i.remove();
344                 }
345                 //                else
346                 //                {
347                 //                    LOGGER.error( getMessage( "failed.download.warning", artifact.getName() ) );
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             //LOGGER.info( "Searching in repository : " + remoteRepo );
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                 // this will run at most twice. The first time, the firstRun flag is turned off, and if the retry flag
421                 // is set on the first run, it will be turned off and not re-set on the second try. This is because the
422                 // only way the retry flag can be set is if ( firstRun == true ).
423                 while ( firstRun || retry )
424                 {
425                     // reset the retry flag.
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                         // keep the checksum files from showing up on the download monitor...
437                         if ( listener != null )
438                         {
439                             wagon.removeTransferListener( listener );
440                         }
441 
442                         // try to verify the MD5 checksum for this file.
443                         try
444                         {
445                             verifyChecksum( md5ChecksumObserver, destination, temp, remotePath, ".md5", wagon );
446                         }
447                         catch ( ChecksumVerificationException e )
448                         {
449                             // if we catch a ChecksumVerificationException, it means the transfer/read succeeded, but the checksum
450                             // doesn't match. This could be a problem with the server (ibiblio HTTP-200 error page), so we'll
451                             // try this up to two times. On the second try, we'll handle it as a bona-fide error, based on the
452                             // repository's checksum checking policy.
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                             // if this IS NOT a ChecksumVerificationException, it was a problem with transfer/read of the checksum
468                             // file...we'll try again with the SHA-1 checksum.
469                             try
470                             {
471                                 verifyChecksum( sha1ChecksumObserver, destination, temp, remotePath, ".sha1", wagon );
472                             }
473                             catch ( ChecksumVerificationException e )
474                             {
475                                 // if we also fail to verify based on the SHA-1 checksum, and the checksum transfer/read
476                                 // succeeded, then we need to determine whether to retry or handle it as a failure.
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                                 // this was a failed transfer, and we don't want to retry.
489                                 throw new ChecksumVerificationException( "Error retrieving checksum file for "
490                                     + remotePath, sha1TryException );
491                             }
492                         }
493                     }
494 
495                     // Artifact was found, continue checking additional remote repos (if any)
496                     // in case there is a newer version (i.e. snapshots) in another repo
497                     artifactFound = true;
498 
499                     if ( !artifact.isSnapshot() )
500                     {
501                         break;
502                     }
503 
504                     // reinstate the download monitor...
505                     if ( listener != null )
506                     {
507                         wagon.addTransferListener( listener );
508                     }
509 
510                     // unset the firstRun flag, so we don't get caught in an infinite loop...
511                     firstRun = false;
512                 }
513 
514             }
515             catch ( ResourceDoesNotExistException e )
516             {
517                 // Multiple repositories may exist, and if the file is not found
518                 // in just one of them, it's no problem, and we don't want to
519                 // even print out an error.
520                 // if it's not found at all, artifactFound will be false, and the
521                 // build _will_ break, and the user will get an error message
522                 LOGGER.debug( "File not found on one of the repos", e );
523             }
524             catch ( Exception e )
525             {
526                 // If there are additional remote repos, then ignore exception
527                 // as artifact may be found in another remote repo. If there
528                 // are no more remote repos to check and the artifact wasn't found in
529                 // a previous remote repo, then artifactFound is false indicating
530                 // that the artifact could not be found in any of the remote repos
531                 //
532                 // arguably, we need to give the user better control (another command-
533                 // line switch perhaps) of what to do in this case? Maven already has
534                 // a command-line switch to work in offline mode, but what about when
535                 // one of two or more remote repos is unavailable? There may be multiple
536                 // remote repos for redundancy, in which case you probably want the build
537                 // to continue. There may however be multiple remote repos because some
538                 // artifacts are on one, and some are on another. In this case, you may
539                 // want the build to break.
540                 //
541                 // print a warning, in any case, so user catches on to mistyped
542                 // hostnames, or other snafus
543                 // FIXME: localize this message
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             // The temporary file is named destination + ".tmp" and is done this way to ensure
566             // that the temporary file is in the same file system as the destination because the
567             // File.renameTo operation doesn't really work across file systems.
568             // So we will attempt to do a File.renameTo for efficiency and atomicity, if this fails
569             // then we will use a brute force copy and delete the temporary file.
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             // don't try another repo if artifact has been found
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             // TODO: don't initialise the wagons all the time - use a session
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     // V E R I F I C A T I O N
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             // grab it first, because it's about to change...
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             // remove whitespaces at the end
664             expectedChecksum = expectedChecksum.trim();
665 
666             // check for 'MD5 (name) = CHECKSUM'
667             if ( expectedChecksum.startsWith( "MD5" ) )
668             {
669                 int lastSpacePos = expectedChecksum.lastIndexOf( ' ' );
670                 expectedChecksum = expectedChecksum.substring( lastSpacePos + 1 );
671             }
672             else
673             {
674                 // remove everything after the first space (if available)
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 }