package org.codehaus.mojo.versions.api;

/*
 * 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 org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.manager.WagonManager;
import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.path.PathTranslator;
import org.apache.maven.settings.Settings;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.UnsupportedProtocolException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.codehaus.mojo.versions.PluginUpdatesDetails;
import org.codehaus.mojo.versions.Property;
import org.codehaus.mojo.versions.model.IgnoreVersion;
import org.codehaus.mojo.versions.model.Rule;
import org.codehaus.mojo.versions.model.RuleSet;
import org.codehaus.mojo.versions.model.io.xpp3.RuleXpp3Reader;
import org.codehaus.mojo.versions.ordering.VersionComparator;
import org.codehaus.mojo.versions.ordering.VersionComparators;
import org.codehaus.mojo.versions.utils.DependencyComparator;
import org.codehaus.mojo.versions.utils.PluginComparator;
import org.codehaus.mojo.versions.utils.RegexUtils;
import org.codehaus.mojo.versions.utils.VersionsExpressionEvaluator;
import org.codehaus.mojo.versions.utils.WagonUtils;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;

import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Pattern;

/**
 * Helper class that provides common functionality required by both the mojos and the reports.
 *
 * @author Stephen Connolly
 * @since 1.0-alpha-3
 */
public class DefaultVersionsHelper
    implements VersionsHelper
{
    private static final String CLASSPATH_PROTOCOL = "classpath";

    private static final String TYPE_EXACT = "exact";

    private static final String TYPE_REGEX = "regex";

    private static final int LOOKUP_PARALLEL_THREADS = 5;

    /**
     * The artifact comparison rules to use.
     *
     * @deprecated
     * @since 1.0-alpha-3
     */
    private final RuleSet ruleSet;

    /**
     * The artifact metadata source to use.
     *
     * @since 1.0-alpha-3
     */
    private final ArtifactMetadataSource artifactMetadataSource;

    /**
     * The local repository to consult.
     *
     * @since 1.0-alpha-3
     */
    private final ArtifactRepository localRepository;

    /**
     * The remote artifact repositories to consult.
     *
     * @since 1.0-alpha-3
     */
    private final List remoteArtifactRepositories;

    /**
     * The remote plugin repositories to consult.
     *
     * @since 1.0-alpha-3
     */
    private final List remotePluginRepositories;

    /**
     * The artifact factory.
     *
     * @since 1.0-alpha-3
     */
    private final ArtifactFactory artifactFactory;

    /**
     * The {@link Log} to send log messages to.
     *
     * @since 1.0-alpha-3
     */
    private final Log log;

    /**
     * The path translator.
     *
     * @since 1.0-beta-1
     */
    private final PathTranslator pathTranslator;

    /**
     * The maven session.
     *
     * @since 1.0-beta-1
     */
    private final MavenSession mavenSession;

    /**
     * The artifact resolver.
     *
     * @since 1.3
     */
    private final ArtifactResolver artifactResolver;

    /**
     * Constructs a new {@link DefaultVersionsHelper}.
     *
     * @param artifactFactory The artifact factory.
     * @param artifactResolver Artifact resolver
     * @param artifactMetadataSource The artifact metadata source to use.
     * @param remoteArtifactRepositories The remote artifact repositories to consult.
     * @param remotePluginRepositories The remote plugin repositories to consult.
     * @param localRepository The local repository to consult.
     * @param wagonManager The wagon manager (used if rules need to be retrieved).
     * @param settings The settings (used to provide proxy information to the wagon manager).
     * @param serverId The serverId hint for the wagon manager.
     * @param rulesUri The URL to retrieve the versioning rules from.
     * @param log The {@link org.apache.maven.plugin.logging.Log} to send log messages to.
     * @param mavenSession The maven session information.
     * @param pathTranslator The path translator component. @throws org.apache.maven.plugin.MojoExecutionException If
     *            things go wrong.
     * @throws MojoExecutionException if something goes wrong.
     * @since 1.0-alpha-3
     */
    public DefaultVersionsHelper( ArtifactFactory artifactFactory, ArtifactResolver artifactResolver,
                                  ArtifactMetadataSource artifactMetadataSource, List remoteArtifactRepositories,
                                  List remotePluginRepositories, ArtifactRepository localRepository,
                                  WagonManager wagonManager, Settings settings, String serverId, String rulesUri,
                                  Log log, MavenSession mavenSession, PathTranslator pathTranslator )
        throws MojoExecutionException
    {
        this.artifactFactory = artifactFactory;
        this.artifactResolver = artifactResolver;
        this.mavenSession = mavenSession;
        this.pathTranslator = pathTranslator;
        this.ruleSet = loadRuleSet( serverId, settings, wagonManager, rulesUri, log );
        this.artifactMetadataSource = artifactMetadataSource;
        this.localRepository = localRepository;
        this.remoteArtifactRepositories = remoteArtifactRepositories;
        this.remotePluginRepositories = remotePluginRepositories;
        this.log = log;
    }

    @Deprecated
    private static RuleSet getRuleSet( Wagon wagon, String remoteURI )
        throws IOException, AuthorizationException, TransferFailedException, ResourceDoesNotExistException
    {
        File tempFile = File.createTempFile( "ruleset", ".xml" );
        try
        {
            wagon.get( remoteURI, tempFile );
            InputStream is = new FileInputStream(tempFile );
            try
            {
                return readRulesFromStream(is);
            }
            finally
            {
                try
                {
                    is.close();
                }
                catch ( IOException e )
                {
                    // ignore
                }
            }
        }
        finally
        {
            if ( !tempFile.delete() )
            {
                // maybe we can delete this later
                tempFile.deleteOnExit();
            }
        }
    }

    private static RuleSet readRulesFromStream(InputStream stream)
        throws IOException {
        RuleXpp3Reader reader = new RuleXpp3Reader();
        BufferedInputStream bis = new BufferedInputStream( stream );

        try
        {
            return reader.read( bis );
        }
        catch ( XmlPullParserException e )
        {
            final IOException ioe = new IOException();
            ioe.initCause( e );
            throw ioe;
        }
        finally
        {
            try
            {
                bis.close();
            }
            catch ( IOException e )
            {
                // ignore
            }
        }
    }

    static boolean exactMatch( String wildcardRule, String value )
    {
        Pattern p = Pattern.compile( RegexUtils.convertWildcardsToRegex( wildcardRule, true ) );
        return p.matcher( value ).matches();
    }

    static boolean match( String wildcardRule, String value )
    {
        Pattern p = Pattern.compile( RegexUtils.convertWildcardsToRegex( wildcardRule, false ) );
        return p.matcher( value ).matches();
    }

    private static RuleSet loadRuleSet( String serverId, Settings settings, WagonManager wagonManager, String rulesUri,
                                        Log logger )
        throws MojoExecutionException {
        RuleSet ruleSet = new RuleSet();
        boolean rulesUriGiven = isRulesUriNotBlank(rulesUri);

        if (rulesUriGiven) {
            RuleSet loadedRules;

            if (isClasspathUri(rulesUri)) {
                loadedRules = getRulesFromClasspath(rulesUri, logger);
            } else {
                loadedRules = getRulesViaWagon(rulesUri, logger, serverId, serverId, wagonManager,
                                                       settings);
            }

            ruleSet.setIgnoreVersions(loadedRules.getIgnoreVersions());
            ruleSet.setRules(loadedRules.getRules());
        }

        return ruleSet;
    }

    private static RuleSet getRulesFromClasspath(String uri, Log logger)
        throws MojoExecutionException {
        logger.debug("Going to load rules from \"" + uri + "\"");

        String choppedUrl = uri.substring(CLASSPATH_PROTOCOL.length() + 3);

        URL url = DefaultVersionsHelper.class.getResource(choppedUrl);

        if (null == url) {
            String message = "Resource \"" + uri + "\" not found in classpath.";

            throw new MojoExecutionException(message);
        }

        try {
            RuleSet rules = readRulesFromStream(url.openStream());
            logger.debug("Loaded rules from \"" + uri + "\" successfully");
            return rules;
        }
        catch (IOException e) {
            throw new MojoExecutionException("Could not load specified rules from " + uri, e);
        }
    }

    private static boolean isRulesUriNotBlank(String rulesUri) {
        return rulesUri != null && rulesUri.trim().length() != 0;
    }

    private static RuleSet getRulesViaWagon(String rulesUri, Log logger, String serverId, String id,
                                            WagonManager wagonManager, Settings settings)
        throws MojoExecutionException {
        RuleSet loadedRules = new RuleSet();

        int split = rulesUri.lastIndexOf('/');
        String baseUri = rulesUri;
        String fileUri = "";

        if (split != -1) {
            baseUri = rulesUri.substring(0, split) + '/';
            fileUri = split + 1 < rulesUri.length() ? rulesUri.substring(split + 1) : "";
        }

        try {
            Wagon wagon = WagonUtils.createWagon(serverId, baseUri, wagonManager, settings, logger);
            try {
                logger.debug("Trying to load ruleset from file \"" + fileUri + "\" in " + baseUri);
                loadedRules = getRuleSet(wagon, fileUri);
            }
            finally {
                logger.debug("Rule set loaded");

                if (wagon != null) {
                    try {
                        wagon.disconnect();
                    }
                    catch (ConnectionException e) {
                        logger.warn("Could not disconnect wagon!", e);
                    }
                }
            }
        }
        catch (TransferFailedException e) {
            throw new MojoExecutionException("Could not transfer rules from " + rulesUri, e);
        }
        catch (AuthorizationException e) {
            throw new MojoExecutionException("Authorization failure trying to load rules from " + rulesUri, e);
        }
        catch (ResourceDoesNotExistException e) {
            throw new MojoExecutionException("Could not load specified rules from " + rulesUri, e);
        }
        catch (AuthenticationException e) {
            throw new MojoExecutionException("Authentication failure trying to load rules from " + rulesUri, e);
        }
        catch (UnsupportedProtocolException e) {
            throw new MojoExecutionException("Unsupported protocol for " + rulesUri, e);
        }
        catch (ConnectionException e) {
            throw new MojoExecutionException("Could not establish connection to " + rulesUri, e);
        }
        catch (IOException e) {
            throw new MojoExecutionException("Could not load specified rules from " + rulesUri, e);
        }

        return loadedRules;
    }

    static boolean isClasspathUri(String uri) {
        boolean startsWithProtocol = null != uri && uri.startsWith(CLASSPATH_PROTOCOL);
        boolean hasColonNext = null != uri && uri.charAt(CLASSPATH_PROTOCOL.length()) == ':';

        return startsWithProtocol && hasColonNext;
    }

    /**
     * {@inheritDoc}
     */
    public ArtifactFactory getArtifactFactory()
    {
        return artifactFactory;
    }

    /**
     * {@inheritDoc}
     */
    public Log getLog()
    {
        return log;
    }

    /**
     * {@inheritDoc}
     */
    public ArtifactVersions lookupArtifactVersions( Artifact artifact, boolean usePluginRepositories )
        throws ArtifactMetadataRetrievalException
    {
        List remoteRepositories = usePluginRepositories ? remotePluginRepositories : remoteArtifactRepositories;
        final List<ArtifactVersion> versions =
            artifactMetadataSource.retrieveAvailableVersions( artifact, localRepository, remoteRepositories );
        final List<IgnoreVersion> ignoredVersions = getIgnoredVersions( artifact );
        if ( !ignoredVersions.isEmpty() )
        {
            if ( getLog().isDebugEnabled() )
            {
                getLog().debug( "Found ignored versions: " + showIgnoredVersions( ignoredVersions ) );
            }

            final Iterator<ArtifactVersion> i = versions.iterator();
            while ( i.hasNext() )
            {
                final String version = i.next().toString();
                for ( final IgnoreVersion ignoreVersion : ignoredVersions )
                {
                    if ( TYPE_REGEX.equals( ignoreVersion.getType() ) )
                    {
                        Pattern p = Pattern.compile( ignoreVersion.getVersion() );
                        if ( p.matcher( version ).matches() )
                        {
                            if ( getLog().isDebugEnabled() )
                            {
                                getLog().debug( "Version " + version + " for artifact "
                                    + ArtifactUtils.versionlessKey( artifact ) + " found on ignore list: "
                                    + ignoreVersion );
                            }
                            i.remove();
                            break;
                        }
                    }
                    else if ( TYPE_EXACT.equals( ignoreVersion.getType() ) )
                    {
                        if ( version.equals( ignoreVersion.getVersion() ) )
                        {
                            if ( getLog().isDebugEnabled() )
                            {
                                getLog().debug( "Version " + version + " for artifact "
                                    + ArtifactUtils.versionlessKey( artifact ) + " found on ignore list: "
                                    + ignoreVersion );
                            }
                            i.remove();
                            break;
                        }
                    }
                }
            }
        }
        return new ArtifactVersions( artifact, versions, getVersionComparator( artifact ) );
    }

    /**
     * Returns a list of versions which should not be considered when looking for updates.
     *
     * @param artifact The artifact
     * @return List of ignored version
     */
    private List<IgnoreVersion> getIgnoredVersions( Artifact artifact )
    {
        final List<IgnoreVersion> ret = new ArrayList<IgnoreVersion>();

        for ( final IgnoreVersion ignoreVersion : ruleSet.getIgnoreVersions() )
        {
            if ( !TYPE_EXACT.equals( ignoreVersion.getType() ) && !TYPE_REGEX.equals( ignoreVersion.getType() ) )
            {
                getLog().warn( "The type attribute '" + ignoreVersion.getType() + "' for global ignoreVersion["
                    + ignoreVersion + "] is not valid." + " Please use either '" + TYPE_EXACT + "' or '" + TYPE_REGEX
                    + "'." );
            }
            else
            {
                ret.add( ignoreVersion );
            }
        }

        final Rule rule = getBestFitRule( artifact.getGroupId(), artifact.getArtifactId() );

        if ( rule != null )
        {
            for ( IgnoreVersion ignoreVersion : rule.getIgnoreVersions() )
            {
                if ( !TYPE_EXACT.equals( ignoreVersion.getType() ) && !TYPE_REGEX.equals( ignoreVersion.getType() ) )
                {
                    getLog().warn( "The type attribute '" + ignoreVersion.getType() + "' for " + rule + " is not valid."
                        + " Please use either '" + TYPE_EXACT + "' or '" + TYPE_REGEX + "'." );
                }
                else
                {
                    ret.add( ignoreVersion );
                }
            }
        }

        return ret;
    }

    /**
     * Pretty print a list of ignored versions.
     *
     * @param ignoredVersions A list of ignored versions
     * @return A String representation of the list
     */
    private String showIgnoredVersions( List<IgnoreVersion> ignoredVersions )
    {
        StringBuilder buf = new StringBuilder();
        Iterator<IgnoreVersion> iterator = ignoredVersions.iterator();
        while ( iterator.hasNext() )
        {
            IgnoreVersion ignoreVersion = iterator.next();
            buf.append( ignoreVersion );
            if ( iterator.hasNext() )
            {
                buf.append( ", " );
            }
        }
        return buf.toString();
    }

    public void resolveArtifact( Artifact artifact, boolean usePluginRepositories )
        throws ArtifactResolutionException, ArtifactNotFoundException
    {
        List remoteRepositories = usePluginRepositories ? remotePluginRepositories : remoteArtifactRepositories;
        artifactResolver.resolve( artifact, remoteRepositories, localRepository );
    }

    /**
     * {@inheritDoc}
     */
    public VersionComparator getVersionComparator( Artifact artifact )
    {
        return getVersionComparator( artifact.getGroupId(), artifact.getArtifactId() );
    }

    /**
     * {@inheritDoc}
     */
    public VersionComparator getVersionComparator( String groupId, String artifactId )
    {
        Rule rule = getBestFitRule( groupId, artifactId );
        final String comparisonMethod = rule == null ? ruleSet.getComparisonMethod() : rule.getComparisonMethod();
        return VersionComparators.getVersionComparator( comparisonMethod );
    }

    /**
     * Find the rule, if any, which best fits the artifact details given.
     *
     * @param groupId Group id of the artifact
     * @param artifactId Artifact id of the artifact
     * @return Rule which best describes the given artifact
     */
    protected Rule getBestFitRule( String groupId, String artifactId )
    {
        Rule bestFit = null;
        final List<Rule> rules = ruleSet.getRules();
        int bestGroupIdScore = Integer.MAX_VALUE;
        int bestArtifactIdScore = Integer.MAX_VALUE;
        boolean exactGroupId = false;
        boolean exactArtifactId = false;
        for ( Rule rule : rules )
        {
            int groupIdScore = RegexUtils.getWildcardScore( rule.getGroupId() );
            if ( groupIdScore > bestGroupIdScore )
            {
                continue;
            }
            boolean exactMatch = exactMatch( rule.getGroupId(), groupId );
            boolean match = exactMatch || match( rule.getGroupId(), groupId );
            if ( !match || ( exactGroupId && !exactMatch ) )
            {
                continue;
            }
            if ( bestGroupIdScore > groupIdScore )
            {
                bestArtifactIdScore = Integer.MAX_VALUE;
                exactArtifactId = false;
            }
            bestGroupIdScore = groupIdScore;
            if ( exactMatch && !exactGroupId )
            {
                exactGroupId = true;
                bestArtifactIdScore = Integer.MAX_VALUE;
                exactArtifactId = false;
            }
            int artifactIdScore = RegexUtils.getWildcardScore( rule.getArtifactId() );
            if ( artifactIdScore > bestArtifactIdScore )
            {
                continue;
            }
            exactMatch = exactMatch( rule.getArtifactId(), artifactId );
            match = exactMatch || match( rule.getArtifactId(), artifactId );
            if ( !match || ( exactArtifactId && !exactMatch ) )
            {
                continue;
            }
            bestArtifactIdScore = artifactIdScore;
            if ( exactMatch && !exactArtifactId )
            {
                exactArtifactId = true;
            }
            bestFit = rule;
        }
        return bestFit;
    }

    /**
     * {@inheritDoc}
     */
    public Artifact createPluginArtifact( String groupId, String artifactId, VersionRange versionRange )
    {
        return artifactFactory.createPluginArtifact( groupId, artifactId, versionRange );
    }

    /**
     * {@inheritDoc}
     */
    public Artifact createDependencyArtifact( String groupId, String artifactId, VersionRange versionRange, String type,
                                              String classifier, String scope, boolean optional )
    {
        return artifactFactory.createDependencyArtifact( groupId, artifactId, versionRange, type, classifier, scope,
                                                         optional );
    }

    /**
     * {@inheritDoc}
     */
    public Artifact createDependencyArtifact( String groupId, String artifactId, VersionRange versionRange, String type,
                                              String classifier, String scope )
    {
        return artifactFactory.createDependencyArtifact( groupId, artifactId, versionRange, type, classifier, scope );
    }

    /**
     * {@inheritDoc}
     */
    public Artifact createDependencyArtifact( Dependency dependency )
        throws InvalidVersionSpecificationException
    {
        return createDependencyArtifact( dependency.getGroupId(), dependency.getArtifactId(),
                                         dependency.getVersion() == null ? VersionRange.createFromVersionSpec( "[0,]" )
                                                         : VersionRange.createFromVersionSpec( dependency.getVersion() ),
                                         dependency.getType(), dependency.getClassifier(), dependency.getScope(),
                                         dependency.isOptional() );
    }

    /**
     * {@inheritDoc}
     */
    public Set<Artifact> extractArtifacts( Collection<MavenProject> mavenProjects )
    {
        Set<Artifact> result = new HashSet<Artifact>();
        for ( MavenProject project : mavenProjects )
        {
            result.add( project.getArtifact() );
        }

        return result;
    }

    /**
     * {@inheritDoc}
     */
    public ArtifactVersion createArtifactVersion( String version )
    {
        return new DefaultArtifactVersion( version );
    }

    /**
     * {@inheritDoc}
     */
    public ArtifactVersions lookupArtifactUpdates( Artifact artifact, boolean allowSnapshots,
                                                   boolean usePluginRepositories )
        throws ArtifactMetadataRetrievalException
    {
        ArtifactVersions artifactVersions = lookupArtifactVersions( artifact, usePluginRepositories );

        artifactVersions.setIncludeSnapshots( allowSnapshots );

        return artifactVersions;
    }

    /**
     * {@inheritDoc}
     */
    public Map<Dependency, ArtifactVersions> lookupDependenciesUpdates( Set<Dependency> dependencies,
                                                                        boolean usePluginRepositories )
        throws ArtifactMetadataRetrievalException, InvalidVersionSpecificationException
    {
        // Create the request for details collection for parallel lookup...
        final List<Callable<DependencyArtifactVersions>> requestsForDetails =
            new ArrayList<Callable<DependencyArtifactVersions>>( dependencies.size() );
        for ( final Dependency dependency : dependencies )
        {
            requestsForDetails.add( new DependencyLookup( dependency, usePluginRepositories ) );
        }

        final Map<Dependency, ArtifactVersions> dependencyUpdates =
            new TreeMap<Dependency, ArtifactVersions>( new DependencyComparator() );

        // Lookup details in parallel...
        final ExecutorService executor = Executors.newFixedThreadPool( LOOKUP_PARALLEL_THREADS );
        try
        {
            final List<Future<DependencyArtifactVersions>> responseForDetails =
                executor.invokeAll( requestsForDetails );

            // Construct the final results...
            for ( final Future<DependencyArtifactVersions> details : responseForDetails )
            {
                final DependencyArtifactVersions dav = details.get();
                dependencyUpdates.put( dav.getDependency(), dav.getArtifactVersions() );
            }
        }
        catch ( final ExecutionException ee )
        {
            throw new ArtifactMetadataRetrievalException( "Unable to acquire metadata for dependencies " + dependencies
                + ": " + ee.getMessage(), ee );
        }
        catch ( final InterruptedException ie )
        {
            throw new ArtifactMetadataRetrievalException( "Unable to acquire metadata for dependencies " + dependencies
                + ": " + ie.getMessage(), ie );
        }
        finally
        {
            executor.shutdownNow();
        }
        return dependencyUpdates;
    }

    /**
     * {@inheritDoc}
     */
    public ArtifactVersions lookupDependencyUpdates( Dependency dependency, boolean usePluginRepositories )
        throws ArtifactMetadataRetrievalException, InvalidVersionSpecificationException
    {
        getLog().debug( "Checking "
            + ArtifactUtils.versionlessKey( dependency.getGroupId(), dependency.getArtifactId() )
            + " for updates newer than " + dependency.getVersion() );
        VersionRange versionRange = VersionRange.createFromVersionSpec( dependency.getVersion() );

        return lookupArtifactVersions( createDependencyArtifact( dependency.getGroupId(), dependency.getArtifactId(),
                                                                 versionRange, dependency.getType(),
                                                                 dependency.getClassifier(), dependency.getScope() ),
                                       usePluginRepositories );
    }

    /**
     * {@inheritDoc}
     */
    public Map<Plugin, PluginUpdatesDetails> lookupPluginsUpdates( Set<Plugin> plugins, boolean allowSnapshots )
        throws ArtifactMetadataRetrievalException, InvalidVersionSpecificationException
    {
        // Create the request for details collection for parallel lookup...
        final List<Callable<PluginPluginUpdatesDetails>> requestsForDetails =
            new ArrayList<Callable<PluginPluginUpdatesDetails>>( plugins.size() );
        for ( final Plugin plugin : plugins )
        {
            requestsForDetails.add( new PluginLookup( plugin, allowSnapshots ) );
        }

        final Map<Plugin, PluginUpdatesDetails> pluginUpdates =
            new TreeMap<Plugin, PluginUpdatesDetails>( new PluginComparator() );

        // Lookup details in parallel...
        final ExecutorService executor = Executors.newFixedThreadPool( LOOKUP_PARALLEL_THREADS );
        try
        {
            final List<Future<PluginPluginUpdatesDetails>> responseForDetails =
                executor.invokeAll( requestsForDetails );

            // Construct the final results...
            for ( final Future<PluginPluginUpdatesDetails> details : responseForDetails )
            {
                final PluginPluginUpdatesDetails pud = details.get();
                pluginUpdates.put( pud.getPlugin(), pud.getPluginUpdatesDetails() );
            }
        }
        catch ( final ExecutionException ee )
        {
            throw new ArtifactMetadataRetrievalException( "Unable to acquire metadata for plugins " + plugins + ": "
                + ee.getMessage(), ee );
        }
        catch ( final InterruptedException ie )
        {
            throw new ArtifactMetadataRetrievalException( "Unable to acquire metadata for plugins " + plugins + ": "
                + ie.getMessage(), ie );
        }
        finally
        {
            executor.shutdownNow();
        }
        return pluginUpdates;
    }

    /**
     * {@inheritDoc}
     */
    public PluginUpdatesDetails lookupPluginUpdates( Plugin plugin, boolean allowSnapshots )
        throws ArtifactMetadataRetrievalException, InvalidVersionSpecificationException
    {
        String version = plugin.getVersion();
        version = version == null ? "LATEST" : version;
        getLog().debug( "Checking " + ArtifactUtils.versionlessKey( plugin.getGroupId(), plugin.getArtifactId() )
            + " for updates newer than " + version );

        VersionRange versionRange = VersionRange.createFromVersion( version );

        final boolean includeSnapshots = allowSnapshots;

        final ArtifactVersions pluginArtifactVersions =
            lookupArtifactVersions( createPluginArtifact( plugin.getGroupId(), plugin.getArtifactId(), versionRange ),
                                    true );

        Set<Dependency> pluginDependencies = new TreeSet<Dependency>( new DependencyComparator() );
        if ( plugin.getDependencies() != null )
        {
            pluginDependencies.addAll( plugin.getDependencies() );
        }
        Map<Dependency, ArtifactVersions> pluginDependencyDetails =
            lookupDependenciesUpdates( pluginDependencies, false );

        return new PluginUpdatesDetails( pluginArtifactVersions, pluginDependencyDetails, includeSnapshots );
    }

    /**
     * {@inheritDoc}
     */
    public ExpressionEvaluator getExpressionEvaluator( MavenProject project )
    {
        return new VersionsExpressionEvaluator( mavenSession, pathTranslator, project );
    }

    /**
     * {@inheritDoc}
     */
    public Map<Property, PropertyVersions> getVersionPropertiesMap( MavenProject project,
                                                                    Property[] propertyDefinitions,
                                                                    String includeProperties, String excludeProperties,
                                                                    boolean autoLinkItems )
        throws MojoExecutionException
    {
        Map<String, Property> properties = new HashMap<String, Property>();
        if ( propertyDefinitions != null )
        {
            for ( Property propertyDefinition : propertyDefinitions )
            {
                properties.put( propertyDefinition.getName(), propertyDefinition );
            }
        }
        Map<String, PropertyVersionsBuilder> builders = new HashMap<String, PropertyVersionsBuilder>();
        if ( autoLinkItems )
        {
            final PropertyVersionsBuilder[] propertyVersionsBuilders;
            try
            {
                propertyVersionsBuilders = PomHelper.getPropertyVersionsBuilders( this, project );
            }
            catch ( ExpressionEvaluationException e )
            {
                throw new MojoExecutionException( e.getMessage(), e );
            }
            catch ( IOException e )
            {
                throw new MojoExecutionException( e.getMessage(), e );
            }

            for ( PropertyVersionsBuilder propertyVersionsBuilder : propertyVersionsBuilders )
            {
                final String name = propertyVersionsBuilder.getName();
                builders.put( name, propertyVersionsBuilder );
                if ( !properties.containsKey( name ) )
                {
                    final Property value = new Property( name );
                    getLog().debug( "Property ${" + name + "}: Adding inferred version range of "
                        + propertyVersionsBuilder.getVersionRange() );
                    value.setVersion( propertyVersionsBuilder.getVersionRange() );
                    properties.put( name, value );
                }
            }
        }

        List<String> includePropertiesList = getSplittedProperties( includeProperties );
        List<String> excludePropertiesList = getSplittedProperties( excludeProperties );

        getLog().debug( "Searching for properties associated with builders" );
        Iterator<Property> i = properties.values().iterator();
        while ( i.hasNext() )
        {
            Property property = i.next();

            getLog().debug( "includePropertiesList:" + includePropertiesList + " property: " + property.getName() );
            getLog().debug( "excludePropertiesList:" + excludePropertiesList + " property: " + property.getName() );
            if ( !includePropertiesList.isEmpty() && !includePropertiesList.contains( property.getName() ) )
            {
                getLog().debug( "Skipping property ${" + property.getName() + "}" );
                i.remove();
            }
            else if ( !excludePropertiesList.isEmpty() && excludePropertiesList.contains( property.getName() ) )
            {
                getLog().debug( "Ignoring property ${" + property.getName() + "}" );
                i.remove();
            }
        }
        i = properties.values().iterator();
        Map<Property, PropertyVersions> propertyVersions =
            new LinkedHashMap<Property, PropertyVersions>( properties.size() );
        while ( i.hasNext() )
        {
            Property property = i.next();
            getLog().debug( "Property ${" + property.getName() + "}" );
            PropertyVersionsBuilder builder = builders.get( property.getName() );
            if ( builder == null || !builder.isAssociated() )
            {
                getLog().debug( "Property ${" + property.getName() + "}: Looks like this property is not "
                    + "associated with any dependency..." );
                builder = new PropertyVersionsBuilder( null, property.getName(), this );
            }
            if ( !property.isAutoLinkDependencies() )
            {
                getLog().debug( "Property ${" + property.getName() + "}: Removing any autoLinkDependencies" );
                builder.clearAssociations();
            }
            Dependency[] dependencies = property.getDependencies();
            if ( dependencies != null )
            {
                for ( Dependency dependency : dependencies )
                {
                    try
                    {
                        getLog().debug( "Property ${" + property.getName() + "}: Adding association to " + dependency );
                        builder.addAssociation( this.createDependencyArtifact( dependency ), false );
                    }
                    catch ( InvalidVersionSpecificationException e )
                    {
                        throw new MojoExecutionException( e.getMessage(), e );
                    }
                }
            }
            try
            {
                final PropertyVersions versions = builder.newPropertyVersions();
                if ( property.isAutoLinkDependencies() && StringUtils.isEmpty( property.getVersion() )
                    && !StringUtils.isEmpty( builder.getVersionRange() ) )
                {
                    getLog().debug( "Property ${" + property.getName() + "}: Adding inferred version range of "
                        + builder.getVersionRange() );
                    property.setVersion( builder.getVersionRange() );
                }
                versions.setCurrentVersion( project.getProperties().getProperty( property.getName() ) );
                propertyVersions.put( property, versions );
            }
            catch ( ArtifactMetadataRetrievalException e )
            {
                throw new MojoExecutionException( e.getMessage(), e );
            }
        }
        return propertyVersions;
    }

    private List<String> getSplittedProperties( String commaSeparatedProperties )
    {
        List<String> propertiesList = Collections.emptyList();
        if ( StringUtils.isNotEmpty( commaSeparatedProperties ) )
        {
            String[] splittedProps = StringUtils.split( commaSeparatedProperties, "," );
            propertiesList = Arrays.asList( StringUtils.stripAll( splittedProps ) );
        }
        return propertiesList;
    }

    // This is a data container to hold the result of a Dependency lookup to its ArtifactVersions.
    private static class DependencyArtifactVersions
    {
        private final Dependency dependency;

        private final ArtifactVersions artifactVersions;

        public DependencyArtifactVersions( final Dependency dependency, final ArtifactVersions artifactVersions )
        {
            this.dependency = dependency;
            this.artifactVersions = artifactVersions;
        }

        public Dependency getDependency()
        {
            return dependency;
        }

        public ArtifactVersions getArtifactVersions()
        {
            return artifactVersions;
        }
    }

    // This is a data container to hold the result of a Dependency lookup to its ArtifactVersions.
    private static class PluginPluginUpdatesDetails
    {
        private final Plugin plugin;

        private final PluginUpdatesDetails pluginUpdatesDetails;

        public PluginPluginUpdatesDetails( final Plugin plugin, final PluginUpdatesDetails pluginUpdatesDetails )
        {
            this.plugin = plugin;
            this.pluginUpdatesDetails = pluginUpdatesDetails;
        }

        public Plugin getPlugin()
        {
            return plugin;
        }

        public PluginUpdatesDetails getPluginUpdatesDetails()
        {
            return pluginUpdatesDetails;
        }
    }

    // This Callable wraps lookupDependencyUpdates so that it can be run in parallel.
    private class DependencyLookup
        implements Callable<DependencyArtifactVersions>
    {
        private final Dependency dependency;

        private final boolean usePluginRepositories;

        public DependencyLookup( final Dependency dependency, final boolean usePluginRepositories )
        {
            this.dependency = dependency;
            this.usePluginRepositories = usePluginRepositories;
        }

        public DependencyArtifactVersions call()
            throws Exception
        {
            return new DependencyArtifactVersions( dependency,
                                                   lookupDependencyUpdates( dependency, usePluginRepositories ) );
        }
    }

    // This Callable wraps lookupPluginUpdates so that it can be run in parallel.
    private class PluginLookup
        implements Callable<PluginPluginUpdatesDetails>
    {
        private final Plugin plugin;

        private final boolean allowSnapshots;

        public PluginLookup( final Plugin plugin, final Boolean allowSnapshots )
        {
            this.plugin = plugin;
            this.allowSnapshots = allowSnapshots;
        }

        public PluginPluginUpdatesDetails call()
            throws Exception
        {
            return new PluginPluginUpdatesDetails( plugin, lookupPluginUpdates( plugin, allowSnapshots ) );
        }
    }

}
