package org.codehaus.mojo.pomtools.helpers; /* * Copyright 2005-2006 The Apache Software Foundation. * * Licensed 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.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.ResolutionNode; import org.codehaus.mojo.pomtools.PomToolsPluginContext; import org.codehaus.mojo.pomtools.PomToolsException; import org.codehaus.mojo.pomtools.versioning.DefaultVersionInfo; import org.codehaus.mojo.pomtools.versioning.VersionInfo; import org.codehaus.mojo.pomtools.wrapper.custom.ModelVersionRange; import org.codehaus.plexus.util.StringUtils; /** This class is used my {@link org.codehaus.mojo.pomtools.helpers.MetadataHelper#getTransitiveDependencies()}. * An instance is created for every possible transitive dependency (whether it is used or not). * The {@link #getResolutionNodes()} represent each possible path that this groupId:artifactId:type * are included into a project. The "selectedNode" is the actual resolutionNode instance that maven * is using during a build. * * @author <a href="mailto:dhawkins@codehaus.org">David Hawkins</a> * @version $Id$ */ public class TransitiveDependencyInfo { private ResolutionNode selectedNode; private boolean hasConflicts = false; private final List resolutionNodes = new ArrayList(); public TransitiveDependencyInfo( ResolutionNode node ) { this.selectedNode = node; } public String toString() { return getKey(); } public Artifact getSelectedArtifact() { return selectedNode.getArtifact(); } public List getResolutionNodes() { Collections.sort( resolutionNodes, new Comparator() { public int compare( Object arg0, Object arg1 ) { return ( (ResolutionNode) arg0 ).getDepth() - ( (ResolutionNode) arg1 ).getDepth(); } } ); return Collections.unmodifiableList( resolutionNodes ); } /** Adds a resolution node to the list. If the artifact version * from the new node differs from the artifact version of the "selectedNode", * this object is noted as having conflicting versions. * <p> * If the version strings do not match literally, * A version range is created and the item being added is * tested to see if it's range includes the "selectedNode" version. * If so, it is not considered a conflict. * * @param pathNode */ public void addResolutionNode( ResolutionNode pathNode ) { // If the pathNode isn't a direct project dependency, and it is optional, it will // never get included into the build so ignore it. if ( pathNode.getDepth() > 1 && pathNode.getArtifact().isOptional() ) { return; } resolutionNodes.add( pathNode ); if ( !StringUtils.equals( selectedNode.getArtifact().getVersion(), pathNode.getArtifact().getVersion() ) ) { ModelVersionRange newRange = new ModelVersionRange( pathNode.getArtifact().getVersionRange() ); if ( newRange.hasRestrictions() ) { hasConflicts = hasConflicts || !newRange.containsVersion( selectedNode.getArtifact().getVersion() ); } else { // The newly added version isn't a range, so see if it is a snapshot build // Snapshots have their fully qualified build numbers appended to them at this point, // So 2 different snapshots can have different string equality, but still represent the same version. // If it is a snapshot, then compare the release version of the string to the release version // of the selectedNode. VersionInfo selectVersion = new DefaultVersionInfo( selectedNode.getArtifact().getVersion() ); VersionInfo newVersion = new DefaultVersionInfo( pathNode.getArtifact().getVersion() ); if ( selectVersion.isParsed() && newVersion.isParsed() ) { if ( selectVersion.isSnapshot() && newVersion.isSnapshot() ) { hasConflicts = hasConflicts || !StringUtils.equals( selectVersion.getReleaseVersionString(), newVersion.getReleaseVersionString() ); } else { hasConflicts = true; } } else { hasConflicts = true; } } } } /** Returns the node used in the constructor which represents the * actual dependency that was resolved for the project. * * @return */ public ResolutionNode getSelectedNode() { return selectedNode; } public String getKey() { return (String) selectedNode.getKey(); } /** Returns wheter any of the possible transitive dependencies * for this groupId:artifactId:type have a different version than * the "selectedNode" which maven is using for the build. */ public boolean hasConflicts() { return hasConflicts; } /** Returns a sorted list of distinct versions from all of the ResolutionNodes * and the number of occurances. * * @return a list of {@link VersionCount} objects */ public List getDistinctVersionCounts() { List infoList = new ArrayList(); List unparsedList = new ArrayList(); CountMap countMap = new CountMap(); // Create a list of VersionInfo objects so we can sort them. for ( Iterator i = resolutionNodes.iterator(); i.hasNext(); ) { ResolutionNode node = (ResolutionNode) i.next(); ModelVersionRange vRange = new ModelVersionRange( node.getArtifact().getVersionRange() ); if ( vRange.hasRestrictions() ) { if ( countMap.increment( vRange.toString() ) ) { unparsedList.add( vRange.toString() ); } } else { VersionInfo vInfo = new DefaultVersionInfo( StringUtils.defaultString( vRange.toString(), "(unknown)" ) ); if ( vInfo.isParsed() ) { // Add it to our parsed list if ( countMap.increment( vInfo.getVersionString() ) ) { infoList.add( vInfo ); } } else { // Add it to the unparsed list if ( countMap.increment( vRange.toString() ) ) { unparsedList.add( vRange.toString() ); PomToolsPluginContext.getInstance().getLog().warn( "Dependency contained an unparsable " + "version: \"" + node.getArtifact().getVersion() + "\" " + node.getKey() ); } } } } List result = new ArrayList( infoList.size() ); // Add all the unparsed versions for ( Iterator iter = unparsedList.iterator(); iter.hasNext(); ) { String version = (String) iter.next(); result.add( new VersionCount( version, countMap.get( version ) ) ); } // Add the sorted parsed versions Collections.sort( infoList ); for ( Iterator iter = infoList.iterator(); iter.hasNext(); ) { VersionInfo info = (VersionInfo) iter.next(); result.add( new VersionCount( info.getVersionString(), countMap.get( info.getVersionString() ) ) ); } return result; } /** Returns an object tree of all of the possible paths which can transitively include * this artifact. The root immediate children of the root node of the resulting tree will * be the directly included dependencies which lead to this artifact. * * @return * @throws PomToolsException */ public TreeNode getInclusionTree() throws PomToolsException { TreeNode tree = new TreeNode( null, new Comparator() { public int compare( Object arg0, Object arg1 ) { ResolutionNode n0 = (ResolutionNode) arg0; ResolutionNode n1 = (ResolutionNode) arg1; return ( (Comparable) n0.getKey() ).compareTo( n1.getKey() ); } } ); for ( Iterator iter = getResolutionNodes().iterator(); iter.hasNext(); ) { ResolutionNode node = (ResolutionNode) iter.next(); List depTrail = APIWorkaroundHelper.getNodeLineage( node ); TreeNode currentTree = tree; for ( Iterator trailIter = depTrail.iterator(); trailIter.hasNext(); ) { // currentTree is reassigned each time to the tree of the newly added child currentTree = currentTree.addChild( trailIter.next() ); } } if ( !tree.hasChildren() ) { // this shouldn't happen throw new PomToolsException( "Unable to determine lineage of transitive dependency" ); } return tree.getSingleChild(); } public static class VersionCount { private final String version; private final int count; VersionCount( String version, int count ) { this.version = version; this.count = count; } public int getCount() { return count; } public String getVersion() { return version; } } /** Simple helper class which assists in incrementing an integer for each key. */ private static class CountMap { private final Map counts = new HashMap(); public CountMap() { } /** Adds or Increments the Integer in the map for the key. * * @param countMap * @param key * @return true if the item was created rather than updated */ public boolean increment( Object key ) { if ( counts.containsKey( key ) ) { Integer cnt = (Integer) counts.get( key ); counts.put( key, new Integer( cnt.intValue() + 1 ) ); return false; } else { counts.put( key, new Integer( 1 ) ); return true; } } public int get( Object key ) { Integer result = (Integer) counts.get( key ); if ( result == null ) { throw new IllegalArgumentException( "Key was not found in the map: " + key.toString() ); } return result.intValue(); } } }