/*
* Copyright (c) 2012 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;
import static com.redhat.rcm.version.mgr.mod.ProjectModder.IMPLIED_MODIFICATIONS;
import static com.redhat.rcm.version.util.InputUtils.getIncludedSubpaths;
import static com.redhat.rcm.version.util.PomUtils.writeModifiedPom;
import static org.apache.commons.io.IOUtils.closeQuietly;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.maven.mae.MAEException;
import org.apache.maven.mae.app.AbstractMAEApplication;
import org.apache.maven.mae.boot.embed.MAEEmbedderBuilder;
import org.apache.maven.mae.internal.container.ComponentSelector;
import org.apache.maven.mae.project.ProjectToolsException;
import org.apache.maven.mae.project.key.FullProjectKey;
import org.apache.maven.mae.project.key.ProjectKey;
import org.apache.maven.mae.project.key.VersionlessProjectKey;
import org.apache.maven.mae.project.session.SessionInitializer;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.FileModelSource;
import org.apache.maven.model.building.ModelBuilder;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingResult;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.aether.impl.ArtifactResolver;
import org.sonatype.aether.impl.RemoteRepositoryManager;
import com.redhat.rcm.version.VManException;
import com.redhat.rcm.version.config.SessionConfigurator;
import com.redhat.rcm.version.maven.VManModelResolver;
import com.redhat.rcm.version.mgr.capture.MissingInfoCapture;
import com.redhat.rcm.version.mgr.mod.ProjectModder;
import com.redhat.rcm.version.mgr.session.VersionManagerSession;
import com.redhat.rcm.version.mgr.verify.ProjectVerifier;
import com.redhat.rcm.version.model.Project;
import com.redhat.rcm.version.report.Report;
import com.redhat.rcm.version.util.PomPeek;
@Component( role = VersionManager.class )
public class VersionManager
extends AbstractMAEApplication
{
private final Logger logger = LoggerFactory.getLogger( getClass() );
@Requirement
private ModelBuilder modelBuilder;
@Requirement
private ArtifactResolver artifactResolver;
@Requirement
private RemoteRepositoryManager remoteRepositoryManager;
@Requirement( role = Report.class )
private Map<String, Report> reports;
@Requirement( role = ProjectModder.class )
private Map<String, ProjectModder> modders;
@Requirement( role = ProjectVerifier.class )
private Map<String, ProjectVerifier> verifiers;
@Requirement
private MissingInfoCapture capturer;
@Requirement
private SessionConfigurator sessionConfigurator;
private DefaultModelBuildingRequest baseMbr;
private static boolean useClasspathScanning = false;
private static Object lock = new Object();
private static VersionManager instance;
public static VersionManager getInstance()
throws MAEException
{
synchronized ( lock )
{
if ( instance == null )
{
instance = new VersionManager();
instance.load();
}
}
return instance;
}
public void generateReports( final File reportsDir, final VersionManagerSession sessionData )
{
if ( reports != null )
{
final Set<String> ids = new HashSet<String>();
for ( final Map.Entry<String, Report> entry : reports.entrySet() )
{
final String id = entry.getKey();
final Report report = entry.getValue();
if ( !id.endsWith( "_" ) )
{
try
{
ids.add( id );
report.generate( reportsDir, sessionData );
}
catch ( final VManException e )
{
logger.error( "Failed to generate report: " + id, e );
}
}
}
logger.info( "Wrote reports: [" + StringUtils.join( ids.iterator(), ", " ) + "] to:\n\t" + reportsDir );
}
}
public Set<File> modifyVersions( final File dir, final String pomNamePattern, final String pomExcludePattern, final List<String> boms,
final String toolchain, final VersionManagerSession session )
throws VManException
{
final String[] includedSubpaths = getIncludedSubpaths( dir, pomNamePattern, pomExcludePattern, session );
final List<File> pomFiles = new ArrayList<File>();
for ( final String subpath : includedSubpaths )
{
File pom = new File( dir, subpath );
try
{
pom = pom.getCanonicalFile();
}
catch ( final IOException e )
{
pom = pom.getAbsoluteFile();
}
if ( !pomFiles.contains( pom ) )
{
logger.info( "Loading POM: '" + pom + "'" );
pomFiles.add( pom );
}
}
final File[] pomFileArray = pomFiles.toArray( new File[] {} );
configureSession( boms, toolchain, session, pomFileArray );
final Set<File> outFiles = modVersions( dir, session, session.isPreserveFiles(), pomFileArray );
logger.info( "Modified " + outFiles.size() + " POM versions in directory.\n\n\tDirectory: " + dir + "\n\tBOMs:\t"
+ StringUtils.join( boms.iterator(), "\n\t\t" ) + "\n\tPOM Backups: " + session.getBackups() + "\n\n" );
return outFiles;
}
public Set<File> modifyVersions( File pom, final List<String> boms, final String toolchain, final VersionManagerSession session )
throws VManException
{
try
{
pom = pom.getCanonicalFile();
}
catch ( final IOException e )
{
pom = pom.getAbsoluteFile();
}
configureSession( boms, toolchain, session, pom );
final Set<File> result = modVersions( pom.getParentFile(), session, true, pom );
if ( !result.isEmpty() )
{
final File out = result.iterator()
.next();
logger.info( "Modified POM versions.\n\n\tTop POM: " + out + "\n\tBOMs:\t"
+ ( boms == null ? "-NONE-" : StringUtils.join( boms.iterator(), "\n\t\t" ) ) + "\n\tPOM Backups: " + session.getBackups() + "\n\n" );
}
return result;
}
public void configureSession( final List<String> boms, final String toolchain, final VersionManagerSession session, final File... pomFiles )
throws VManException
{
sessionConfigurator.configureSession( boms, toolchain, session, pomFiles );
final List<Throwable> errors = session.getErrors();
if ( errors != null && !errors.isEmpty() )
{
throw new MultiVManException( "Failed to configure session.", errors );
}
}
protected LinkedHashSet<Project> loadProjectWithModules( final File topPom, final VersionManagerSession session )
throws ProjectToolsException, IOException
{
final List<PomPeek> peeked = peekAtPomHierarchy( topPom, session );
final LinkedHashSet<Project> projects = new LinkedHashSet<Project>();
for ( final PomPeek peek : peeked )
{
final File pom = peek.getPom();
// Sucks, but we have to brute-force reading in the raw model.
// The effective-model building, below, has a tantalizing getRawModel()
// method on the result, BUT this seems to return models that have
// the plugin versions set inside profiles...so they're not entirely
// raw.
Model raw = null;
InputStream in = null;
try
{
in = new FileInputStream( pom );
raw = new MavenXpp3Reader().read( in );
}
catch ( final IOException e )
{
session.addError( new VManException( "Failed to build model for POM: %s.\n--> %s", e, pom, e.getMessage() ) );
}
catch ( final XmlPullParserException e )
{
session.addError( new VManException( "Failed to build model for POM: %s.\n--> %s", e, pom, e.getMessage() ) );
}
finally
{
closeQuietly( in );
}
if ( raw == null )
{
continue;
}
final Project project;
if ( session.isUseEffectivePoms() )
{
// FIXME: Need an option to disable this for self-contained use cases...
// Is this the same as 'non-strict' mode??
final ModelBuildingRequest req = newModelBuildingRequest( pom, session );
ModelBuildingResult mbResult = null;
try
{
mbResult = modelBuilder.build( req );
}
catch ( final ModelBuildingException e )
{
session.addError( new VManException( "Failed to build model for POM: %s.\n--> %s", e, pom, e.getMessage() ) );
}
if ( mbResult == null )
{
continue;
}
project = new Project( raw, mbResult, pom );
}
else
{
project = new Project( pom, raw );
}
projects.add( project );
}
return projects;
}
protected List<PomPeek> peekAtPomHierarchy( final File topPom, final VersionManagerSession session )
throws IOException
{
final LinkedList<File> pendingPoms = new LinkedList<File>();
pendingPoms.add( topPom.getCanonicalFile() );
final String topDir = topPom.getParentFile()
.getCanonicalPath();
final Set<File> seen = new HashSet<File>();
final List<PomPeek> peeked = new ArrayList<PomPeek>();
while ( !pendingPoms.isEmpty() )
{
final File pom = pendingPoms.removeFirst();
seen.add( pom );
logger.info( "PEEK: " + pom );
final PomPeek peek = new PomPeek( pom );
final FullProjectKey key = peek.getKey();
if ( key != null )
{
session.addPeekPom( key, pom );
peeked.add( peek );
final File dir = pom.getParentFile();
final String relPath = peek.getParentRelativePath();
if ( relPath != null )
{
logger.info( "Found parent relativePath: " + relPath + " in pom: " + pom );
File parent = new File( dir, relPath );
if ( parent.isDirectory() )
{
parent = new File( parent, "pom.xml" );
}
logger.info( "Looking for parent POM: " + parent );
parent = parent.getCanonicalFile();
if ( parent.getParentFile()
.getCanonicalPath()
.startsWith( topDir ) && parent.exists() && !seen.contains( parent ) && !pendingPoms.contains( parent ) )
{
pendingPoms.add( parent );
}
else
{
logger.info( "Skipping reference to non-existent parent relativePath: '" + relPath + "' in: " + pom );
}
}
final Set<String> modules = peek.getModules();
if ( modules != null && !modules.isEmpty() )
{
for ( final String module : modules )
{
logger.info( "Found module: " + module + " in pom: " + pom );
File modPom = new File( dir, module );
if ( modPom.isDirectory() )
{
modPom = new File( modPom, "pom.xml" );
}
logger.info( "Looking for module POM: " + modPom );
if ( modPom.getParentFile()
.getCanonicalPath()
.startsWith( topDir ) && modPom.exists() && !seen.contains( modPom ) && !pendingPoms.contains( modPom ) )
{
pendingPoms.addLast( modPom );
}
else
{
logger.info( "Skipping reference to non-existent module: '" + module + "' in: " + pom );
}
}
}
}
else
{
logger.info( "Skipping " + pom + " as its a template file." );
}
}
return peeked;
}
private synchronized ModelBuildingRequest newModelBuildingRequest( final File pom, final VersionManagerSession session )
{
if ( baseMbr == null )
{
final DefaultModelBuildingRequest mbr = new DefaultModelBuildingRequest();
mbr.setSystemProperties( System.getProperties() );
mbr.setValidationLevel( ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL );
mbr.setProcessPlugins( false );
mbr.setLocationTracking( true );
this.baseMbr = mbr;
}
final DefaultModelBuildingRequest req = new DefaultModelBuildingRequest( baseMbr );
req.setModelSource( new FileModelSource( pom ) );
final VManModelResolver resolver = new VManModelResolver( session, pom, artifactResolver, remoteRepositoryManager );
req.setModelResolver( resolver );
return req;
}
private Set<File> modVersions( final File basedir, final VersionManagerSession session, final boolean preserveDirs, final File... pomFiles )
{
final Set<File> result = new LinkedHashSet<File>();
final Set<Project> projects = new HashSet<Project>();
for ( final File pom : pomFiles )
{
if ( session.isExcludedModulePom( pom ) )
{
logger.info( "Skipping excluded module pom: {}.", pom );
continue;
}
try
{
final Set<Project> pomProjects = loadProjectWithModules( pom, session );
if ( pomProjects != null && !pomProjects.isEmpty() )
{
projects.addAll( pomProjects );
}
}
catch ( final ProjectToolsException e )
{
session.addError( e );
}
catch ( final IOException e )
{
session.addError( e );
}
}
if ( !session.getErrors()
.isEmpty() )
{
return result;
}
session.setCurrentProjects( projects );
logger.info( "Modifying " + projects.size() + " project(s)..." );
// NOTE: Using sorted projects list from session instead of unsorted set from above.
for ( final Project project : session.getCurrentProjects() )
{
logger.info( "Modifying '" + project.getKey() + "'..." );
List<String> modderKeys = session.getModderKeys();
modderKeys = calculateActualModderKeys( modderKeys );
Collections.sort( modderKeys, ProjectModder.KEY_COMPARATOR );
boolean changed = false;
if ( modders != null )
{
for ( final String key : modderKeys )
{
final ProjectModder modder = modders.get( key );
if ( modder == null )
{
logger.info( "Skipping missing project modifier: '" + key + "'" );
session.addError( new VManException( "Cannot find modder for key: '%s'. Skipping...", key ) );
continue;
}
logger.info( "Modifying '" + project.getKey() + " using: '" + key + "' with modder " + modder.getClass()
.getName() );
changed = modder.inject( project, session ) || changed;
}
}
if ( changed )
{
for ( final String key : modderKeys )
{
final ProjectVerifier verifier = verifiers.get( key );
if ( verifier != null )
{
logger.info( "Verifying '" + project.getKey() + "' (" + key + ") with verifier " + verifier.getClass()
.getName() );
verifier.verify( project, session );
}
}
logger.info( "Writing modified '" + project.getKey() + "'..." );
final Model model = project.getModel();
final Parent parent = model.getParent();
String groupId = model.getGroupId();
String originalVersion = model.getVersion();
if ( parent != null )
{
if ( groupId == null )
{
groupId = parent.getGroupId();
}
if ( originalVersion == null )
{
originalVersion = parent.getVersion();
}
}
final ProjectKey originalCoord = new VersionlessProjectKey( groupId, model.getArtifactId() );
final File pom = project.getPom();
final File out = writePom( model, originalCoord, originalVersion, pom, basedir, session, preserveDirs );
if ( out != null )
{
result.add( out );
}
}
else
{
logger.info( project.getKey() + " NOT modified." );
}
}
if ( session.getCapturePom() != null )
{
capturer.captureMissing( session );
logger.warn( "\n\n\n\nMissing version information has been logged to:\n\n\t" + session.getCapturePom()
.getAbsolutePath() + "\n\n\n\n" );
}
return result;
}
private List<String> calculateActualModderKeys( final List<String> modders )
{
final List<String> keys = new ArrayList<String>( modders );
for ( final String key : modders )
{
final Set<String> implications = IMPLIED_MODIFICATIONS.get( key );
if ( implications != null )
{
final int idx = keys.indexOf( key );
for ( final String implied : implications )
{
if ( !keys.contains( implied ) )
{
keys.add( idx, implied );
}
}
}
}
return keys;
}
private File writePom( final Model model, final ProjectKey originalCoord, final String originalVersion, final File pom, final File basedir,
final VersionManagerSession session, final boolean preserveDirs )
{
File backup = pom;
final File backupDir = session.getBackups();
if ( backupDir != null )
{
String path = pom.getParent();
path = path.substring( basedir.getPath()
.length() );
final File dir = new File( backupDir, path );
if ( !dir.exists() && !dir.mkdirs() )
{
session.addError( new VManException( "Failed to create backup subdirectory: %s", dir ) );
return null;
}
backup = new File( dir, pom.getName() );
try
{
session.getLog( pom )
.add( "Writing: %s\nTo backup: %s", pom, backup );
FileUtils.copyFile( pom, backup );
}
catch ( final IOException e )
{
session.addError( new VManException( "Error making backup of POM: %s.\n\tTarget: %s\n\tReason: %s", e, pom, backup, e.getMessage() ) );
return null;
}
}
String version = model.getVersion();
String groupId = model.getGroupId();
if ( model.getParent() != null )
{
final Parent parent = model.getParent();
if ( version == null )
{
version = parent.getVersion();
}
if ( groupId == null )
{
groupId = parent.getGroupId();
}
}
boolean relocatePom = false;
final ProjectKey coord = new VersionlessProjectKey( groupId, model.getArtifactId() );
if ( !preserveDirs && ( !coord.equals( originalCoord ) || !version.equals( originalVersion ) ) )
{
relocatePom = true;
}
return writeModifiedPom( model, pom, coord, version, basedir, session, relocatePom );
}
@Override
public String getId()
{
return "rh.vmod";
}
@Override
public String getName()
{
return "Red Hat POM Version Modifier";
}
@Override
protected void configureBuilder( final MAEEmbedderBuilder builder )
throws MAEException
{
super.configureBuilder( builder );
if ( useClasspathScanning )
{
builder.withClassScanningEnabled( true );
}
}
public static void setClasspathScanning( final boolean scanning )
{
if ( instance == null )
{
useClasspathScanning = scanning;
}
}
public Map<String, ProjectModder> getModders()
{
return modders;
}
public Map<String, Report> getReports()
{
return reports;
}
@Override
public ComponentSelector getComponentSelector()
{
return new ComponentSelector().setSelection( SessionInitializer.class, "vman" );
}
}