package org.apache.maven.verifier;
/* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*/
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.AbstractMavenComponent;
import org.apache.maven.MavenConstants;
import org.apache.maven.jelly.MavenJellyContext;
import org.apache.maven.project.Project;
import org.apache.maven.repository.Artifact;
import org.apache.maven.util.BootstrapDownloadMeter;
import org.apache.maven.util.ConsoleDownloadMeter;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.TransferListener;
import org.apache.maven.wagon.observers.ChecksumObserver;
import org.apache.maven.wagon.providers.file.FileWagon;
import org.apache.maven.wagon.providers.http.HttpWagon;
import org.apache.maven.wagon.providers.ssh.jsch.SftpWagon;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.repository.Repository;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
/**
* Make sure that everything that is required for the project to build
* successfully is present. We will start by looking at the dependencies
* and make sure they are all here before trying to compile.
*
* @author <a href="mailto:jason@zenplex.com">Jason van Zyl</a>
* @author <a href="mailto:vmassol@apache.org">Vincent Massol</a>
*
*
* @todo Separate out the local settings verifier because this only needs to be run
* once a session, but is currently being run during project verification so
* this is a big waste in the reactor for example.
*/
public class DependencyVerifier
extends AbstractMavenComponent
{
/** LOGGER for debug output */
private static final Log LOGGER = LogFactory.getLog( DependencyVerifier.class );
/** List of failed deps. */
private List failedDependencies;
/** Local Repository verifier. */
private LocalSettingsVerifier localRepositoryVerifier;
private static Set resolvedArtifacts = new HashSet();
private ProxyInfo proxyInfo = null;
private TransferListener listener = null;
/**
* Default ctor.
* @param project the project to verify
*/
public DependencyVerifier( Project project )
{
super( project );
failedDependencies = new ArrayList();
localRepositoryVerifier = new LocalSettingsVerifier( project );
MavenJellyContext context = getProject().getContext();
if ( context.getProxyHost() != null )
{
proxyInfo = new ProxyInfo();
proxyInfo.setHost( context.getProxyHost() );
try
{
proxyInfo.setPort( Integer.valueOf( context.getProxyPort() ).intValue() );
}
catch ( NumberFormatException e )
{
LOGGER.warn( "Ignoring invalid proxy port: '" + context.getProxyPort() + "'" );
}
proxyInfo.setUserName( context.getProxyUserName() );
proxyInfo.setPassword( context.getProxyPassword() );
proxyInfo.setNonProxyHosts( (String) context.getVariable( MavenConstants.PROXY_NONPROXYHOSTS ) );
proxyInfo.setNtlmHost( (String) context.getVariable( MavenConstants.PROXY_NTLM_HOST ) );
proxyInfo.setNtlmDomain( (String) context.getVariable( MavenConstants.PROXY_NTLM_DOMAIN ) );
}
String meterType = (String) context.getVariable( MavenConstants.DOWNLOAD_METER );
if ( meterType == null )
{
meterType = "console";
}
if ( "bootstrap".equals( meterType ) )
{
listener = new BootstrapDownloadMeter();
}
else if ( "console".equals( meterType ) )
{
listener = new ConsoleDownloadMeter();
}
}
/**
* Execute the verification process.
*
* @throws RepoConfigException If an error occurs while verifying basic maven settings.
* @throws UnsatisfiedDependencyException If there are unsatisfied dependencies.
* @throws ChecksumVerificationException if the download checksum doesn't match the calculated
*/
public void verify()
throws RepoConfigException, UnsatisfiedDependencyException, ChecksumVerificationException
{
localRepositoryVerifier.verifyLocalRepository();
satisfyDependencies();
}
/**
* Clear the failed dependencies. Required when reusing the
* project verifier.
*/
private void clearFailedDependencies()
{
failedDependencies.clear();
}
/**
* Check to see that all dependencies are present and if they are
* not then download them.
*
* @throws UnsatisfiedDependencyException If there are unsatisfied dependencies.
* @throws ChecksumVerificationException If checksums don't match.
*/
private void satisfyDependencies()
throws UnsatisfiedDependencyException, ChecksumVerificationException
{
// Is the remote repository enabled?
boolean remoteRepoEnabled = getProject().getContext().getRemoteRepositoryEnabled().booleanValue();
// Is the user online?
boolean online = getProject().getContext().getOnline().booleanValue();
if ( !remoteRepoEnabled )
{
LOGGER.warn( getMessage( "remote.repository.disabled.warning" ) );
}
clearFailedDependencies();
for ( Iterator i = getProject().getArtifacts().iterator(); i.hasNext(); )
{
Artifact artifact = (Artifact) i.next();
String path = artifact.getUrlPath();
if ( resolvedArtifacts.contains( path ) )
{
if ( LOGGER.isDebugEnabled() )
LOGGER.debug( "(previously resolved: " + path + ")" );
continue;
}
resolvedArtifacts.add( path );
// The artifact plain doesn't exist so chalk it up as a failed dependency.
if ( !artifact.exists() )
{
if ( LOGGER.isDebugEnabled() )
LOGGER.debug( "Artifact [" + path + "] not found in local repository" );
failedDependencies.add( artifact );
}
else if ( artifact.isSnapshot() && !Artifact.OVERRIDE_PATH.equals( artifact.getOverrideType() ) )
{
// The artifact exists but we need to take into account the user
// being online and whether the artifact is a snapshot. If the user
// is online then snapshots are added to the list of failed dependencies
// so that a newer version can be retrieved if one exists. We make
// an exception when the user is working offline and let them
// take their chances with a strong warning that they could possibly
// be using an out-of-date artifact. We don't want to cripple users
// when working offline.
if ( online )
{
failedDependencies.add( artifact );
}
else
{
LOGGER.warn( getMessage( "offline.snapshot.warning", artifact.getName() ) );
}
}
}
// If we have any failed dependencies then we will attempt to download
// them for the user if the remote repository is enabled.
if ( !failedDependencies.isEmpty() && remoteRepoEnabled && online )
{
getDependencies();
}
// If we still have failed dependencies after we have tried to
// satisfy all dependencies then we have a problem. There might
// also be a problem if the use of the remote repository has
// been disabled and dependencies just aren't present. In any
// case we have a problem.
if ( !failedDependencies.isEmpty() )
{
throw new UnsatisfiedDependencyException( createUnsatisfiedDependenciesMessage() );
}
}
/**
* Create a message for the user stating the dependencies that are unsatisfied.
*
* @return The unsatisfied dependency message.
*/
private String createUnsatisfiedDependenciesMessage()
{
StringBuffer message = new StringBuffer();
if ( failedDependencies.size() == 1 )
{
message.append( getMessage( "single.unsatisfied.dependency.error" ) );
}
else
{
message.append( getMessage( "multiple.unsatisfied.dependency.error" ) );
}
message.append( "\n" );
for ( Iterator i = failedDependencies.iterator(); i.hasNext(); )
{
Artifact artifact = (Artifact) i.next();
message.append( "- " + artifact.getDescription() );
String overrideType = artifact.getOverrideType();
if ( overrideType != Artifact.OVERRIDE_NONE )
{
if ( Artifact.OVERRIDE_VERSION.equals( overrideType ) )
{
message.append( "; version override doesn't exist: " + artifact.getDependency().getVersion() );
}
else if ( Artifact.OVERRIDE_PATH.equals( overrideType ) )
{
message.append( "; path override doesn't exist: " + artifact.getPath() );
}
}
String url = artifact.getDependency().getUrl();
if ( StringUtils.isNotEmpty( url ) )
{
// FIXME: internationalize
message.append( " (" ).append( "try downloading from " ).append( url ).append( ")" );
}
message.append( "\n" );
}
return message.toString();
}
/**
* Try and retrieve the dependencies from the remote repository in
* order to satisfy the dependencies of the project.
* @throws ChecksumVerificationException If checksums don't match.
*/
private void getDependencies()
throws ChecksumVerificationException
{
if ( LOGGER.isDebugEnabled() )
LOGGER.debug( "Getting failed dependencies: " + failedDependencies );
// if there are failed dependencies try to indicate for which project:
if ( failedDependencies.size() > 0 )
{
LOGGER.info( getMessage( "satisfy.project.message", getProject().getName() ) );
}
Artifact artifact;
Iterator i = failedDependencies.iterator();
while ( i.hasNext() )
{
artifact = (Artifact) i.next();
// before we try to download a missing dependency we have to verify
// that the dependency is not of the type Artifact.OVERRIDE_PATH,
// in which case it can not be downloaded. Just skip this iteration.
// Since the dependency won't get removed from the failedDependencies list
// an error message will be created.
String overrideType = artifact.getOverrideType();
if ( Artifact.OVERRIDE_PATH.equals( overrideType ) )
{
continue;
}
// The directory structure for the project this dependency belongs to
// may not exists so attempt to create the project directory structure
// before attempting to download the dependency.
File directory = artifact.getFile().getParentFile();
if ( !directory.exists() )
{
directory.mkdirs();
}
if ( getRemoteArtifact( artifact ) )
{
// The dependency has been successfully downloaded so lets remove
// it from the failed dependency list.
i.remove();
}
else
{
if ( artifact.exists() )
{
// The snapshot jar locally exists and not in remote repository
//LOGGER.info( getMessage( "not.existing.artifact.in.repo", artifact.getUrlPath() ) );
i.remove();
}
// else
// {
// LOGGER.error( getMessage( "failed.download.warning", artifact.getName() ) );
// }
}
}
}
/**
* Retrieve a <code>remoteFile</code> from the maven remote repositories
* and store it at <code>localFile</code>
* @param artifact the artifact to retrieve from the repositories.
* @return true if the retrieval succeeds, false otherwise.
* @throws ChecksumVerificationException If checksums don't match.
*/
private boolean getRemoteArtifact( Artifact artifact )
throws ChecksumVerificationException
{
boolean artifactFound = false;
int count = 0;
Iterator i = getProject().getContext().getMavenRepoRemote().iterator();
while(i.hasNext())
{
String remoteRepo = (String) i.next();
if ( artifact.isSnapshot() && artifact.exists() )
{
LOGGER.info( getMessage( "update.message" ) + " " + artifact.getDescription() + " from " + remoteRepo );
}
else
{
LOGGER
.info( getMessage( "download.message" ) + " " + artifact.getDescription() + " from " + remoteRepo );
}
//LOGGER.info( "Searching in repository : " + remoteRepo );
Repository repository = new Repository( "repo" + count++, remoteRepo.trim() );
final Wagon wagon = new DefaultWagonFactory().getWagon( repository.getProtocol() );
if ( listener != null )
{
wagon.addTransferListener( listener );
}
ChecksumObserver md5ChecksumObserver = null;
ChecksumObserver sha1ChecksumObserver = null;
try
{
md5ChecksumObserver = new ChecksumObserver( "MD5" );
wagon.addTransferListener( md5ChecksumObserver );
sha1ChecksumObserver = new ChecksumObserver( "SHA-1" );
wagon.addTransferListener( sha1ChecksumObserver );
}
catch ( NoSuchAlgorithmException e )
{
throw new ChecksumVerificationException( "Unable to add checksum methods: " + e.getMessage(), e );
}
File destination = artifact.getFile();
String remotePath = artifact.getUrlPath();
File temp = new File( destination + ".tmp" );
temp.deleteOnExit();
boolean downloaded = false;
try
{
wagon.connect( repository, proxyInfo );
boolean firstRun = true;
boolean retry = true;
// this will run at most twice. The first time, the firstRun flag is turned off, and if the retry flag
// is set on the first run, it will be turned off and not re-set on the second try. This is because the
// only way the retry flag can be set is if ( firstRun == true ).
while ( firstRun || retry )
{
// reset the retry flag.
retry = false;
downloaded = wagon.getIfNewer( remotePath, temp, destination.lastModified() );
if ( !downloaded && firstRun )
{
LOGGER.info( getMessage( "skip.download.message" ) );
}
if ( downloaded )
{
// keep the checksum files from showing up on the download monitor...
if ( listener != null )
{
wagon.removeTransferListener( listener );
}
// try to verify the MD5 checksum for this file.
try
{
verifyChecksum( md5ChecksumObserver, destination, temp, remotePath, ".md5", wagon );
}
catch ( ChecksumVerificationException e )
{
// if we catch a ChecksumVerificationException, it means the transfer/read succeeded, but the checksum
// doesn't match. This could be a problem with the server (ibiblio HTTP-200 error page), so we'll
// try this up to two times. On the second try, we'll handle it as a bona-fide error, based on the
// repository's checksum checking policy.
if ( firstRun )
{
LOGGER.warn( "*** CHECKSUM FAILED - " + e.getMessage() + " - RETRYING" );
retry = true;
}
else
{
throw new ChecksumVerificationException( e.getMessage(), e.getCause() );
}
}
catch ( ResourceDoesNotExistException md5TryException )
{
LOGGER.debug( "MD5 not found, trying SHA1", md5TryException );
// if this IS NOT a ChecksumVerificationException, it was a problem with transfer/read of the checksum
// file...we'll try again with the SHA-1 checksum.
try
{
verifyChecksum( sha1ChecksumObserver, destination, temp, remotePath, ".sha1", wagon );
}
catch ( ChecksumVerificationException e )
{
// if we also fail to verify based on the SHA-1 checksum, and the checksum transfer/read
// succeeded, then we need to determine whether to retry or handle it as a failure.
if ( firstRun )
{
retry = true;
}
else
{
throw new ChecksumVerificationException( e.getMessage(), e.getCause() );
}
}
catch ( ResourceDoesNotExistException sha1TryException )
{
// this was a failed transfer, and we don't want to retry.
throw new ChecksumVerificationException( "Error retrieving checksum file for "
+ remotePath, sha1TryException );
}
}
}
// Artifact was found, continue checking additional remote repos (if any)
// in case there is a newer version (i.e. snapshots) in another repo
artifactFound = true;
if ( !artifact.isSnapshot() )
{
break;
}
// reinstate the download monitor...
if ( listener != null )
{
wagon.addTransferListener( listener );
}
// unset the firstRun flag, so we don't get caught in an infinite loop...
firstRun = false;
}
}
catch ( ResourceDoesNotExistException e )
{
// Multiple repositories may exist, and if the file is not found
// in just one of them, it's no problem, and we don't want to
// even print out an error.
// if it's not found at all, artifactFound will be false, and the
// build _will_ break, and the user will get an error message
LOGGER.debug( "File not found on one of the repos", e );
}
catch ( Exception e )
{
// If there are additional remote repos, then ignore exception
// as artifact may be found in another remote repo. If there
// are no more remote repos to check and the artifact wasn't found in
// a previous remote repo, then artifactFound is false indicating
// that the artifact could not be found in any of the remote repos
//
// arguably, we need to give the user better control (another command-
// line switch perhaps) of what to do in this case? Maven already has
// a command-line switch to work in offline mode, but what about when
// one of two or more remote repos is unavailable? There may be multiple
// remote repos for redundancy, in which case you probably want the build
// to continue. There may however be multiple remote repos because some
// artifacts are on one, and some are on another. In this case, you may
// want the build to break.
//
// print a warning, in any case, so user catches on to mistyped
// hostnames, or other snafus
// FIXME: localize this message
LOGGER.warn( "Error retrieving artifact from [" + repository.getUrl() + "]: " + e );
LOGGER.debug( "Error details", e );
}
finally
{
try
{
wagon.disconnect();
}
catch ( ConnectionException e )
{
LOGGER.debug( "Error disconnecting wagon", e );
}
}
if ( !temp.exists() && downloaded )
{
LOGGER.debug( "Downloaded file does not exist: " + temp );
artifactFound = false;
}
// The temporary file is named destination + ".tmp" and is done this way to ensure
// that the temporary file is in the same file system as the destination because the
// File.renameTo operation doesn't really work across file systems.
// So we will attempt to do a File.renameTo for efficiency and atomicity, if this fails
// then we will use a brute force copy and delete the temporary file.
if ( !temp.renameTo( destination ) && downloaded )
{
try
{
FileUtils.copyFile( temp, destination );
temp.delete();
}
catch ( IOException e )
{
LOGGER.debug( "Error copying temporary file to the final destination: " + e.getMessage() );
artifactFound = false;
}
}
// don't try another repo if artifact has been found
if ( artifactFound )
{
break;
}
}
return artifactFound;
}
/**
* Creates Wagons. Replace it with a IoC container?
*/
private static class DefaultWagonFactory
{
private final Map map = new HashMap();
public DefaultWagonFactory()
{
map.put( "http", HttpWagon.class );
map.put( "https", HttpWagon.class );
map.put( "sftp", SftpWagon.class );
map.put( "file", FileWagon.class );
}
public final Wagon getWagon( final String protocol )
{
// TODO: don't initialise the wagons all the time - use a session
Wagon ret;
final Class aClass = (Class) map.get( protocol );
if ( aClass == null )
{
LOGGER.info( "Unknown protocol: `" + protocol + "'. Trying file wagon" );
ret = new FileWagon();
}
else
{
try
{
ret = (Wagon) aClass.newInstance();
}
catch ( final Exception e )
{
throw new RuntimeException( e );
}
}
return ret;
}
}
// ----------------------------------------------------------------------
// V E R I F I C A T I O N
// ----------------------------------------------------------------------
/**
* Rules for verifying the checksum.
*
* We attempt to download artifacts and their accompanying md5 checksum
* files.
*/
private void verifyChecksum( ChecksumObserver checksumObserver, File destination, File tempDestination,
String remotePath, String checksumFileExtension, Wagon wagon )
throws ResourceDoesNotExistException, TransferFailedException, AuthorizationException,
ChecksumVerificationException
{
try
{
// grab it first, because it's about to change...
String actualChecksum = checksumObserver.getActualChecksum();
File tempChecksumFile = new File( tempDestination + checksumFileExtension + ".tmp" );
tempChecksumFile.deleteOnExit();
wagon.get( remotePath + checksumFileExtension, tempChecksumFile );
String expectedChecksum = FileUtils.fileRead( tempChecksumFile );
// remove whitespaces at the end
expectedChecksum = expectedChecksum.trim();
// check for 'MD5 (name) = CHECKSUM'
if ( expectedChecksum.startsWith( "MD5" ) )
{
int lastSpacePos = expectedChecksum.lastIndexOf( ' ' );
expectedChecksum = expectedChecksum.substring( lastSpacePos + 1 );
}
else
{
// remove everything after the first space (if available)
int spacePos = expectedChecksum.indexOf( ' ' );
if ( spacePos != -1 )
{
expectedChecksum = expectedChecksum.substring( 0, spacePos );
}
}
if ( expectedChecksum.equals( actualChecksum ) )
{
File checksumFile = new File( destination + checksumFileExtension );
if ( checksumFile.exists() )
{
checksumFile.delete();
}
FileUtils.copyFile( tempChecksumFile, checksumFile );
}
else
{
throw new ChecksumVerificationException( "Checksum failed on download: local = '" + actualChecksum
+ "'; remote = '" + expectedChecksum + "'" );
}
}
catch ( IOException e )
{
throw new ChecksumVerificationException( "Invalid checksum file", e );
}
}
}