package org.apache.maven.shared.dependency.tree; /* * 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.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.Map; import java.util.Stack; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.ResolutionListener; import org.apache.maven.artifact.resolver.ResolutionListenerForDepMgmt; import org.apache.maven.artifact.versioning.VersionRange; import org.codehaus.plexus.logging.Logger; /** * An artifact resolution listener that constructs a dependency tree. * * @author Edwin Punzalan * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a> * @version $Id: DependencyTreeResolutionListener.java 661727 2008-05-30 14:21:49Z bentmann $ */ public class DependencyTreeResolutionListener implements ResolutionListener, ResolutionListenerForDepMgmt { // fields ----------------------------------------------------------------- /** * The log to write debug messages to. */ private final Logger logger; /** * The parent dependency nodes of the current dependency node. */ private final Stack parentNodes; /** * A map of dependency nodes by their attached artifact. */ private final Map nodesByArtifact; /** * The root dependency node of the computed dependency tree. */ private DependencyNode rootNode; /** * The dependency node currently being processed by this listener. */ private DependencyNode currentNode; /** * Map < String replacementId, String premanaged version > */ private Map managedVersions = new HashMap(); /** * Map < String replacementId, String premanaged scope > */ private Map managedScopes = new HashMap(); // constructors ----------------------------------------------------------- /** * Creates a new dependency tree resolution listener that writes to the specified log. * * @param logger * the log to write debug messages to */ public DependencyTreeResolutionListener( Logger logger ) { this.logger = logger; parentNodes = new Stack(); nodesByArtifact = new IdentityHashMap(); rootNode = null; currentNode = null; } // ResolutionListener methods --------------------------------------------- /** * {@inheritDoc} */ public void testArtifact( Artifact artifact ) { log( "testArtifact: artifact=" + artifact ); } /** * {@inheritDoc} */ public void startProcessChildren( Artifact artifact ) { log( "startProcessChildren: artifact=" + artifact ); if ( !currentNode.getArtifact().equals( artifact ) ) { throw new IllegalStateException( "Artifact was expected to be " + currentNode.getArtifact() + " but was " + artifact ); } parentNodes.push( currentNode ); } /** * {@inheritDoc} */ public void endProcessChildren( Artifact artifact ) { DependencyNode node = ( DependencyNode ) parentNodes.pop(); log( "endProcessChildren: artifact=" + artifact ); if ( node == null ) { throw new IllegalStateException( "Parent dependency node was null" ); } if ( !node.getArtifact().equals( artifact ) ) { throw new IllegalStateException( "Parent dependency node artifact was expected to be " + node.getArtifact() + " but was " + artifact ); } } /** * {@inheritDoc} */ public void includeArtifact( Artifact artifact ) { log( "includeArtifact: artifact=" + artifact ); DependencyNode existingNode = getNode( artifact ); /* * Ignore duplicate includeArtifact calls since omitForNearer can be called prior to includeArtifact on the same * artifact, and we don't wish to include it twice. */ if ( existingNode == null && isCurrentNodeIncluded() ) { DependencyNode node = addNode( artifact ); /* * Add the dependency management information cached in any prior manageArtifact calls, since includeArtifact * is always called after manageArtifact. */ flushDependencyManagement( node ); } } /** * {@inheritDoc} */ public void omitForNearer( Artifact omitted, Artifact kept ) { log( "omitForNearer: omitted=" + omitted + " kept=" + kept ); if ( !omitted.getDependencyConflictId().equals( kept.getDependencyConflictId() ) ) { throw new IllegalArgumentException( "Omitted artifact dependency conflict id " + omitted.getDependencyConflictId() + " differs from kept artifact dependency conflict id " + kept.getDependencyConflictId() ); } if ( isCurrentNodeIncluded() ) { DependencyNode omittedNode = getNode( omitted ); if ( omittedNode != null ) { removeNode( omitted ); } else { omittedNode = createNode( omitted ); currentNode = omittedNode; } omittedNode.omitForConflict( kept ); /* * Add the dependency management information cached in any prior manageArtifact calls, since omitForNearer * is always called after manageArtifact. */ flushDependencyManagement( omittedNode ); DependencyNode keptNode = getNode( kept ); if ( keptNode == null ) { addNode( kept ); } } } /** * {@inheritDoc} */ public void updateScope( Artifact artifact, String scope ) { log( "updateScope: artifact=" + artifact + ", scope=" + scope ); DependencyNode node = getNode( artifact ); if ( node == null ) { // updateScope events can be received prior to includeArtifact events node = addNode( artifact ); } node.setOriginalScope( artifact.getScope() ); } /** * {@inheritDoc} */ public void manageArtifact( Artifact artifact, Artifact replacement ) { // TODO: remove when ResolutionListenerForDepMgmt merged into ResolutionListener log( "manageArtifact: artifact=" + artifact + ", replacement=" + replacement ); if ( replacement.getVersion() != null ) { manageArtifactVersion( artifact, replacement ); } if ( replacement.getScope() != null ) { manageArtifactScope( artifact, replacement ); } } /** * {@inheritDoc} */ public void omitForCycle( Artifact artifact ) { log( "omitForCycle: artifact=" + artifact ); if ( isCurrentNodeIncluded() ) { DependencyNode node = createNode( artifact ); node.omitForCycle(); } } /** * {@inheritDoc} */ public void updateScopeCurrentPom( Artifact artifact, String scopeIgnored ) { log( "updateScopeCurrentPom: artifact=" + artifact + ", scopeIgnored=" + scopeIgnored ); DependencyNode node = getNode( artifact ); if ( node == null ) { // updateScopeCurrentPom events can be received prior to includeArtifact events node = addNode( artifact ); // TODO remove the node that tried to impose its scope and add some info } node.setFailedUpdateScope( scopeIgnored ); } /** * {@inheritDoc} */ public void selectVersionFromRange( Artifact artifact ) { log( "selectVersionFromRange: artifact=" + artifact ); DependencyNode node = getNode( artifact ); /* * selectVersionFromRange is called before includeArtifact */ if ( node == null && isCurrentNodeIncluded() ) { node = addNode( artifact ); } node.setVersionSelectedFromRange( artifact.getVersionRange() ); node.setAvailableVersions( artifact.getAvailableVersions() ); } /** * {@inheritDoc} */ public void restrictRange( Artifact artifact, Artifact replacement, VersionRange versionRange ) { log( "restrictRange: artifact=" + artifact + ", replacement=" + replacement + ", versionRange=" + versionRange ); // TODO: track range restriction in node (MNG-3093) } // ResolutionListenerForDepMgmt methods ----------------------------------- /** * {@inheritDoc} */ public void manageArtifactVersion( Artifact artifact, Artifact replacement ) { log( "manageArtifactVersion: artifact=" + artifact + ", replacement=" + replacement ); /* * DefaultArtifactCollector calls manageArtifact twice: first with the change; then subsequently with no change. * We ignore the second call when the versions are equal. */ if ( isCurrentNodeIncluded() && !replacement.getVersion().equals( artifact.getVersion() ) ) { /* * Cache management information and apply in includeArtifact, since DefaultArtifactCollector mutates the * artifact and then calls includeArtifact after manageArtifact. */ managedVersions.put( getRangeId( replacement ), artifact.getVersion() ); } } /** * {@inheritDoc} */ public void manageArtifactScope( Artifact artifact, Artifact replacement ) { log( "manageArtifactScope: artifact=" + artifact + ", replacement=" + replacement ); /* * DefaultArtifactCollector calls manageArtifact twice: first with the change; then subsequently with no change. * We ignore the second call when the scopes are equal. */ if ( isCurrentNodeIncluded() && !replacement.getScope().equals( artifact.getScope() ) ) { /* * Cache management information and apply in includeArtifact, since DefaultArtifactCollector mutates the * artifact and then calls includeArtifact after manageArtifact. */ managedScopes.put( getRangeId( replacement ), artifact.getScope() ); } } // public methods --------------------------------------------------------- /** * Gets a list of all dependency nodes in the computed dependency tree. * * @return a list of dependency nodes * @deprecated As of 1.1, use a CollectingDependencyNodeVisitor on the root dependency node */ public Collection getNodes() { return Collections.unmodifiableCollection( nodesByArtifact.values() ); } /** * Gets the root dependency node of the computed dependency tree. * * @return the root node */ public DependencyNode getRootNode() { return rootNode; } // private methods -------------------------------------------------------- /** * Writes the specified message to the log at debug level with indentation for the current node's depth. * * @param message * the message to write to the log */ private void log( String message ) { int depth = parentNodes.size(); StringBuffer buffer = new StringBuffer(); for ( int i = 0; i < depth; i++ ) { buffer.append( " " ); } buffer.append( message ); logger.debug( buffer.toString() ); } /** * Creates a new dependency node for the specified artifact and appends it to the current parent dependency node. * * @param artifact * the attached artifact for the new dependency node * @return the new dependency node */ private DependencyNode createNode( Artifact artifact ) { DependencyNode node = new DependencyNode( artifact ); if ( !parentNodes.isEmpty() ) { DependencyNode parent = ( DependencyNode ) parentNodes.peek(); parent.addChild( node ); } return node; } /** * Creates a new dependency node for the specified artifact, appends it to the current parent dependency node and * puts it into the dependency node cache. * * @param artifact * the attached artifact for the new dependency node * @return the new dependency node */ // package protected for unit test DependencyNode addNode( Artifact artifact ) { DependencyNode node = createNode( artifact ); DependencyNode previousNode = ( DependencyNode ) nodesByArtifact.put( node.getArtifact(), node ); if ( previousNode != null ) { throw new IllegalStateException( "Duplicate node registered for artifact: " + node.getArtifact() ); } if ( rootNode == null ) { rootNode = node; } currentNode = node; return node; } /** * Gets the dependency node for the specified artifact from the dependency node cache. * * @param artifact * the artifact to find the dependency node for * @return the dependency node, or <code>null</code> if the specified artifact has no corresponding dependency * node */ private DependencyNode getNode( Artifact artifact ) { return ( DependencyNode ) nodesByArtifact.get( artifact ); } /** * Removes the dependency node for the specified artifact from the dependency node cache. * * @param artifact * the artifact to remove the dependency node for */ private void removeNode( Artifact artifact ) { DependencyNode node = ( DependencyNode ) nodesByArtifact.remove( artifact ); if ( !artifact.equals( node.getArtifact() ) ) { throw new IllegalStateException( "Removed dependency node artifact was expected to be " + artifact + " but was " + node.getArtifact() ); } } /** * Gets whether the all the ancestors of the dependency node currently being processed by this listener have an * included state. * * @return <code>true</code> if all the ancestors of the current dependency node have a state of * <code>INCLUDED</code> */ private boolean isCurrentNodeIncluded() { boolean included = true; for ( Iterator iterator = parentNodes.iterator(); included && iterator.hasNext(); ) { DependencyNode node = ( DependencyNode ) iterator.next(); if ( node.getState() != DependencyNode.INCLUDED ) { included = false; } } return included; } /** * Updates the specified node with any dependency management information cached in prior <code>manageArtifact</code> * calls. * * @param node * the node to update */ private void flushDependencyManagement( DependencyNode node ) { Artifact artifact = node.getArtifact(); String premanagedVersion = ( String ) managedVersions.get( getRangeId( artifact ) ); String premanagedScope = ( String ) managedScopes.get( getRangeId( artifact ) ); if ( premanagedVersion != null || premanagedScope != null ) { if ( premanagedVersion != null ) { node.setPremanagedVersion( premanagedVersion ); } if ( premanagedScope != null ) { node.setPremanagedScope( premanagedScope ); } premanagedVersion = null; premanagedScope = null; } } private static String getRangeId( Artifact artifact ) { return artifact.getDependencyConflictId() + ":" + artifact.getVersionRange(); } public void manageArtifactSystemPath( Artifact artifact, Artifact replacement ) { // NO-OP } }