org.eclipse.ebr.maven.BundleMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ebr.maven.BundleMojo.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Gunnar Wagenknecht and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Gunnar Wagenknecht - initial API and implementation
 *    Sonatype Inc. - methods for reading OSGi I10N properties from Tycho
 *    Brian de Alwis - handle artifacts that lack source
 *******************************************************************************/
package org.eclipse.ebr.maven;

import static aQute.bnd.osgi.Constants.CREATED_BY;
import static aQute.bnd.osgi.Constants.SNAPSHOT;
import static java.lang.String.format;
import static org.eclipse.ebr.maven.OsgiLocalizationUtil.I18N_KEY_BUNDLE_NAME;
import static org.eclipse.ebr.maven.OsgiLocalizationUtil.I18N_KEY_BUNDLE_VENDOR;
import static org.eclipse.ebr.maven.OsgiLocalizationUtil.I18N_KEY_PREFIX;
import static org.osgi.framework.Constants.BUNDLE_CLASSPATH;
import static org.osgi.framework.Constants.BUNDLE_LOCALIZATION;
import static org.osgi.framework.Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME;
import static org.osgi.framework.Constants.BUNDLE_MANIFESTVERSION;
import static org.osgi.framework.Constants.BUNDLE_NAME;
import static org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME;
import static org.osgi.framework.Constants.BUNDLE_VENDOR;
import static org.osgi.framework.Constants.BUNDLE_VERSION;
import static org.twdata.maven.mojoexecutor.MojoExecutor.artifactId;
import static org.twdata.maven.mojoexecutor.MojoExecutor.configuration;
import static org.twdata.maven.mojoexecutor.MojoExecutor.element;
import static org.twdata.maven.mojoexecutor.MojoExecutor.executeMojo;
import static org.twdata.maven.mojoexecutor.MojoExecutor.executionEnvironment;
import static org.twdata.maven.mojoexecutor.MojoExecutor.goal;
import static org.twdata.maven.mojoexecutor.MojoExecutor.groupId;
import static org.twdata.maven.mojoexecutor.MojoExecutor.name;
import static org.twdata.maven.mojoexecutor.MojoExecutor.plugin;
import static org.twdata.maven.mojoexecutor.MojoExecutor.version;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.ebr.maven.shared.BundleUtil;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.felix.bundleplugin.ManifestPlugin;
import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.rtinfo.RuntimeInformation;

import org.codehaus.plexus.archiver.FileSet;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;
import org.codehaus.plexus.util.AbstractScanner;
import org.codehaus.plexus.util.IOUtil;

import org.twdata.maven.mojoexecutor.MojoExecutor.Element;

/**
 * A Maven plug-on for downloading dependencies and re-packaging them as a
 * single OSGi bundle.
 */
