/* * Copyright (c) 2011 Red Hat, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * <http://www.gnu.org/licenses>. */ package com.redhat.rcm.version.mgr.mod; import static com.redhat.rcm.version.mgr.mod.Interpolations.interpolate; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.mae.project.key.FullProjectKey; import org.apache.maven.mae.project.key.VersionlessProjectKey; import org.apache.maven.model.Dependency; import org.apache.maven.model.DependencyManagement; import org.apache.maven.model.Exclusion; import org.apache.maven.model.Model; import org.apache.maven.model.ModelBase; import org.apache.maven.model.Profile; import org.codehaus.plexus.component.annotations.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.redhat.rcm.version.mgr.session.VersionManagerSession; import com.redhat.rcm.version.model.DependencyManagementKey; import com.redhat.rcm.version.model.Project; import com.redhat.rcm.version.model.ReadOnlyDependency; @Component( role = ProjectModder.class, hint = "bom-realignment" ) public class BomModder implements ProjectModder { private final Logger logger = LoggerFactory.getLogger( getClass() ); @Override public String getDescription() { return "Forcibly realign dependencies to use those declared in the supplied BOM file(s). Inject supplied BOM(s) into the project root POM."; } @Override public boolean inject( final Project project, final VersionManagerSession session ) { final Model model = project.getModel(); final List<ModelBase> bases = new ArrayList<ModelBase>(); bases.add( model ); final List<Profile> profiles = model.getProfiles(); if ( profiles != null && !profiles.isEmpty() ) { bases.addAll( profiles ); } final File pom = project.getPom(); boolean changed = false; for ( final ModelBase base : bases ) { logger.info( "Processing: {} in model: {}", base, model ); DependencyManagement dm = null; if ( base.getDependencies() != null ) { logger.info( "Processing dependencies for '" + project.getKey() + "'..." ); for ( final Iterator<Dependency> it = base.getDependencies() .iterator(); it.hasNext(); ) { final Dependency dep = it.next(); logger.info( "Processing: {}", dep ); final DepModResult depResult = modifyDep( dep, project, pom, session, false ); if ( depResult == DepModResult.DELETED ) { logger.info( "Removing: {}", dep ); it.remove(); changed = true; } else { if ( depResult == DepModResult.MODIFIED ) { logger.info( "Modified {}", dep ); } else { logger.info( "NO CHANGE to: {}", dep ); } changed = DepModResult.MODIFIED == depResult || changed; } } } if ( session.isStrict() ) { dm = base.getDependencyManagement(); if ( base.getDependencyManagement() != null && dm.getDependencies() != null ) { logger.info( "Processing dependencyManagement for '" + project.getKey() + "'..." ); for ( final Iterator<Dependency> it = dm.getDependencies() .iterator(); it.hasNext(); ) { final Dependency dep = it.next(); logger.info( "Processing: {}", dep ); final DepModResult depResult = modifyDep( dep, project, pom, session, true ); if ( depResult == DepModResult.DELETED ) { logger.info( "Removing: {}", dep ); it.remove(); changed = true; } else { if ( depResult == DepModResult.MODIFIED ) { logger.info( "Modified {}", dep ); } else { logger.info( "NO CHANGE to: {}", dep ); } changed = DepModResult.MODIFIED == depResult || changed; } } } } } // NOTE: Inject BOMs directly, but ONLY if the parent project is NOT in // the current projects list. (If the parent is a current project, we // want to inject the BOMs there instead.) final List<FullProjectKey> bomCoords = session.getBomCoords(); logger.info( "{} BOMs available for injection...is my parent ({}) being modified in this session? {}", bomCoords.size(), project.getParent(), session.isCurrentProject( project.getParent() ) ); if ( !session.isCurrentProject( project.getParent() ) && bomCoords != null && !bomCoords.isEmpty() ) { logger.info( "Injecting BOMs..." ); DependencyManagement dm = model.getDependencyManagement(); if ( dm == null ) { dm = new DependencyManagement(); model.setDependencyManagement( dm ); } // Used to track inserting the BOMs in the correct order in the dependencyMgmt // section. int insertCounter = 0; for ( final FullProjectKey bomCoord : bomCoords ) { final Dependency dep = new Dependency(); dep.setGroupId( bomCoord.getGroupId() ); dep.setArtifactId( bomCoord.getArtifactId() ); dep.setVersion( bomCoord.getVersion() ); dep.setType( "pom" ); dep.setScope( Artifact.SCOPE_IMPORT ); logger.info( "Adding BOM: {} at index: {} of {}", dep, insertCounter, model ); changed = true; dm.getDependencies() .add( insertCounter++, dep ); } } else if ( !session.isStrict() && model.getDependencyManagement() != null ) { model.setDependencyManagement( null ); changed = true; } return changed; } private Dependency interpolateDep( final Dependency d, final Project project ) { Dependency dep = d.clone(); dep.setGroupId( interpolate( d.getGroupId(), project ) ); dep.setArtifactId( interpolate( d.getArtifactId(), project ) ); if ( dep.getVersion() != null ) { dep.setVersion( interpolate( d.getVersion(), project ) ); } if ( dep.getExclusions() != null && !dep.getExclusions() .isEmpty() ) { for ( final Exclusion ex : dep.getExclusions() ) { ex.setGroupId( interpolate( ex.getGroupId(), project ) ); ex.setArtifactId( interpolate( ex.getArtifactId(), project ) ); } } // Interpolation is done, now LOCK IT DOWN! dep = new ReadOnlyDependency( dep ); return dep; } private DepModResult modifyDep( final Dependency d, final Project project, final File pom, final VersionManagerSession session, final boolean isManaged ) { DepModResult result = DepModResult.UNCHANGED; final Dependency dep = interpolateDep( d, project ); if ( project.getParent() == null && session.isBom( new FullProjectKey( dep ) ) ) { return result; } VersionlessProjectKey key = new VersionlessProjectKey( dep.getGroupId(), dep.getArtifactId() ); if ( session.isCurrentProject( key ) ) { logger.info( "NOT CHANGING version for interdependency from current project set: " + key ); session.getLog( pom ) .add( "NOT changing version for: %s%s. This is an interdependency in the current project set.", key, isManaged ? " [MANAGED]" : "" ); return result; } final FullProjectKey newKey = session.getRelocation( key ); if ( newKey != null && !key.equals( newKey ) ) { logger.info( "Relocating dependency: " + key + " to: " + newKey ); session.addRelocatedCoordinate( pom, key, newKey ); d.setGroupId( newKey.getGroupId() ); d.setArtifactId( newKey.getArtifactId() ); d.setVersion( newKey.getVersion() ); result = DepModResult.MODIFIED; key = new VersionlessProjectKey( d ); } else { logger.info( "No relocation available for: " + key ); } final String version = d.getVersion(); if ( version == null ) { session.getLog( pom ) .add( "NOT changing version for: %s%s. Version is inherited.", key, isManaged ? " [MANAGED]" : "" ); return result; } Dependency managed = session.getManagedDependency( new DependencyManagementKey( d ) ); if ( managed == null ) { // if we don't find the one with the specific type/classifier, look for the generic one // if we find that, we can list the specific one as a missing dep managed = session.getManagedDependency( new DependencyManagementKey( d.getGroupId(), d.getArtifactId() ) ); if ( managed != null ) { session.addMissingDependency( project, d ); } // in non-strict mode, we can make more assumptions. if ( session.isStrict() ) { // now, reset it to null so we don't change the way the next section works in the absence of a matching BOM-managed dep. managed = null; } } // If in non-strict mode (default), wipe it out even if the dependency isn't in the BOM // ...assume it will be added from the capture POM. if ( managed != null || !session.isStrict() ) { d.setVersion( null ); final Dependency target = d.clone(); if ( managed != null ) { target.setVersion( managed.getVersion() ); } if ( isManaged ) { // TODO: Is this right?? Shouldn't we be looking at the relocated, interpolated one?? // if ( !overridesManagedInfo( dep, managed ) ) if ( !overridesManagedInfo( d, managed ) ) { result = DepModResult.DELETED; } else { d.setVersion( session.replacePropertyVersion( project, d.getGroupId(), d.getArtifactId(), d.getType(), d.getClassifier() ) ); result = DepModResult.MODIFIED; } } session.addDependencyModification( project.getVersionlessKey(), dep, target ); } if ( managed == null ) { // if we're in strict mode and this is a BOM, don't add it to the missing list. if ( !session.isStrict() || !"pom".equals( dep.getType() ) || !"import".equals( dep.getScope() ) ) { // log this dependency as missing from the BOM(s) to can be captured and added. session.addMissingDependency( project, dep ); } } return result; } /** * Determines whether dependency dep overrides the mananged dependency (i.e. * is different). * * @param dep * @param managed * @return true if dep overrides. */ private boolean overridesManagedInfo( final Dependency dep, final Dependency managed ) { String depScope = dep.getScope(); if ( depScope == null ) { depScope = "compile"; } String mgdScope = managed.getScope(); if ( mgdScope == null ) { mgdScope = "compile"; } if ( !depScope.equals( mgdScope ) ) { return true; } final Set<ComparableExclusion> depExclusions = new HashSet<ComparableExclusion>(); final Set<ComparableExclusion> mgdExclusions = new HashSet<ComparableExclusion>(); List<Exclusion> dEx = dep.getExclusions(); if ( dEx == null ) { dEx = Collections.emptyList(); } for ( final Exclusion exclusion : dEx ) { depExclusions.add( new ComparableExclusion( exclusion ) ); } List<Exclusion> mEx = managed.getExclusions(); if ( mEx == null ) { mEx = Collections.emptyList(); } for ( final Exclusion exclusion : mEx ) { mgdExclusions.add( new ComparableExclusion( exclusion ) ); } if ( depExclusions.isEmpty() ) { return false; } // Compare to see if the exclusions are the same...if not, then the list differs and the local dep is overriding the managed one. if ( !mgdExclusions.equals( depExclusions ) ) { return true; } return false; } private static enum DepModResult { UNCHANGED, MODIFIED, DELETED; } private static class ComparableExclusion extends Exclusion { private static final long serialVersionUID = 8470840709131335264L; public ComparableExclusion( final Exclusion e ) { super(); super.setGroupId( e.getGroupId() ); super.setArtifactId( e.getArtifactId() ); } @Override public boolean equals( final Object obj ) { if ( this == obj ) { return true; } if ( obj == null ) { return false; } if ( !( obj instanceof Exclusion ) ) { return false; } final ComparableExclusion other = (ComparableExclusion) obj; if ( getArtifactId() == null ) { if ( other.getArtifactId() != null ) { return false; } } else if ( !getArtifactId().equals( other.getArtifactId() ) ) { return false; } if ( getGroupId() == null ) { if ( other.getGroupId() != null ) { return false; } } else if ( !getGroupId().equals( other.getGroupId() ) ) { return false; } return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ( ( getArtifactId() == null ) ? 0 : getArtifactId().hashCode() ); result = prime * result + ( ( getGroupId() == null ) ? 0 : getGroupId().hashCode() ); return result; } @Override public String toString() { return "Exclusion : " + getGroupId() + ":" + getArtifactId(); } } }