org.ops4j.pax.construct.clone.CloneMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.ops4j.pax.construct.clone.CloneMojo.java

Source

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 &lt;dependencyManagement&gt;, 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);
    }
}