@Mojo(name = "bundle", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
public class BundleMojo extends ManifestPlugin {

    private static final String CLASSIFIER_SOURCES = "sources";

    static boolean isRecipeProject(final MavenProject project) {
        return "eclipse-bundle-recipe".equals(project.getPackaging());
    }

    /**
     * The project output directory where all classes and resources will be
     * collected for generating the final bundle jar later.
     */
    @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true)
    protected File outputDirectory;

    /**
     * The directory for the generated JAR.
     */
    @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true)
    protected String buildDirectory;

    /** The directory for gathering and extracting dependencies. */
    @Parameter(defaultValue = "${project.build.directory}/dependency-bin", readonly = true, required = true)
    protected String dependenciesDirectory;

    /**
     * The directory for gathering and extracting sources of all dependencies
     */
    @Parameter(defaultValue = "${project.build.directory}/dependency-src", readonly = true, required = true)
    protected String dependenciesSourcesDirectory;

    /**
     * Include files from the project resource directory into the generated
     * source bundle JAR.
     */
    @Parameter(property = "includeProjectResourceDir", defaultValue = "true")
    protected boolean includeProjectResourceDir;

    /**
     * Indicates if dependencies should be unpacked (recommended).
     * <p>
     * If set to <code>false</code>, dependencies will be included as jar files
     * within the bundle and the <code>Bundle-ClassPath</code> header will be
     * populated.
     * </p>
     * <p>
     * Note that excludes and includes will be ignored when unpacking
     * dependencies is disabled.
     * </p>
     */
    @Parameter(property = "unpackDependencies", defaultValue = "true")
    protected boolean unpackDependencies;

    /**
     * Indicates if dependencies that are not unpacked should have their version
     * stripped from their file name.
     * <p>
     * If set to true, dependencies that are to be included as unpacked
     * artifacts within the bundle (<code>unpackDependencies</code> set to
     * <code>true</code>) will not include their version in the file name.
     * </p>
     */
    @Parameter(property = "stripVersion", defaultValue = "false")
    protected boolean stripVersion;

    /**
     * Prevents inclusion of '.' into the code>Bundle-ClassPath</code> header
     * when <code>unpackDependencies</code> is set to <code>false</code>.
     */
    @Parameter(property = "excludeDotFolderFromBundleClasspath", defaultValue = "false")
    protected boolean excludeDotFolderFromBundleClasspath;

    @Component
    private BuildPluginManager pluginManager;

    @Component
    private MavenProjectHelper projectHelper;

    @Component
    private RuntimeInformation mavenRuntimeInformation;

    /**
     * The instructions passed to BND for the bundle.
     */
    @Parameter
    protected Map<String, String> bndInstructions = new LinkedHashMap<String, String>();

    /**
     * A comma separated list of file patterns to include when unpacking the
     * artifacts. i.e. <code>**\/*.xml,**\/*.properties</code> NOTE: Excludes
     * patterns override the includes. (component code =
     * <code>return isIncluded( name ) AND !isExcluded( name );</code>)
     */
    @Parameter
    protected String includes;

    /**
     * A comma separated list of file patterns to exclude when unpacking the
     * artifacts. i.e. <code>**\/*.xml,**\/*.properties</code> NOTE: Excludes
     * patterns override the includes. (component code =
     * <code>return isIncluded( name ) AND !isExcluded( name );</code>)
     */
    @Parameter
    protected String excludes;

    /**
     * A comma separated list of artifactIds which should be exclude completely
     * from the processing. TEST dependencies will always be excluded.
     */
    @Parameter(defaultValue = "${excludeDependencies}")
    protected String excludeDependencies;

    /**
     * Name of the generated JAR.
     */
    @Parameter(defaultValue = "${project.build.finalName}", alias = "jarName", required = true)
    protected String finalName;

    /**
     * The maven archiver to use.
     */
    @Parameter
    private final MavenArchiveConfiguration archive = new MavenArchiveConfiguration();

    /**
     * Configuration of the source archive
     */
    @Parameter
    private final MavenArchiveConfiguration sourceArchive = new MavenArchiveConfiguration();

    /**
     * Build qualifier. Recommended way to set this parameter is using
     * build-qualifier goal.
     */
    @Parameter(defaultValue = "${buildQualifier}")
    protected String qualifier;

    /**
     * Set to 'eclipse' in order to sign using Eclipse.org signing service.
     * Possible values: <code>none</code>, <code>eclipse</code>
     */
    @Parameter(defaultValue = "none", property = "signingServiceType")
    protected String signingServiceType;

    @Parameter(defaultValue = "1.0.0-SNAPSHOT", property = "ebr-tycho-extras-plugin.version", required = true)
    protected String ebrTychoExtrasPluginVersionFallback;

    @Parameter(defaultValue = "0.26.0", property = "tycho-plugin.version", required = true)
    protected String tychoPluginVersionFallback;

    @Parameter(defaultValue = "0.26.0", property = "tycho-extras-plugin.version", required = true)
    protected String tychoExtrasPluginVersionFallback;

    @Parameter(defaultValue = "2.7", property = "maven-resource-plugin.version", required = true)
    protected String mavenResourcesPluginVersionFallback;

    @Parameter(defaultValue = "2.10", property = "maven-dependency-plugin.version", required = true)
    protected String mavenDependencyPluginVersionFallback;

    @Parameter(defaultValue = "1.1.3", property = "cbi-plugin.version", required = true)
    protected String cbiPluginVersionFallback;

    private File assembleJar(final String jarName, final File manifest, final File directory,
            final MavenArchiveConfiguration archiveConfiguration) throws MojoExecutionException {
        try {
            final MavenArchiver archiver = new MavenArchiver();
            archiver.setArchiver(new JarArchiver());

            final File jarFile = new File(buildDirectory, jarName);
            if (jarFile.exists()) {
                FileUtils.forceDelete(jarFile);
            }

            // 1. include all collected files
            archiver.getArchiver().addFileSet(getFileSet(directory));

            // 2. update the manifest
            if (manifest.exists()) {
                archiveConfiguration.setManifestFile(manifest);
            }

            archiver.setOutputFile(jarFile);
            if (!archiveConfiguration.isForced()) {
                // optimized archive creation not supported for now because of build qualifier mismatch issues
                // see TYCHO-502
                getLog().warn("ignoring unsupported archive forced = false parameter.");
                archiveConfiguration.setForced(true);
            }
            archiver.createArchive(session, project, archiveConfiguration);
            return jarFile;
        } catch (final Exception e) {
            throw new MojoExecutionException("Error assembling JAR " + jarName + ": " + e.getMessage(), e);
        }
    }

    private void assembleP2Repository() throws MojoExecutionException {
        // copy into output directory
        getLog().debug("Assembling p2 repository...");

        // @formatter:off
        executeMojo(
                plugin(groupId("org.eclipse.ebr"), artifactId("ebr-tycho-extras-plugin"),
                        version(detectPluginVersion("org.eclipse.ebr", "ebr-tycho-extras-plugin",
                                ebrTychoExtrasPluginVersionFallback))),
                goal("assemble-bundle-p2-repository"), configuration(),
                executionEnvironment(project, session, pluginManager));
        // @formatter:on
    }

    private void buildBundle(final Set<Artifact> dependencies) throws MojoExecutionException {
        // unpack dependencies
        getLog().info("Gathering dependencies");
        executeMavenDependenciesPluginForGatheringBinaryDependencies(dependencies);

        // copy into output directory
        getLog().info("Merging collected dependencies");
        // @formatter:off
        executeMojo(
                plugin(groupId("org.apache.maven.plugins"), artifactId("maven-resources-plugin"),
                        version(detectPluginVersion("org.apache.maven.plugins", "maven-resources-plugin",
                                mavenResourcesPluginVersionFallback))),
                goal("copy-resources"),
                configuration(element("outputDirectory", "${project.build.outputDirectory}"),
                        element("resources", element("resource", element("directory", dependenciesDirectory)))),
                executionEnvironment(project, session, pluginManager));
        // @formatter:on

        // generate manifest based on output only
        getLog().info("Generating OSGi MANIFEST.MF");
        try {
            setOutputDirectory(outputDirectory);
            setBuildDirectory(buildDirectory);
            manifestLocation = new File(outputDirectory, "META-INF");
            super.excludeDependencies = excludeDependencies;

            // sanity check
            if (bndInstructions.containsKey(BUNDLE_SYMBOLICNAME)
                    && !StringUtils.equals(project.getArtifactId(), bndInstructions.get(BUNDLE_SYMBOLICNAME)))
                // something is wrong, fail and report to the use instead of overriding quietly
                throw new MojoExecutionException(
                        "Plug-in configuration is wrong! The Bundle-SymbolicName must match the project's artifact id but it doesn't. Please correct the value in bndInstructions.");

            initializeBndInstruction(BUNDLE_SYMBOLICNAME, project.getArtifactId());
            initializeBndInstruction(BUNDLE_VERSION, getExpandedVersion());
            initializeBndInstruction(BUNDLE_NAME, project.getName());
            initializeBndInstruction(SNAPSHOT, qualifier);
            if (!unpackDependencies) {
                initializeBndInstruction(BUNDLE_CLASSPATH, getBundleClassPathHeaderPopulatedWithDependencyJars());
            }
            execute(project, buildDependencyGraph(project), bndInstructions, new Properties()); // BND also needs transitive dependencies
        } catch (final Exception e) {
            throw new MojoExecutionException("Error generating Bundle manifest: " + e.getMessage(), e);
        }

        // create JAR
        getLog().debug("Generating OSGi bundle jar...");
        final File pluginFile = createPluginJar();
        project.getArtifact().setFile(pluginFile);
    }

    private void buildSourceBundle(final Set<Artifact> dependencies) throws MojoExecutionException {
        // unpack sources
        getLog().info("Gathering sources");
        // @formatter:off
        final List<Element> unpackConfigurationSource = getDependenciesUnpackConfiguration(
                dependenciesSourcesDirectory, dependencies, CLASSIFIER_SOURCES);
        try {
            executeMojo(
                    plugin(groupId("org.apache.maven.plugins"), artifactId("maven-dependency-plugin"),
                            version(detectPluginVersion("org.apache.maven.plugins", "maven-dependency-plugin",
                                    mavenDependencyPluginVersionFallback))),
                    goal("unpack"),
                    configuration(unpackConfigurationSource.toArray(new Element[unpackConfigurationSource.size()])),
                    executionEnvironment(project, session, pluginManager));
        } catch (final MojoExecutionException e) {
            getLog().warn("Unable to resolve source jar; skipping source bundle");
            getLog().debug(e);
            return;
        }
        // @formatter:on

        // @formatter:off
        if (includeProjectResourceDir) {
            executeMojo(
                    plugin(groupId("org.apache.maven.plugins"), artifactId("maven-resources-plugin"),
                            version(detectPluginVersion("org.apache.maven.plugins", "maven-resources-plugin",
                                    mavenResourcesPluginVersionFallback))),
                    goal("copy-resources"),
                    configuration(element("outputDirectory", "${project.build.directory}/dependency-src"),
                            element("resources",
                                    element("resource",
                                            element("directory", "${project.basedir}/src/main/resources")),
                                    element("resource", element("directory", "${project.basedir}/src/main/java")))),
                    executionEnvironment(project, session, pluginManager));
        }
        // @formatter:on

        // create sources JAR
        getLog().debug("Generating OSGi bundle jar...");
        final File sourceBundleFile = createSourcesJar();
        projectHelper.attachArtifact(project, "java-source", CLASSIFIER_SOURCES, sourceBundleFile);
    }

    private File createPluginJar() throws MojoExecutionException {
        return assembleJar(finalName + ".jar", generateFinalBundleManifest(), outputDirectory, archive);
    }

    private File createSourcesJar() throws MojoExecutionException {
        sourceArchive.setAddMavenDescriptor(false); // no maven descriptors in source bundle
        return assembleJar(finalName + "-sources.jar", generateSourceBundleManifest(),
                new File(dependenciesSourcesDirectory), sourceArchive);
    }

    private String detectPluginVersion(final String groupId, final String artifactId,
            final String fallbackVersion) {
        final List<Plugin> plugins = project.getPluginManagement().getPlugins();
        for (final Plugin plugin : plugins) {
            if (groupId.equals(plugin.getGroupId()) && artifactId.equals(plugin.getArtifactId())) {
                getLog().debug("Using managed version " + plugin.getVersion() + " for plugin " + groupId + ":"
                        + artifactId + ".");
                return plugin.getVersion();
            }
        }
        getLog().warn(format(
                "No version defined in the efective model for plugin %s:%s. Please consider defining one in the pluginManagement section. Falling back to version \"%s\"",
                groupId, artifactId, fallbackVersion));
        return fallbackVersion;
    }

    @Override
    public void execute() throws MojoExecutionException {
        if (!isRecipeProject(project)) {
            getLog().debug(
                    format("Skipping execution for project with packaging type \"%s\"", project.getPackaging()));
            return;
        }

        // https://issues.apache.org/jira/browse/MNG-5742
        if (!mavenRuntimeInformation.isMavenVersion("[3.3.9,)"))
            throw new MojoExecutionException(
                    "The minimum required Maven version is 3.3.9. Please update your Maven installation!");

        final Set<Artifact> dependencies = getDependenciesToInclude();
        buildBundle(dependencies);
        buildSourceBundle(dependencies);
        packAndSignBundle();
        publishP2Metadata();
        assembleP2Repository();
    }

    private void executeMavenDependenciesPluginForGatheringBinaryDependencies(final Set<Artifact> dependencies)
            throws MojoExecutionException {
        if (unpackDependencies) {
            // @formatter:off
            final List<Element> unpackConfiguration = getDependenciesUnpackConfiguration(dependenciesDirectory,
                    dependencies, null);
            executeMojo(
                    plugin(groupId("org.apache.maven.plugins"), artifactId("maven-dependency-plugin"),
                            version(detectPluginVersion("org.apache.maven.plugins", "maven-dependency-plugin",
                                    mavenDependencyPluginVersionFallback))),
                    goal("unpack"),
                    configuration(unpackConfiguration.toArray(new Element[unpackConfiguration.size()])),
                    executionEnvironment(project, session, pluginManager));
            // @formatter:on
        } else {
            // place jars into "lib" folder
            final String outputDirectory = dependenciesDirectory.concat("/lib");
            // @formatter:off
            final List<Element> copyConfiguration = getDependenciesCopyConfiguration(outputDirectory, dependencies,
                    null);
            executeMojo(
                    plugin(groupId("org.apache.maven.plugins"), artifactId("maven-dependency-plugin"),
                            version(detectPluginVersion("org.apache.maven.plugins", "maven-dependency-plugin",
                                    mavenDependencyPluginVersionFallback))),
                    goal("copy"), configuration(copyConfiguration.toArray(new Element[copyConfiguration.size()])),
                    executionEnvironment(project, session, pluginManager));
            // @formatter:on
        }
    }

    private File generateFinalBundleManifest() throws MojoExecutionException {
        try {
            File mfile = new File(outputDirectory, "META-INF/MANIFEST.MF");
            final InputStream is = new FileInputStream(mfile);
            Manifest mf;
            try {
                mf = new Manifest(is);
            } finally {
                is.close();
            }
            final Attributes attributes = mf.getMainAttributes();

            if (attributes.getValue(Name.MANIFEST_VERSION) == null) {
                attributes.put(Name.MANIFEST_VERSION, "1.0");
            }

            // shameless self-promotion
            attributes.putValue(CREATED_BY, "Eclipse Bundle Recipe Maven Plug-in");

            final String expandedVersion = getExpandedVersion();
            attributes.putValue(BUNDLE_VERSION, expandedVersion);

            mfile = getFinalBundleManifestFile();
            mfile.getParentFile().mkdirs();
            final BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(mfile));
            try {
                mf.write(os);
            } finally {
                os.close();
            }

            return mfile;
        } catch (final Exception e) {
            throw new MojoExecutionException("Error generating bundle manifest: " + e.getMessage(), e);
        }
    }

    private void generateSourceBundleL10nFile() throws IOException {
        // read generates manifest for resolving bundle name
        final InputStream is = new FileInputStream(getFinalBundleManifestFile());
        Manifest bundleManifest;
        try {
            bundleManifest = new Manifest(is);
        } finally {
            is.close();
        }
        final Properties l10nProps = readL10nProps(bundleManifest);
        String bundleName = getL10nResolvedValue(bundleManifest, BUNDLE_NAME, l10nProps);
        if (bundleName == null) {
            getLog().warn("Bundle-Name header not found in " + getFinalBundleManifestFile()
                    + ", fallback to Bundle-SymbolicName for source bundle");
            bundleName = getSourceBundleSymbolicName();
        }
        final String sourceBundleName = bundleName + " Source";
        String bundleVendor = getL10nResolvedValue(bundleManifest, BUNDLE_VENDOR, l10nProps);
        if (bundleVendor == null) {
            getLog().warn("Bundle-Vendor header not found in " + getFinalBundleManifestFile()
                    + ", fallback to 'unknown' for source bundle");
            bundleVendor = "unknown";
        }
        final File l10nOutputDir = new File(dependenciesSourcesDirectory);
        final Properties sourceL10nProps = new Properties();
        sourceL10nProps.setProperty(I18N_KEY_BUNDLE_NAME, sourceBundleName);
        sourceL10nProps.setProperty(I18N_KEY_BUNDLE_VENDOR, bundleVendor);
        final File l10nPropsFile = new File(l10nOutputDir, BUNDLE_LOCALIZATION_DEFAULT_BASENAME + ".properties");
        l10nPropsFile.getParentFile().mkdirs();
        OutputStream out = null;
        try {
            out = new FileOutputStream(l10nPropsFile);
            sourceL10nProps.store(out, "Source Bundle Localization");
        } finally {
            IOUtil.close(out);
        }
    }

    private File generateSourceBundleManifest() throws MojoExecutionException {
        try {
            generateSourceBundleL10nFile();

            final Manifest mf = new Manifest();
            final Attributes attributes = mf.getMainAttributes();

            if (attributes.getValue(Name.MANIFEST_VERSION) == null) {
                attributes.put(Name.MANIFEST_VERSION, "1.0");
            }

            final String expandedVersion = getExpandedVersion();
            attributes.putValue(BUNDLE_VERSION, expandedVersion);
            attributes.putValue(BUNDLE_MANIFESTVERSION, "2");
            attributes.putValue(BUNDLE_SYMBOLICNAME, getSourceBundleSymbolicName());
            attributes.putValue(BUNDLE_NAME, I18N_KEY_PREFIX + I18N_KEY_BUNDLE_NAME);
            attributes.putValue(BUNDLE_VENDOR, I18N_KEY_PREFIX + I18N_KEY_BUNDLE_VENDOR);
            //attributes.putValue(BUNDLE_LOCALIZATION, BUNDLE_LOCALIZATION_DEFAULT_BASENAME);
            attributes.putValue("Eclipse-SourceBundle",
                    project.getArtifactId() + ";version=\"" + expandedVersion + "\";roots:=\".\"");
            attributes.putValue(CREATED_BY, "Eclipse Bundle Recipe Maven Plug-in");

            final File mfile = getSourceBundleManifestFile();
            mfile.getParentFile().mkdirs();
            final BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(mfile));
            try {
                mf.write(os);
            } finally {
                os.close();
            }

            return mfile;
        } catch (final Exception e) {
            throw new MojoExecutionException("Error generating source bundle manifest: " + e.getMessage(), e);
        }
    }

    private Element getArtifactItems(final Set<Artifact> dependencies, final String classifier) {
        final List<Element> artifactItems = new ArrayList<Element>();
        for (final Artifact artifact : dependencies) {
            // @formatter:off
            if (classifier != null) {
                artifactItems.add(element("artifactItem", element("groupId", artifact.getGroupId()),
                        element("artifactId", artifact.getArtifactId()), element("version", artifact.getVersion()),
                        element("classifier", classifier)));
            } else {
                artifactItems.add(element("artifactItem", element("groupId", artifact.getGroupId()),
                        element("artifactId", artifact.getArtifactId()), element("version", artifact.getVersion()),
                        element("classifier", artifact.getClassifier())));
                // @formatter:on
            }
        }
        return element("artifactItems", artifactItems.toArray(new Element[artifactItems.size()]));
    }

    private String getBundleClassPathHeaderPopulatedWithDependencyJars() throws MojoExecutionException {
        if (excludeDotFolderFromBundleClasspath)
            return getDependenciesJarFilesFromLibFolderAsCommaSeparatedString();
        else
            return ".,".concat(getDependenciesJarFilesFromLibFolderAsCommaSeparatedString());
    }

    private String getBundleVersion() {
        return BundleUtil.getBundleVersion(project.getVersion());
    }

    private List<Element> getDependenciesCopyConfiguration(final String outputDirectory,
            final Set<Artifact> dependencies, final String classifier) throws MojoExecutionException {
        final List<Element> copyConfiguration = new ArrayList<Element>();
        copyConfiguration.add(element(name("outputDirectory"), outputDirectory));
        copyConfiguration.add(element(name("stripVersion"), String.valueOf(stripVersion)));
        copyConfiguration.add(getArtifactItems(dependencies, classifier));
        return copyConfiguration;
    }

    private String getDependenciesJarFilesFromLibFolderAsCommaSeparatedString() throws MojoExecutionException {
        final File libDirectory = Paths.get(dependenciesDirectory).resolve("lib").toFile();
        if (!libDirectory.isDirectory())
            throw new MojoExecutionException(format(
                    "Folder '%s' does not exists. It seems no dependencies were downloaded at all.", libDirectory));

        final String[] jars = libDirectory.list((f, s) -> {
            return s.toLowerCase().endsWith(".jar");
        });
        if ((jars == null) || (jars.length == 0))
            throw new MojoExecutionException(format(
                    "No jar files found in folder '%s'. Please verify that dependencies are specified and downloaded successfully.",
                    libDirectory));

        return Stream.of(jars).sorted().map((jar) -> {
            return "lib/".concat(jar);
        }).collect(Collectors.joining(","));
    }

    private Set<Artifact> getDependenciesToInclude() {
        final DependencyUtil dependencyUtil = new DependencyUtil(getLog(), session);
        dependencyUtil.initializeExcludeDependencies(excludeDependencies);
        return dependencyUtil.getDependenciesToInclude(project);
    }

    private List<Element> getDependenciesUnpackConfiguration(final String outputDirectory,
            final Set<Artifact> dependencies, final String classifier) throws MojoExecutionException {
        final List<Element> unpackConfiguration = new ArrayList<Element>();
        unpackConfiguration.add(element(name("outputDirectory"), outputDirectory));
        if (null != excludes) {
            unpackConfiguration.add(element(name("excludes"), excludes));
        }
        if (null != includes) {
            unpackConfiguration.add(element(name("includes"), includes));
        }
        unpackConfiguration.add(getArtifactItems(dependencies, classifier));
        return unpackConfiguration;
    }

    private String getExpandedVersion() {
        return BundleUtil.getExpandedVersion(getBundleVersion(), qualifier);
    }

    private FileSet getFileSet(final File basedir) {
        final DefaultFileSet fileSet = new DefaultFileSet();
        fileSet.setDirectory(basedir);
        fileSet.setExcludes(AbstractScanner.DEFAULTEXCLUDES);
        return fileSet;
    }

    private File getFinalBundleManifestFile() {
        return new File(buildDirectory, "MANIFEST.MF");
    }

    private String getL10nResolvedValue(final Manifest manifest, final String manifestHeaderKey,
            final Properties l10nProps) {
        final String value = manifest.getMainAttributes().getValue(manifestHeaderKey);
        if ((value == null) || !value.startsWith(I18N_KEY_PREFIX))
            return value;
        if (l10nProps == null)
            return null;
        final String key = value.substring(1).trim();
        return l10nProps.getProperty(key);
    }

    @Override
    protected File getOutputDirectory() {
        return outputDirectory;
    }

    private File getSourceBundleManifestFile() {
        return new File(buildDirectory, "MANIFEST-SRC.MF");
    }

    private String getSourceBundleSymbolicName() {
        return BundleUtil.getSourceBundleSymbolicName(project);
    }

    private void initializeBndInstruction(final String key, final String value) {
        if (StringUtils.isBlank(bndInstructions.get(key))) {
            bndInstructions.put(key, value);
        }
    }

    private void packAndSignBundle() throws MojoExecutionException {
        if (!"eclipse".equalsIgnoreCase(signingServiceType)) {
            getLog().debug(
                    "Skipping pack and signing. Set signing service type to 'eclipse' in order to enable signing using Eclipse.org CBI signing plug-in..");
            return;
        }

        getLog().info("Packing and signing bundle");

        // 1) normalize
        // @formatter:off
        executeMojo(
                plugin(groupId("org.eclipse.tycho.extras"), artifactId("tycho-pack200a-plugin"),
                        version(detectPluginVersion("org.eclipse.tycho.extras", "tycho-pack200a-plugin",
                                tychoExtrasPluginVersionFallback))),
                goal("normalize"),
                configuration(
                        element("supportedProjectTypes", element("supportedProjectType", "eclipse-bundle-recipe"))),
                executionEnvironment(project, session, pluginManager));
        // @formatter:on

        // 2) sign
        // @formatter:off
        executeMojo(
                plugin(groupId("org.eclipse.cbi.maven.plugins"), artifactId("eclipse-jarsigner-plugin"),
                        version(detectPluginVersion("org.eclipse.cbi.maven.plugins", "eclipse-jarsigner-plugin",
                                cbiPluginVersionFallback))),
                goal("sign"), configuration(), executionEnvironment(project, session, pluginManager));
        // @formatter:on

        // 3) pack
        // @formatter:off
        executeMojo(
                plugin(groupId("org.eclipse.tycho.extras"), artifactId("tycho-pack200b-plugin"),
                        version(detectPluginVersion("org.eclipse.tycho.extras", "tycho-pack200a-plugin",
                                tychoExtrasPluginVersionFallback))),
                goal("pack"),
                configuration(
                        element("supportedProjectTypes", element("supportedProjectType", "eclipse-bundle-recipe"))),
                executionEnvironment(project, session, pluginManager));
        // @formatter:on
    }

    private void publishP2Metadata() throws MojoExecutionException {
        // copy into output directory
        getLog().debug("Publishing p2 metadata...");
        try {
            // @formatter:off
            executeMojo(
                    plugin(groupId("org.eclipse.tycho"), artifactId("tycho-p2-plugin"),
                            version(detectPluginVersion(
                                    "org.eclipse.tycho", "tycho-p2-plugin", tychoPluginVersionFallback))),
                    goal("p2-metadata"),
                    configuration(element("supportedProjectTypes",
                            element("supportedProjectType", "eclipse-bundle-recipe"))),
                    executionEnvironment(project, session, pluginManager));
            // @formatter:on
        } catch (final MojoExecutionException e) {
            // check for http://eclip.se/428950
            final Throwable rootCause = ExceptionUtils.getRootCause(e);
            if ((rootCause instanceof IllegalArgumentException) && StringUtils.isBlank(rootCause.getMessage())) {
                final String[] trace = ExceptionUtils.getRootCauseStackTrace(e);
                if ((trace.length > 1) && (trace[1].indexOf("P2GeneratorImpl.getCanonicalArtifact") > 0)) {
                    getLog().debug(e);
                    throw new MojoExecutionException(
                            "The generated bundle manifest is broken. Unfortunately, the error is hard to discover (see http://eclip.se/428950). Try running Maven with '-Dosgi.logfile=/tmp/tycho-eclipse.log' to get a log file of the embedded Equinox OSGi framework.");
                }
            }
            throw new MojoExecutionException(format(
                    "Unable to generate p2 metadata. Please check the generated bundle manifest and any bnd instructions. Try running Maven with '-Dosgi.logfile=/tmp/tycho-eclipse.log' to get a log file of the embedded Equinox OSGi framework. %s",
                    e.getMessage()), e);
        }

    }

    private Properties readL10nProps(final Manifest manifest) throws IOException {
        String bundleL10nBase = manifest.getMainAttributes().getValue(BUNDLE_LOCALIZATION);
        if (bundleL10nBase == null) {
            bundleL10nBase = BUNDLE_LOCALIZATION_DEFAULT_BASENAME;
        }
        final File l10nPropsFile = new File(outputDirectory, bundleL10nBase + ".properties");
        if (!l10nPropsFile.isFile()) {
            getLog().warn("Bundle localization file " + l10nPropsFile + " not found.");
            return null;
        }
        final Properties l10nProps = new Properties();
        FileInputStream in = null;
        try {
            in = new FileInputStream(l10nPropsFile);
            l10nProps.load(in);
        } finally {
            IOUtil.close(in);
        }
        return l10nProps;
    }
}