package org.ops4j.pax.construct.clone;
/*
* Copyright 2007 Stuart McCulloch
*
* 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.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.ops4j.pax.construct.util.DirUtils;
import org.ops4j.pax.construct.util.PomUtils;
import org.ops4j.pax.construct.util.PomUtils.Pom;
/**
* Clones an existing project and produces a script (plus archetypes) to mimic its structure using Pax-Construct
*
* <code><pre>
* mvn pax:clone
* </pre></code>
*
* @goal clone
* @aggregator true
*/
public class CloneMojo extends AbstractMojo
{
/**
* Component factory for various archivers
*
* @component
*/
private ArchiverManager m_archiverManager;
/**
* Initiating groupId.
*
* @parameter expression="${project.groupId}"
* @required
* @readonly
*/
private String m_rootGroupId;
/**
* Initiating artifactId.
*
* @parameter expression="${project.artifactId}"
* @required
* @readonly
*/
private String m_rootArtifactId;
/**
* Initiating base directory.
*
* @parameter expression="${project.basedir}"
* @required
* @readonly
*/
private File m_basedir;
/**
* Temporary directory, where scripts and templates will be saved.
*
* @parameter expression="${project.build.directory}/clone"
* @required
* @readonly
*/
private File m_tempdir;
/**
* The current Maven reactor.
*
* @parameter expression="${reactorProjects}"
* @required
* @readonly
*/
private List m_reactorProjects;
/**
* When true, replace any local bundle dependencies with pax-import-bundle commands.
*
* @parameter expression="${repair}"
*/
private boolean repair;
/**
* When true, unify multiple projects under one single pax-create-project command. Warning: this doesn't merge
* settings from sub-projects, such as <dependencyManagement>, so some manually editing will be necessary.
*
* @parameter expression="${unify}"
*/
private boolean unify;
/**
* List of directories that have already been processed
*/
private List m_handledDirs;
/**
* Maps Maven POMs to pax-create-project commands
*/
private Map m_majorProjectMap;
/**
* Maps groupId:artifactId to local bundle names
*/
private Map m_bundleNameMap;
/**
* Sequence of archetypes with project/bundle content
*/
private List m_installCommands;
/**
* {@inheritDoc}
*/
public void execute()
throws MojoExecutionException
{
// general purpose Pax-Construct script
PaxScript buildScript = new PaxScriptImpl();
m_bundleNameMap = new HashMap();
m_majorProjectMap = new HashMap();
m_handledDirs = new ArrayList();
m_installCommands = new ArrayList();
getFragmentDir().mkdirs();
for( Iterator i = m_reactorProjects.iterator(); i.hasNext(); )
{
// potential project to be converted / captured
MavenProject project = (MavenProject) i.next();
String packaging = project.getPackaging();
// fixup standalone maven project
if( m_reactorProjects.size() == 1 )
{
// always repair
repair = true;
// provide basic jar conversion
if( "jar".equals( packaging ) )
{
packaging = "bundle";
}
}
if( "bundle".equals( packaging ) )
{
handleBundleProject( buildScript, project );
}
else if( "pom".equals( packaging ) )
{
if( isMajorProject( project ) )
{
handleMajorProject( buildScript, project );
}
else
{
handleBundleImport( buildScript, project );
}
}
// else handled by the major project(s)
}
// grab everything else
archiveMajorProjects();
writePlatformScripts( buildScript );
}
/**
* Write out various platform-specific scripts based on the abstract build script
*
* @param script build script
*/
private void writePlatformScripts( PaxScript script )
{
String cloneId = PomUtils.getCompoundId( m_rootGroupId, m_rootArtifactId );
String scriptName = "create-" + cloneId;
File winScript = new File( m_tempdir, scriptName + ".bat" );
File nixScript = new File( m_tempdir, scriptName + ".sh" );
getLog().info( "" );
getLog().info( "SUCCESSFULLY CLONED " + cloneId );
getLog().info( "" );
String title = m_rootGroupId + ':' + m_rootArtifactId;
try
{
getLog().info( "Saving UNIX shell script " + nixScript );
script.write( title, nixScript, m_installCommands );
}
catch( IOException e )
{
getLog().warn( "Unable to write " + nixScript );
}
try
{
getLog().info( "Saving Windows batch file " + winScript );
script.write( title, winScript, m_installCommands );
}
catch( IOException e )
{
getLog().warn( "Unable to write " + winScript );
}
getLog().info( "" );
getLog().info( "CLONE DIRECTORY " + m_tempdir );
getLog().info( "" );
getLog().info( "(this directory can be zipped and shared with other team members)" );
}
/**
* Analyze major project and build the right pax-create-project call
*
* @param script build script
* @param project major Maven project
*/
private void handleMajorProject( PaxScript script, MavenProject project )
{
if( unify && !project.isExecutionRoot() )
{
// exclude the local poms settings from the unified project
m_handledDirs.add( new File( project.getBasedir(), "poms" ) );
return;
}
PaxCommandBuilder command = script.call( PaxScript.CREATE_PROJECT );
command.option( 'g', project.getGroupId() );
command.option( 'a', project.getArtifactId() );
command.option( 'v', project.getVersion() );
setTargetDirectory( command, project.getBasedir().getParentFile() );
registerProject( project );
m_majorProjectMap.put( project, command );
}
/**
* Analyze bundle project and determine if any pax-create-bundle or pax-wrap-jar calls are needed
*
* @param script build script
* @param project Maven bundle project
* @throws MojoExecutionException
*/
private void handleBundleProject( PaxScript script, MavenProject project )
throws MojoExecutionException
{
PaxCommandBuilder command;
String bundleName;
String namespace = findBundleNamespace( project );
if( null != namespace )
{
bundleName = project.getArtifactId();
command = script.call( PaxScript.CREATE_BUNDLE );
command.option( 'p', namespace );
if( !bundleName.equals( namespace ) )
{
command.option( 'n', bundleName );
}
command.option( 'v', project.getVersion() );
command.maven().flag( "noDeps" );
}
else
{
Dependency wrappee = findWrappee( project );
if( wrappee != null )
{
command = script.call( PaxScript.WRAP_JAR );
command.option( 'g', wrappee.getGroupId() );
command.option( 'a', wrappee.getArtifactId() );
command.option( 'v', wrappee.getVersion() );
if( repair )
{
// this is expected to be the generated bundle name
bundleName = PomUtils.getCompoundId( wrappee.getGroupId(), wrappee.getArtifactId() );
// detect if we need to add the version back later on...
if( project.getArtifactId().endsWith( wrappee.getVersion() ) )
{
command.maven().flag( "addVersion" );
}
}
else
{
bundleName = project.getArtifactId();
// need to retain the old name and version settings
command.maven().option( "bundleName", bundleName );
command.maven().option( "bundleVersion", project.getVersion() );
}
}
else
{
getLog().warn( "Unable to clone bundle project " + project.getId() );
return;
}
}
Pom customizedPom = null;
if( repair )
{
// fix references to local bundles by re-importing
customizedPom = repairBundleImports( script, project );
}
else
{
// need to keep old groupId intact (name is already retained)
command.maven().option( "bundleGroupId", project.getGroupId() );
}
addFragmentToCommand( command, createBundleArchetype( project, namespace, customizedPom ) );
setTargetDirectory( command, project.getBasedir().getParentFile() );
registerProject( project );
registerBundleName( project, bundleName );
}
/**
* Analyze POM project and determine if any pax-import-bundle calls are needed
*
* @param script build script
* @param project Maven POM project
*/
private void handleBundleImport( PaxScript script, MavenProject project )
{
Dependency importee = findImportee( project );
if( importee != null )
{
PaxCommandBuilder command;
command = script.call( PaxScript.IMPORT_BUNDLE );
command.option( 'g', importee.getGroupId() );
command.option( 'a', importee.getArtifactId() );
command.option( 'v', importee.getVersion() );
// enable overwrite
command.flag( 'o' );
// imported bundles now in provision POM
setTargetDirectory( command, m_basedir );
registerProject( project );
}
// else handled by the major project
}
/**
* Analyze the position of this project in the tree, as not all projects need their own distinct set of "poms"
*
* @param project Maven POM project
* @return true if this project requires a pax-create-project call
*/
private boolean isMajorProject( MavenProject project )
{
if( project.isExecutionRoot() )
{
// top-most project
return true;
}
// disconnected project, or project with its own set of "poms" where settings can be customized
return ( null == project.getParent() || new File( project.getBasedir(), "poms" ).isDirectory() );
}
/**
* Analyze bundle project to see if it actually just wraps another artifact
*
* @param project Maven bundle project
* @return wrapped artifact, null if it isn't a wrapper project
*/
private Dependency findWrappee( MavenProject project )
{
Properties properties = project.getProperties();
Dependency wrappee = new Dependency();
// Pax-Construct v2
wrappee.setGroupId( properties.getProperty( "wrapped.groupId" ) );
wrappee.setArtifactId( properties.getProperty( "wrapped.artifactId" ) );
wrappee.setVersion( properties.getProperty( "wrapped.version" ) );
if( null == wrappee.getArtifactId() )
{
// original Pax-Construct
wrappee.setGroupId( properties.getProperty( "jar.groupId" ) );
wrappee.setArtifactId( properties.getProperty( "jar.artifactId" ) );
wrappee.setVersion( properties.getProperty( "jar.version" ) );
if( null == wrappee.getArtifactId() )
{
// has someone customized their wrapper?
return findCustomizedWrappee( project );
}
}
return wrappee;
}
/**
* Analyze project structure to try to deduce if this really is a wrapper
*
* @param project Maven bundle project
* @return wrapped artifact, null if it isn't a wrapper project
*/
private Dependency findCustomizedWrappee( MavenProject project )
{
List dependencies = project.getDependencies();
String sourcePath = project.getBuild().getSourceDirectory();
// try to find a dependency that relates to the wrapper project
if( dependencies.size() > 0 && !new File( sourcePath ).exists() )
{
for( Iterator i = dependencies.iterator(); i.hasNext(); )
{
Dependency dependency = (Dependency) i.next();
if( project.getArtifactId().indexOf( dependency.getArtifactId() ) >= 0 )
{
return dependency; // closest match
}
}
return (Dependency) dependencies.get( 0 );
}
return null;
}
/**
* Analyze POM project to see if it actually just imports an existing bundle
*
* @param project Maven POM project
* @return imported bundle, null if it isn't a import project
*/
private Dependency findImportee( MavenProject project )
{
Properties properties = project.getProperties();
Dependency importee = new Dependency();
// original Pax-Construct
importee.setGroupId( properties.getProperty( "bundle.groupId" ) );
importee.setArtifactId( properties.getProperty( "bundle.artifactId" ) );
importee.setVersion( properties.getProperty( "bundle.version" ) );
if( importee.getArtifactId() != null )
{
return importee;
}
return null;
}
/**
* Analyze bundle project to find the primary namespace it provides
*
* @param project Maven project
* @return primary Java namespace
*/
private String findBundleNamespace( MavenProject project )
{
Properties properties = project.getProperties();
// Pax-Construct v2
String namespace = properties.getProperty( "bundle.namespace" );
if( null == namespace )
{
// original Pax-Construct
namespace = properties.getProperty( "bundle.package" );
String sourcePath = project.getBuild().getSourceDirectory();
if( null == namespace && new File( sourcePath ).exists() )
{
namespace = findPrimaryPackage( sourcePath );
}
}
return namespace;
}
/**
* Find the most likely candidate for the primary Java package
*
* @param dir source directory
* @return primary Java package
*/
private String findPrimaryPackage( String dir )
{
String[] pathInclude = new String[]
{
"**/*.java" // consider all Java sources
};
DirectoryScanner scanner = new DirectoryScanner();
scanner.setIncludes( pathInclude );
scanner.setFollowSymlinks( false );
scanner.addDefaultExcludes();
scanner.setBasedir( dir );
scanner.scan();
String path = null;
String[] candidates = scanner.getIncludedFiles();
for( int i = 0; i < scanner.getIncludedFiles().length; i++ )
{
String newPath = candidates[i];
if( null == path || ( validCandidate( path, newPath ) && path.length() > newPath.length() ) )
{
path = newPath;
}
}
return getJavaNamespace( path );
}
/**
* @param current current primary java path
* @param candidate candidate java path
* @return true if the candidate might be a better primary java path, otherwise false
*/
private boolean validCandidate( String current, String candidate )
{
// internal packages are good candidates to find the primary package
String candidateFolder = new File( candidate ).getParentFile().getName();
if( "internal".equalsIgnoreCase( candidateFolder ) || "impl".equalsIgnoreCase( candidateFolder ) )
{
return true;
}
// but if we already have an internal package, ignore non-internal packages
String currentFolder = new File( current ).getParentFile().getName();
if( "internal".equalsIgnoreCase( currentFolder ) || "impl".equalsIgnoreCase( currentFolder ) )
{
return false;
}
// neither is an internal package, so candidate package may be better
return true;
}
/**
* Convert source code location into dotted Java namespace
*
* @param javaFile source location
* @return Java namespace
*/
private String getJavaNamespace( String javaFile )
{
if( null == javaFile )
{
return null;
}
// strip the classname and any internal package
File packageDir = new File( javaFile ).getParentFile();
if( "internal".equals( packageDir.getName() ) || "impl".equals( packageDir.getName() ) )
{
packageDir = packageDir.getParentFile();
}
// standard slashes to dots conversion
return packageDir.getPath().replaceAll( "[/\\\\]+", "." );
}
/**
* Set the directory where the Pax-Construct command should be run
*
* @param command Pax-Construct command
* @param targetDir target directory
*/
private void setTargetDirectory( PaxCommandBuilder command, File targetDir )
{
String[] pivot = DirUtils.calculateRelativePath( m_basedir.getParentFile(), targetDir );
if( pivot != null && pivot[0].length() == 0 && pivot[2].length() > 0 )
{
// fix path to use the correct artifactId, in case directory tree has been renamed
String relativePath = StringUtils.replaceOnce( pivot[2], m_basedir.getName(), m_rootArtifactId );
command.maven().option( "targetDirectory", relativePath );
}
}
/**
* Create a new archetype for a bundle project, with potentially customized POM and Bnd settings
*
* @param project Maven project
* @param namespace Java namespace, may be null
* @param customizedPom customized Maven project model, may be null
* @return clause identifying the archetype fragment
* @throws MojoExecutionException
*/
private String createBundleArchetype( MavenProject project, String namespace, Pom customizedPom )
throws MojoExecutionException
{
File baseDir = project.getBasedir();
getLog().info( "Cloning bundle project " + project.getArtifactId() );
ArchetypeFragment fragment = new ArchetypeFragment( getFragmentDir(), namespace, false );
fragment.addPom( baseDir, customizedPom );
if( null != namespace )
{
fragment.addSources( baseDir, project.getBuild().getSourceDirectory(), false );
fragment.addSources( baseDir, project.getBuild().getTestSourceDirectory(), true );
}
for( Iterator i = project.getTestResources().iterator(); i.hasNext(); )
{
Resource r = (Resource) i.next();
fragment.addResources( baseDir, r.getDirectory(), r.getIncludes(), r.getExcludes(), true );
}
List excludes = new ArrayList();
excludes.addAll( fragment.getIncludedFiles() );
excludes.add( "target/" );
excludes.add( "runner/" );
excludes.add( "pom.xml" );
// consider everything else in the bundle directory to be a resource
fragment.addResources( baseDir, baseDir.getPath(), null, excludes, false );
// archetype must use different id
String groupId = project.getGroupId();
String artifactId = project.getArtifactId() + "-archetype";
String version = project.getVersion();
// archive customized bundle sources, POM and Bnd instructions
String fragmentId = groupId + ':' + artifactId + ':' + version;
fragment.createArchive( fragmentId.replace( ':', '_' ), newJarArchiver() );
return fragmentId;
}
/**
* Attempt to repair bundle imports by replacing them with pax-import-bundle commands
*
* @param script build script
* @param project Maven project
* @return customized Maven project model
*/
private Pom repairBundleImports( PaxScript script, MavenProject project )
{
Pom pom;
try
{
File tempFile = File.createTempFile( "pom", ".xml", m_tempdir );
FileUtils.copyFile( project.getFile(), tempFile );
pom = PomUtils.readPom( tempFile );
tempFile.deleteOnExit();
}
catch( IOException e )
{
pom = null;
}
for( Iterator i = project.getDependencies().iterator(); i.hasNext(); )
{
Dependency dependency = (Dependency) i.next();
String bundleId = dependency.getGroupId() + ':' + dependency.getArtifactId();
String bundleName = (String) m_bundleNameMap.get( bundleId );
// is this a local bundle project?
if( null != bundleName )
{
PaxCommandBuilder command;
command = script.call( PaxScript.IMPORT_BUNDLE );
command.option( 'a', bundleName );
// enable overwrite
command.flag( 'o' );
// only apply to the importing bundle project
setTargetDirectory( command, project.getBasedir() );
if( null != pom )
{
pom.removeDependency( dependency );
}
}
}
try
{
if( null != pom )
{
pom.write();
}
return pom;
}
catch( IOException e )
{
return null;
}
}
/**
* Create archetype fragments for all recorded major projects (as well as any non-bundle modules)
*
* @throws MojoExecutionException
*/
private void archiveMajorProjects()
throws MojoExecutionException
{
for( Iterator i = m_majorProjectMap.entrySet().iterator(); i.hasNext(); )
{
Map.Entry entry = (Map.Entry) i.next();
MavenProject project = (MavenProject) entry.getKey();
addFragmentToCommand( (PaxCommandBuilder) entry.getValue(), createProjectArchetype( project ) );
}
}
/**
* Archive all the selected resources under a single Maven archetype
*
* @param project containing Maven project
* @return clause identifying the archetype fragment
* @throws MojoExecutionException
*/
private String createProjectArchetype( MavenProject project )
throws MojoExecutionException
{
File baseDir = project.getBasedir();
getLog().info( "Cloning primary project " + project.getArtifactId() );
ArchetypeFragment fragment = new ArchetypeFragment( getFragmentDir(), null, unify );
fragment.addPom( baseDir, null );
List excludes = new ArrayList();
excludes.addAll( getExcludedPaths( project ) );
excludes.add( "**/target/" );
excludes.add( "runner/" );
excludes.add( "pom.xml" );
// consider everything else that's not been handled to be a resource
fragment.addResources( baseDir, baseDir.getPath(), null, excludes, false );
// archetype must use different id
String groupId = project.getGroupId();
String artifactId = project.getArtifactId() + "-archetype";
String version = project.getVersion();
// archive all the customized non-bundle POMs and projects
String fragmentId = groupId + ':' + artifactId + ':' + version;
fragment.createArchive( fragmentId.replace( ':', '_' ), newJarArchiver() );
return fragmentId;
}
/**
* Find which paths in this Maven project have already been collected, and should therefore be excluded
*
* @param project major Maven project
* @return list of excluded paths
*/
private List getExcludedPaths( MavenProject project )
{
List excludes = new ArrayList();
File baseDir = project.getBasedir();
for( Iterator i = m_handledDirs.iterator(); i.hasNext(); )
{
File dir = (File) i.next();
String[] pivot = DirUtils.calculateRelativePath( baseDir, dir );
if( pivot != null && pivot[0].length() == 0 && pivot[2].length() > 0 )
{
excludes.add( pivot[2] );
}
}
return excludes;
}
/**
* @return new Jar archiver
* @throws MojoExecutionException
*/
private Archiver newJarArchiver()
throws MojoExecutionException
{
try
{
return m_archiverManager.getArchiver( "jar" );
}
catch( NoSuchArchiverException e )
{
throw new MojoExecutionException( "Unable to find Jar archiver", e );
}
}
/**
* @return temporary fragment directory
*/
private File getFragmentDir()
{
return new File( m_tempdir, "fragments" );
}
/**
* @param project Maven project
*/
private void registerProject( MavenProject project )
{
m_handledDirs.add( project.getBasedir() );
}
/**
* @param project Maven bundle project
* @param bundleName expected symbolic name of the bundle (null if imported)
*/
private void registerBundleName( MavenProject project, String bundleName )
{
m_bundleNameMap.put( project.getGroupId() + ':' + project.getArtifactId(), bundleName );
}
/**
* @param command one of the create commands
* @param fragmentId archetype fragment id
*/
private void addFragmentToCommand( PaxCommandBuilder command, String fragmentId )
{
if( null == fragmentId )
{
return;
}
command.maven().option( "contents", fragmentId );
StringBuffer buffer = new StringBuffer();
String[] ids = fragmentId.split( ":" );
// add Maven command to install the archetype fragment before using it
buffer.append( "mvn -N install:install-file \"-Dpackaging=jar\" \"-DgroupId=" );
buffer.append( ids[0] );
buffer.append( "\" \"-DartifactId=" );
buffer.append( ids[1] );
buffer.append( "\" \"-Dversion=" );
buffer.append( ids[2] );
buffer.append( "\" \"-Dfile=${_SCRIPTDIR_}/fragments/" );
buffer.append( fragmentId.replace( ':', '_' ) );
buffer.append( ".jar\"" );
m_installCommands.add( buffer );
}
}