/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package org.apache.felix.bundleplugin;


import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.apache.maven.archiver.ManifestSection;
import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Exclusion;
import org.apache.maven.model.License;
import org.apache.maven.model.Model;
import org.apache.maven.model.Resource;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
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.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import org.apache.maven.shared.osgi.DefaultMaven2OsgiConverter;
import org.apache.maven.shared.osgi.Maven2OsgiConverter;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.PropertyUtils;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.sonatype.plexus.build.incremental.BuildContext;

import aQute.bnd.header.Attrs;
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.header.Parameters;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.EmbeddedResource;
import aQute.bnd.osgi.FileResource;
import aQute.bnd.osgi.Instruction;
import aQute.bnd.osgi.Instructions;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.Packages;
import aQute.bnd.osgi.Processor;
import aQute.lib.collections.ExtList;
import aQute.lib.spring.SpringXMLType;
import aQute.libg.generics.Create;


/**
 * Create an OSGi bundle from Maven project
 *
 */
@Mojo( name = "bundle", requiresDependencyResolution = ResolutionScope.TEST,
       threadSafe = true,
       defaultPhase = LifecyclePhase.PACKAGE )
public class BundlePlugin extends AbstractMojo
{
    /**
     * Directory where the manifest will be written
     */
    @Parameter( property = "manifestLocation", defaultValue = "${project.build.outputDirectory}/META-INF" )
    protected File manifestLocation;

    /**
     * Output a nicely formatted manifest that still respects the 72 character line limit.
     */
    @Parameter( property = "niceManifest", defaultValue = "false" )
    protected boolean niceManifest;
    /**
     * File where the BND instructions will be dumped
     */
    @Parameter( property = "dumpInstructions" )
    protected File dumpInstructions;

    /**
     * File where the BND class-path will be dumped
     */
    @Parameter( property = "dumpClasspath" )
    protected File dumpClasspath;

    /**
     * When true, unpack the bundle contents to the outputDirectory
     */
    @Parameter( property = "unpackBundle" )
    protected boolean unpackBundle;

    /**
     * Comma separated list of artifactIds to exclude from the dependency classpath passed to BND (use "true" to exclude everything)
     */
    @Parameter( property = "excludeDependencies" )
    protected String excludeDependencies;

    /**
     * Final name of the bundle (without classifier or extension)
     */
    @Parameter( defaultValue = "${project.build.finalName}")
    private String finalName;

    /**
     * Classifier type of the bundle to be installed.  For example, "jdk14".
     * Defaults to none which means this is the project's main bundle.
     */
    @Parameter
    protected String classifier;

    /**
     * Packaging type of the bundle to be installed.  For example, "jar".
     * Defaults to none which means use the same packaging as the project.
     */
    @Parameter
    protected String packaging;

    /**
     * If true, remove any inlined or embedded dependencies from the resulting pom.
     */
    @Parameter
    protected boolean createDependencyReducedPom;

    /**
     * Where to put the dependency reduced pom. Note: setting a value for this parameter with a directory other than
     * ${basedir} will change the value of ${basedir} for all executions that come after the shade execution. This is
     * often not what you want. This is considered an open issue with this plugin.
     */
    @Parameter( defaultValue = "${basedir}/dependency-reduced-pom.xml" )
    protected File dependencyReducedPomLocation;

    /**
     * Directory where the SCR files will be written
     */
    @Parameter(defaultValue="${project.build.outputDirectory}")
    protected File scrLocation;

    /**
     * When true, dump the generated SCR files
     */
    @Parameter
    protected boolean exportScr;
    
    @Component
    private MavenProjectHelper m_projectHelper;

    @Component
    private ArchiverManager m_archiverManager;

    @Component
    private ArtifactHandlerManager m_artifactHandlerManager;

    /* The current Maven session.  */
    @Parameter( defaultValue = "${session}", readonly = true )
    protected MavenSession session;


    /**
     * ProjectBuilder, needed to create projects from the artifacts.
     */
    @Component
    protected MavenProjectBuilder mavenProjectBuilder;

    @Component
    protected DependencyGraphBuilder dependencyGraphBuilder;

    @Component
    private ArtifactMetadataSource artifactMetadataSource;

    @Component
    private ArtifactCollector artifactCollector;

    @Component
    protected ArtifactFactory artifactFactory;

    /**
     * Artifact resolver, needed to download source jars for inclusion in classpath.
     */
    @Component
    protected ArtifactResolver artifactResolver;


    /**
     * Local maven repository.
     */
    @Parameter( readonly = true, required = true, defaultValue = "${localRepository}" )
    protected ArtifactRepository localRepository;

    /**
     * Remote repositories which will be searched for source attachments.
     */
    @Parameter( readonly = true, required = true, defaultValue = "${project.remoteArtifactRepositories}" )
    protected List<ArtifactRepository> remoteArtifactRepositories;



    /**
     * Project types which this plugin supports.
     */
    @Parameter
    protected List<String> supportedProjectTypes = Arrays.asList("jar", "bundle");

    /**
     * Project types which are not supported, but silently ignored.
     */
    @Parameter
    protected List<String> noWarningProjectTypes = Collections.emptyList();

    /**
     * The directory for the generated bundles.
     */
    @Parameter( defaultValue = "${project.build.outputDirectory}" )
    private File outputDirectory;

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

    /**
     * The Maven project.
     */
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    protected MavenProject project;

    /**
     * The BND instructions for the bundle.
     * Maven will expand property macros in these values. If you want to use a BND macro, you must double the dollar sign
     * for the plugin to pass it to BND correctly. For example: <br>
     * {@code <_consumer-policy>$${range;[===,+)}<code>}<code>{@code </_consumer-policy> }
     */
    @Parameter
    private Map<String, String> instructions = new LinkedHashMap<>();

    /**
     * Use locally patched version for now.
     */
    private final Maven2OsgiConverter m_maven2OsgiConverter = new DefaultMaven2OsgiConverter();

    /**
     * The archive configuration to use.
     */
    @Parameter
    private MavenArchiveConfiguration archive; // accessed indirectly in JarPluginConfiguration

    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    private MavenSession m_mavenSession;

    /**
     * Timestamp for reproducible output archive entries, either formatted as ISO 8601
     * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
     * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
     *
     * @since 5.1.3
     */
    @Parameter( defaultValue = "${project.build.outputTimestamp}" )
    private String outputTimestamp;

    @Component
    protected BuildContext buildContext;

    @Parameter(defaultValue = "false", property = "bundle.skip")
    private boolean skip = false;
    
    private static final String MAVEN_SYMBOLICNAME = "maven-symbolicname";
    private static final String MAVEN_RESOURCES = "{maven-resources}";
    private static final String MAVEN_TEST_RESOURCES = "{maven-test-resources}";
    private static final String LOCAL_PACKAGES = "{local-packages}";
    private static final String MAVEN_SOURCES = "{maven-sources}";
    private static final String MAVEN_TEST_SOURCES = "{maven-test-sources}";
    private static final String BUNDLE_PLUGIN_EXTENSION = "BNDExtension-";
    private static final String BUNDLE_PLUGIN_PREPEND_EXTENSION = "BNDPrependExtension-";

    private static final String[] EMPTY_STRING_ARRAY =
        {};
    private static final String[] DEFAULT_INCLUDES =
        { "**/**" };

    private static final String NL = System.getProperty( "line.separator" );


    protected Maven2OsgiConverter getMaven2OsgiConverter()
    {
        return m_maven2OsgiConverter;
    }


    protected MavenProject getProject()
    {
        return project;
    }

    protected Map<String, String> getInstructions() {
        return instructions;
    }

    /**
     * @see org.apache.maven.plugin.AbstractMojo#execute()
     */
    public void execute() throws MojoExecutionException
    {
        if (skip) {
            getLog().info("Skipping Bundle plugin");
            return;
        }
        String projectType = getProject().getArtifact().getType();

        // ignore unsupported project types, useful when bundleplugin is configured in parent pom
        if ( !supportedProjectTypes.contains( projectType ) )
        {
            if (!noWarningProjectTypes.contains( projectType ) )
            {
                getLog().warn(
                        "Ignoring project type " + projectType + " - supportedProjectTypes = " + supportedProjectTypes);
            }
            return;
        }

        try
        {
            execute( instructions, getClasspath( project ) );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Error calculating classpath for project " + project, e );
        }
    }


    /* transform directives from their XML form to the expected BND syntax (eg. _include becomes -include) */
    protected static Map<String, String> transformDirectives( Map<String, String> originalInstructions )
    {
        Map<String, String> transformedInstructions = new LinkedHashMap<>();
        for ( Map.Entry<String, String> e : originalInstructions.entrySet() )
        {
            String key = e.getKey();
            if ( key.startsWith( "_" ) )
            {
                key = "-" + key.substring( 1 );
            }

            String value = e.getValue();
            if ( null == value )
            {
                value = "";
            }
            else
            {
                value = value.replaceAll( "\\p{Blank}*[\r\n]\\p{Blank}*", "" );
            }

            if ( Analyzer.WAB.equals( key ) && value.isEmpty() )
            {
                // provide useful default
                value = "src/main/webapp/";
            }

            transformedInstructions.put( key, value );
        }
        return transformedInstructions;
    }


    protected boolean reportErrors( String prefix, Analyzer analyzer )
    {
        List<String> errors = analyzer.getErrors();
        List<String> warnings = analyzer.getWarnings();

        for ( String msg : warnings )
        {
            getLog().warn( prefix + " : " + msg );
        }

        boolean hasErrors = false;
        String fileNotFound = "Input file does not exist: ";
        for ( String msg : errors )
        {
            if ( msg.startsWith(fileNotFound) && msg.endsWith( "~" ) )
            {
                // treat as warning; this error happens when you have duplicate entries in Include-Resource
                String duplicate = Processor.removeDuplicateMarker( msg.substring( fileNotFound.length() ) );
                getLog().warn( prefix + " : Duplicate path '" + duplicate + "' in Include-Resource" );
            }
            else
            {
                getLog().error( prefix + " : " + msg );
                hasErrors = true;
            }
        }
        return hasErrors;
    }


    protected void execute(Map<String, String> originalInstructions,
                           ClassPathItem[] classpath) throws MojoExecutionException
    {
        try
        {
            File jarFile = new File( getBuildDirectory(), getBundleName( project ) );
            Builder builder = buildOSGiBundle( project, originalInstructions, classpath );
            boolean hasErrors = reportErrors( "Bundle " + project.getArtifact(), builder );
            if ( hasErrors )
            {
                String failok = builder.getProperty( "-failok" );
                if ( null == failok || "false".equalsIgnoreCase( failok ) )
                {
                    jarFile.delete();

                    throw new MojoFailureException( "Error(s) found in bundle configuration" );
                }
            }

            // attach bundle to maven project
            getLog().info("Building bundle: " + jarFile.getPath());
            jarFile.getParentFile().mkdirs();
            builder.getJar().write( jarFile );

            Artifact mainArtifact = project.getArtifact();

            if ( "bundle".equals( mainArtifact.getType() ) )
            {
                // workaround for MNG-1682: force maven to install artifact using the "jar" handler
                mainArtifact.setArtifactHandler( m_artifactHandlerManager.getArtifactHandler( "jar" ) );
            }

            boolean customClassifier = null != classifier && classifier.trim().length() > 0;
            boolean customPackaging = null != packaging && packaging.trim().length() > 0;

            if ( customClassifier && customPackaging )
            {
                m_projectHelper.attachArtifact( project, packaging, classifier, jarFile );
            }
            else if ( customClassifier )
            {
                m_projectHelper.attachArtifact( project, jarFile, classifier );
            }
            else if ( customPackaging )
            {
                m_projectHelper.attachArtifact( project, packaging, jarFile );
            }
            else
            {
                mainArtifact.setFile( jarFile );
            }

            if ( unpackBundle )
            {
                unpackBundle( jarFile );
            }

            if ( manifestLocation != null )
            {
                File outputFile = new File( manifestLocation, "MANIFEST.MF" );

                try
                {
                    ManifestPlugin.writeManifest( builder, outputFile, niceManifest, exportScr, scrLocation, buildContext, getLog() );
                }
                catch ( IOException e )
                {
                    getLog().error( "Error trying to write Manifest to file " + outputFile, e );
                }
            }

            // cleanup...
            builder.close();
        }
        catch ( MojoFailureException e )
        {
            getLog().error( e.getLocalizedMessage() );
            throw new MojoExecutionException( "Error(s) found in bundle configuration", e );
        }
        catch ( Exception e )
        {
            getLog().error( "An internal error occurred", e );
            throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
        }
    }


    protected Builder getOSGiBuilder( MavenProject currentProject, Map<String, String> originalInstructions,
        ClassPathItem[] classpath ) throws Exception
    {
        Properties properties = new Properties();
        properties.putAll( getDefaultProperties( currentProject ) );
        properties.putAll( transformDirectives( originalInstructions ) );
        if (MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).isPresent())
        {
          properties.put( Constants.REPRODUCIBLE, outputTimestamp );
        }

        // process overrides from project
        final Map<String, String> addProps = new HashMap<>();
        for ( Entry<Object, Object> entry : currentProject.getProperties().entrySet() )
        {
            final String key = entry.getKey().toString();
            if ( key.startsWith(BUNDLE_PLUGIN_EXTENSION) )
            {
                final String oKey = key.substring(BUNDLE_PLUGIN_EXTENSION.length());
                final String currentValue = properties.getProperty(oKey);
                if ( currentValue == null )
                {
                    addProps.put(oKey, entry.getValue().toString());
                }
                else
                {
                    addProps.put(oKey, currentValue + ',' + entry.getValue());
                }
            }
            if ( key.startsWith(BUNDLE_PLUGIN_PREPEND_EXTENSION) )
            {
                final String oKey = key.substring(BUNDLE_PLUGIN_PREPEND_EXTENSION.length());
                final String currentValue = properties.getProperty(oKey);
                if ( currentValue == null )
                {
                    addProps.put(oKey, entry.getValue().toString());
                }
                else
                {
                    addProps.put(oKey, entry.getValue() + "," + currentValue);
                }
            }
        }
        properties.putAll( addProps );
        for ( String key : addProps.keySet() )
        {
            properties.remove(BUNDLE_PLUGIN_EXTENSION + key);
            properties.remove(BUNDLE_PLUGIN_PREPEND_EXTENSION + key);
        }

        if (properties.getProperty("Bundle-Activator") != null
                && properties.getProperty("Bundle-Activator").isEmpty())
        {
            properties.remove("Bundle-Activator");
        }
        if (properties.containsKey("-disable-plugin"))
        {
            String[] disabled = properties.remove("-disable-plugin").toString().replaceAll(" ", "").split(",");
            String[] enabled = properties.getProperty(Analyzer.PLUGIN, "").replaceAll(" ", "").split(",");
            Set<String> plugin = new LinkedHashSet<>();
            plugin.addAll(Arrays.asList(enabled));
            plugin.removeAll(Arrays.asList(disabled));
            StringBuilder sb = new StringBuilder();
            for (String s : plugin)
            {
                if (sb.length() > 0)
                {
                    sb.append(",");
                }
                sb.append(s);
            }
            properties.setProperty(Analyzer.PLUGIN, sb.toString());
        }

        Builder builder = new Builder();
        synchronized ( BundlePlugin.class ) // protect setBase...getBndLastModified which uses static DateFormat
        {
            builder.setBase( getBase( currentProject ) );
        }
        builder.setProperties( sanitize( properties ) );
        if ( classpath != null )
        {
            List<Jar> jars = new ArrayList<>();
            for ( int i = 0; i < classpath.length; i++ ) {
                if ( classpath[i].file.exists() ) {
                    jars.add( new Jar( classpath[i].id, classpath[i].file ) );
                }
            }
            builder.setClasspath( jars );
        }

        return builder;
    }


    protected static Properties sanitize( Properties properties )
    {
        // convert any non-String keys/values to Strings
        Properties sanitizedEntries = new Properties();
        for ( Iterator<Map.Entry<Object,Object>> itr = properties.entrySet().iterator(); itr.hasNext(); )
        {
            Map.Entry<Object,Object> entry = itr.next();
            if ( !(entry.getKey() instanceof String) )
            {
                String key = sanitize(entry.getKey());
                if ( !properties.containsKey( key ) )
                {
                    sanitizedEntries.setProperty( key, sanitize( entry.getValue() ) );
                }
                itr.remove();
            }
            else if ( !(entry.getValue() instanceof String) )
            {
                entry.setValue( sanitize( entry.getValue() ) );
            }
        }
        properties.putAll( sanitizedEntries );
        return properties;
    }


    protected static String sanitize( Object value )
    {
        if ( value instanceof String )
        {
            return ( String ) value;
        }
        else if ( value instanceof Iterable )
        {
            String delim = "";
            StringBuilder buf = new StringBuilder();
            for ( Object i : ( Iterable<?> ) value )
            {
                buf.append( delim ).append( i );
                delim = ", ";
            }
            return buf.toString();
        }
        else if ( value.getClass().isArray() )
        {
            String delim = "";
            StringBuilder buf = new StringBuilder();
            for ( int i = 0, len = Array.getLength( value ); i < len; i++ )
            {
                buf.append( delim ).append( Array.get( value, i ) );
                delim = ", ";
            }
            return buf.toString();
        }
        else
        {
            return String.valueOf( value );
        }
    }


    protected void addMavenInstructions(MavenProject currentProject, Builder builder) throws Exception
    {
        if ( currentProject.getBasedir() != null )
        {
            // update BND instructions to add included Maven resources
            includeMavenResources(currentProject, builder, getLog());

            // calculate default export/private settings based on sources
            addLocalPackages(outputDirectory, builder);

            // tell BND where the current project source resides
            addMavenSourcePath(currentProject, builder, getLog());
        }

        // update BND instructions to embed selected Maven dependencies
        Collection<Artifact> embeddableArtifacts = getEmbeddableArtifacts( currentProject, builder );
        DependencyEmbedder dependencyEmbedder = new DependencyEmbedder(getLog(), embeddableArtifacts);
        dependencyEmbedder.processHeaders(builder);

        Collection<Artifact> embeddedArtifacts = dependencyEmbedder.getEmbeddedArtifacts();
        if ( !embeddedArtifacts.isEmpty() && createDependencyReducedPom )
        {
            Set<String> embeddedIds = new HashSet<>();
            for ( Artifact artifact : embeddedArtifacts )
            {
                embeddedIds.add( getId( artifact ) );
            }
            createDependencyReducedPom( embeddedIds );

        }

        if ( dumpInstructions != null || getLog().isDebugEnabled() )
        {
            StringBuilder buf = new StringBuilder();
            getLog().debug( "BND Instructions:" + NL + dumpInstructions( builder.getProperties(), buf ) );
            if ( dumpInstructions != null )
            {
                getLog().info( "Writing BND instructions to " + dumpInstructions );
                dumpInstructions.getParentFile().mkdirs();
                FileUtils.fileWrite( dumpInstructions, "# BND instructions" + NL + buf );
            }
        }



        if ( dumpClasspath != null || getLog().isDebugEnabled() )
        {
            StringBuilder buf = new StringBuilder();
            getLog().debug("BND Classpath:" + NL + dumpClasspath(builder.getClasspath(), buf));
            if ( dumpClasspath != null )
            {
                getLog().info( "Writing BND classpath to " + dumpClasspath );
                dumpClasspath.getParentFile().mkdirs();
                FileUtils.fileWrite( dumpClasspath, "# BND classpath" + NL + buf );
            }
        }
    }


    // We need to find the direct dependencies that have been included in the uber JAR so that we can modify the
    // POM accordingly.
    private void createDependencyReducedPom( Set<String> artifactsToRemove )
            throws IOException, ProjectBuildingException, DependencyGraphBuilderException {
        Model model = project.getOriginalModel();
        List<Dependency> dependencies = new ArrayList<>();

        boolean modified = false;

        List<Dependency> transitiveDeps = new ArrayList<>();

        for ( Artifact artifact : project.getArtifacts() )
        {
            if ( "pom".equals( artifact.getType() ) )
            {
                // don't include pom type dependencies in dependency reduced pom
                continue;
            }

            //promote
            Dependency dep = new Dependency();
            dep.setArtifactId( artifact.getArtifactId() );
            if ( artifact.hasClassifier() )
            {
                dep.setClassifier( artifact.getClassifier() );
            }
            dep.setGroupId( artifact.getGroupId() );
            dep.setOptional( artifact.isOptional() );
            dep.setScope( artifact.getScope() );
            dep.setType( artifact.getType() );
            dep.setVersion( artifact.getVersion() );

            //we'll figure out the exclusions in a bit.

            transitiveDeps.add( dep );
        }
        List<Dependency> origDeps = project.getDependencies();

        for (Dependency d : origDeps)
        {
            dependencies.add(d);

            String id = getId(d);

            if (artifactsToRemove.contains(id))
            {
                modified = true;

                dependencies.remove(d);
            }
        }

        // Check to see if we have a reduction and if so rewrite the POM.
        if ( modified )
        {
            while ( modified )
            {

                model.setDependencies( dependencies );

                if ( dependencyReducedPomLocation == null )
                {
                    // MSHADE-123: We can't default to 'target' because it messes up uses of ${project.basedir}
                    dependencyReducedPomLocation = new File ( project.getBasedir(), "dependency-reduced-pom.xml" );
                }

                File f = dependencyReducedPomLocation;
                if ( f.exists() )
                {
                    f.delete();
                }

                Writer w = WriterFactory.newXmlWriter( f );

                String origRelativePath = null;
                String replaceRelativePath = null;
                if ( model.getParent() != null)
                {
                    origRelativePath = model.getParent().getRelativePath();

                }
                replaceRelativePath = origRelativePath;

                if ( origRelativePath == null )
                {
                    origRelativePath = "../pom.xml";
                }

                if ( model.getParent() != null )
                {
                    File parentFile = new File( project.getBasedir(), model.getParent().getRelativePath() ).getCanonicalFile();
                    if ( !parentFile.isFile() )
                    {
                        parentFile = new File( parentFile, "pom.xml");
                    }

                    parentFile = parentFile.getCanonicalFile();

                    String relPath = RelativizePath.convertToRelativePath( parentFile, f );
                    model.getParent().setRelativePath( relPath );
                }

                try
                {
                    new MavenXpp3Writer().write( w, model );
                }
                finally
                {
                    if ( model.getParent() != null )
                    {
                        model.getParent().setRelativePath( replaceRelativePath );
                    }
                    w.close();
                }

                MavenProject p2 = mavenProjectBuilder.build( f, localRepository, null );
                modified = updateExcludesInDeps( p2, dependencies, transitiveDeps );

            }

            project.setFile( dependencyReducedPomLocation );
        }
    }

    private String getId( Artifact artifact )
    {
        return getId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getType(), artifact.getClassifier() );
    }

    private String getId( Dependency dependency )
    {
        return getId( dependency.getGroupId(), dependency.getArtifactId(), dependency.getType(),
                dependency.getClassifier() );
    }

    private String getId( String groupId, String artifactId, String type, String classifier )
    {
        return groupId + ":" + artifactId + ":" + type + ":" + ( ( classifier != null ) ? classifier : "" );
    }

    public boolean updateExcludesInDeps( MavenProject project, List<Dependency> dependencies, List<Dependency> transitiveDeps )
            throws DependencyGraphBuilderException
    {
        ProjectBuildingRequest request = new DefaultProjectBuildingRequest();
        request.setProject(project);
        request.setRepositorySession(session.getRepositorySession());
        DependencyNode node = dependencyGraphBuilder.buildDependencyGraph(request, null);
        boolean modified = false;
        for (DependencyNode n2 : node.getChildren())
        {
            for (DependencyNode n3 : n2.getChildren())
            {
                //anything two levels deep that is marked "included"
                //is stuff that was excluded by the original poms, make sure it
                //remains excluded IF promoting transitives.
                if (true)
                {
                    //check if it really isn't in the list of original dependencies.  Maven
                    //prior to 2.0.8 may grab versions from transients instead of
                    //from the direct deps in which case they would be marked included
                    //instead of OMITTED_FOR_DUPLICATE

                    //also, if not promoting the transitives, level 2's would be included
                    boolean found = false;
                    for (Dependency dep : transitiveDeps)
                    {
                        if (dep.getArtifactId().equals(n3.getArtifact().getArtifactId())
                                && dep.getGroupId().equals(n3.getArtifact().getGroupId()))
                        {
                            found = true;
                        }

                    }

                    if (!found)
                    {
                        for (Dependency dep : dependencies)
                        {
                            if (dep.getArtifactId().equals(n2.getArtifact().getArtifactId())
                                    && dep.getGroupId().equals(n2.getArtifact().getGroupId()))
                            {
                                Exclusion exclusion = new Exclusion();
                                exclusion.setArtifactId(n3.getArtifact().getArtifactId());
                                exclusion.setGroupId(n3.getArtifact().getGroupId());
                                dep.addExclusion(exclusion);
                                modified = true;
                                break;
                            }
                        }
                    }
                }
            }
        }
        return modified;
    }


    protected Builder buildOSGiBundle(MavenProject currentProject, Map<String, String> originalInstructions,
                                      ClassPathItem[] classpath) throws Exception
    {
        Builder builder = getOSGiBuilder( currentProject, originalInstructions, classpath );

        addMavenInstructions( currentProject, builder );

        builder.build();

        mergeMavenManifest(currentProject, builder);

        return builder;
    }


    protected static StringBuilder dumpInstructions( Properties properties, StringBuilder buf )
    {
        try
        {
            buf.append( "#-----------------------------------------------------------------------" + NL );
            Properties stringProperties = new Properties();
            for ( Enumeration<String> e = (Enumeration<String>) properties.propertyNames(); e.hasMoreElements(); )
            {
                // we can only store String properties
                String key = e.nextElement();
                String value = properties.getProperty( key );
                if ( value != null )
                {
                    stringProperties.setProperty( key, value );
                }
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            stringProperties.store( out, null ); // properties encoding is 8859_1
            buf.append( out.toString( "8859_1" ) );
            buf.append("#-----------------------------------------------------------------------").append(NL);
        }
        catch ( Throwable e )
        {
            // ignore...
        }
        return buf;
    }


    protected static StringBuilder dumpClasspath( List<Jar> classpath, StringBuilder buf )
    {
        try
        {
            buf.append("#-----------------------------------------------------------------------").append(NL);
            buf.append("-classpath:\\").append(NL);
            for ( Iterator<Jar> i = classpath.iterator(); i.hasNext(); )
            {
                File path = i.next().getSource();
                if ( path != null )
                {
                    buf.append(' ').append(path.toString()).append(i.hasNext() ? ",\\" : "").append(NL);
                }
            }
            buf.append("#-----------------------------------------------------------------------").append(NL);
        }
        catch ( Throwable e )
        {
            // ignore...
        }
        return buf;
    }


    protected static StringBuilder dumpManifest( Manifest manifest, StringBuilder buf )
    {
        try
        {
            buf.append("#-----------------------------------------------------------------------").append(NL);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ManifestWriter.outputManifest(manifest, out, true); // manifest encoding is UTF8
            buf.append( out.toString( "UTF8" ) );
            buf.append("#-----------------------------------------------------------------------").append(NL);
        }
        catch ( Throwable e )
        {
            // ignore...
        }
        return buf;
    }


    protected static void includeMavenResources( MavenProject currentProject, Analyzer analyzer, Log log )
    {
        // pass maven resource paths onto BND analyzer
        final String mavenResourcePaths = getMavenResourcePaths( currentProject, false );
        final String mavenTestResourcePaths = getMavenResourcePaths( currentProject, true );
        final String includeResource = analyzer.getProperty( Analyzer.INCLUDE_RESOURCE );
        if ( includeResource != null )
        {
            if ( includeResource.contains( MAVEN_RESOURCES ) || includeResource.contains( MAVEN_TEST_RESOURCES ) )
            {
                String combinedResource = StringUtils.replace( includeResource, MAVEN_RESOURCES, mavenResourcePaths );
                combinedResource = StringUtils.replace( combinedResource, MAVEN_TEST_RESOURCES, mavenTestResourcePaths );
                if ( combinedResource.length() > 0 )
                {
                    analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, combinedResource );
                }
                else
                {
                    analyzer.unsetProperty( Analyzer.INCLUDE_RESOURCE );
                }
            }
            else if ( mavenResourcePaths.length() > 0 )
            {
                log.warn( Analyzer.INCLUDE_RESOURCE + ": overriding " + mavenResourcePaths + " with " + includeResource
                        + " (add " + MAVEN_RESOURCES + " if you want to include the maven resources)" );
            }
        }
        else if ( mavenResourcePaths.length() > 0 )
        {
            analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, mavenResourcePaths );
        }
    }


    protected void mergeMavenManifest(MavenProject currentProject, Builder builder) throws Exception
    {
        Jar jar = builder.getJar();

        if ( getLog().isDebugEnabled() )
        {
            getLog().debug( "BND Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
        }

        boolean addMavenDescriptor = currentProject.getBasedir() != null;

        try
        {
            /*
             * Grab customized manifest entries from the maven-jar-plugin configuration
             */
            MavenArchiveConfiguration archiveConfig = JarPluginConfiguration.getArchiveConfiguration( currentProject );
            String mavenManifestText = new MavenArchiver().getManifest( session, currentProject, archiveConfig ).toString();
            addMavenDescriptor = addMavenDescriptor && archiveConfig.isAddMavenDescriptor();

            Manifest mavenManifest = new Manifest();

            // First grab the external manifest file (if specified and different to target location)
            File externalManifestFile = archiveConfig.getManifestFile();
            if ( null != externalManifestFile )
            {
                if ( !externalManifestFile.isAbsolute() )
                {
                    externalManifestFile = new File( currentProject.getBasedir(), externalManifestFile.getPath() );
                }
                if ( externalManifestFile.exists() && !externalManifestFile.equals( new File( manifestLocation, "MANIFEST.MF" ) ) )
                {
                    InputStream mis = new FileInputStream( externalManifestFile );
                    mavenManifest.read( mis );
                    mis.close();
                }
            }

            // Then apply customized entries from the jar plugin; note: manifest encoding is UTF8
            mavenManifest.read( new ByteArrayInputStream( mavenManifestText.getBytes( "UTF8" ) ) );

            if ( !archiveConfig.isManifestSectionsEmpty() )
            {
                /*
                 * Add customized manifest sections (for some reason MavenArchiver doesn't do this for us)
                 */
                List<ManifestSection> sections = archiveConfig.getManifestSections();
                for ( Iterator<ManifestSection> i = sections.iterator(); i.hasNext(); )
                {
                    ManifestSection section = i.next();
                    Attributes attributes = new Attributes();

                    if ( !section.isManifestEntriesEmpty() )
                    {
                        Map<String, String> entries = section.getManifestEntries();
                        for ( Iterator<Map.Entry<String, String>> j = entries.entrySet().iterator(); j.hasNext(); )
                        {
                            Map.Entry<String, String> entry = j.next();
                            attributes.putValue( entry.getKey(), entry.getValue() );
                        }
                    }

                    mavenManifest.getEntries().put( section.getName(), attributes );
                }
            }

            Attributes mainMavenAttributes = mavenManifest.getMainAttributes();
            mainMavenAttributes.putValue( "Created-By", createdBy( "Apache Maven Bundle Plugin", "org.apache.felix",
                                                                   "maven-bundle-plugin" ) );

            String[] removeHeaders = builder.getProperty( Constants.REMOVEHEADERS, "" ).split( "," );

            // apply -removeheaders to the custom manifest
            for ( int i = 0; i < removeHeaders.length; i++ )
            {
                for ( Iterator<Object> j = mainMavenAttributes.keySet().iterator(); j.hasNext(); )
                {
                    if ( j.next().toString().matches( removeHeaders[i].trim() ) )
                    {
                        j.remove();
                    }
                }
            }

            /*
             * Overlay generated bundle manifest with customized entries
             */
            Properties properties = builder.getProperties();
            Manifest bundleManifest = jar.getManifest();
            if ( properties.containsKey( "Merge-Headers" ) )
            {
                Instructions instructions = new Instructions( ExtList.from(builder.getProperty("Merge-Headers")) );
                mergeManifest( instructions, bundleManifest, mavenManifest );
            }
            else
            {
                bundleManifest.getMainAttributes().putAll( mainMavenAttributes );
                bundleManifest.getEntries().putAll( mavenManifest.getEntries() );
            }

            // adjust the import package attributes so that optional dependencies use
            // optional resolution.
            String importPackages = bundleManifest.getMainAttributes().getValue( "Import-Package" );
            if ( importPackages != null )
            {
                Set optionalPackages = getOptionalPackages( currentProject);

                Map<String, ? extends Map<String, String>> values;
                try (Analyzer analyzer = new Analyzer()) {
                    values = analyzer.parseHeader( importPackages );
                }
                for ( Map.Entry<String, ? extends Map<String, String>> entry : values.entrySet() )
                {
                    String pkg = entry.getKey();
                    Map<String, String> options = entry.getValue();
                    if ( !options.containsKey( "resolution:" ) && optionalPackages.contains( pkg ) )
                    {
                        options.put( "resolution:", "optional" );
                    }
                }
                String result = Processor.printClauses( values );
                bundleManifest.getMainAttributes().putValue( "Import-Package", result );
            }

            jar.setManifest( bundleManifest );
        }
        catch ( Exception e )
        {
            getLog().warn( "Unable to merge Maven manifest: " + e.getLocalizedMessage() );
        }

        if ( addMavenDescriptor )
        {
            doMavenMetadata( currentProject, jar );
        }

        if ( getLog().isDebugEnabled() )
        {
            getLog().debug( "Final Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
        }

        builder.setJar( jar );
    }

    private static String getCreatedByVersion( String groupId, String artifactId )
    {
        try
        {
            final Properties properties = PropertyUtils.loadProperties( MavenArchiver.class.getResourceAsStream(
                "/META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties" ) );
    
            return properties.getProperty( "version" );
        }
        catch ( IOException ioe )
        {
            return null;
        }
    }

    private String createdBy( String description, String groupId, String artifactId )
    {
        String createdBy = description;
        String version = getCreatedByVersion( groupId, artifactId );
        if ( version != null )
        {
            createdBy += " " + version;
        }
        return createdBy;
    }

    protected static void mergeManifest( Instructions instructions, Manifest... manifests ) throws IOException
    {
        for ( int i = manifests.length - 2; i >= 0; i-- )
        {
            Manifest mergedManifest = manifests[i];
            Manifest manifest = manifests[i + 1];
            Attributes mergedMainAttributes = mergedManifest.getMainAttributes();
            Attributes mainAttributes = manifest.getMainAttributes();
            Attributes filteredMainAttributes = filterAttributes( instructions, mainAttributes, null );
            if ( !filteredMainAttributes.isEmpty() )
            {
                mergeAttributes( mergedMainAttributes, filteredMainAttributes );
            }
            Map<String, Attributes> mergedEntries = mergedManifest.getEntries();
            Map<String, Attributes> entries = manifest.getEntries();
            for ( Map.Entry<String, Attributes> entry : entries.entrySet() )
            {
                String name = entry.getKey();
                Attributes attributes = entry.getValue();
                Attributes filteredAttributes = filterAttributes( instructions, attributes, null );
                if ( !filteredAttributes.isEmpty() )
                {
                    Attributes mergedAttributes = mergedManifest.getAttributes( name );
                    if ( mergedAttributes != null)
                    {
                        mergeAttributes(mergedAttributes, filteredAttributes);
                    }
                    else
                    {
                        mergedEntries.put(name, filteredAttributes);
                    }
                }
            }
        }
    }


    /**
     * @see Analyzer#filter
     */
    private static Attributes filterAttributes(Instructions instructions, Attributes source, Set<Instruction> nomatch) {
        Attributes result = new Attributes();
        Map<String, Object> keys = new TreeMap<>();
        for ( Object key : source.keySet() )
        {
            keys.put( key.toString(), key );
        }

        List<Instruction> filters = new ArrayList<>( instructions.keySet() );
        if (nomatch == null)
        {
            nomatch = Create.set();
        }
        for ( Instruction instruction : filters ) {
            boolean match = false;
            for (Iterator<Map.Entry<String, Object>> i = keys.entrySet().iterator(); i.hasNext();)
            {
                Map.Entry<String, Object> entry = i.next();
                String key = entry.getKey();
                if ( instruction.matches( key ) )
                {
                    match = true;
                    if (!instruction.isNegated()) {
                        Object name = entry.getValue();
                        Object value = source.get( name );
                        result.put( name, value );
                    }
                    i.remove(); // Can never match again for another pattern
                }
            }
            if (!match && !instruction.isAny())
                nomatch.add(instruction);
        }

        /*
         * Tricky. If we have umatched instructions they might indicate that we
         * want to have multiple decorators for the same package. So we check
         * the unmatched against the result list. If then then match and have
         * actually interesting properties then we merge them
         */

        for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
            Instruction instruction = i.next();

            // We assume the user knows what he is
            // doing and inserted a literal. So
            // we ignore any not matched literals
            // #252, we should not be negated to make it a constant
            if (instruction.isLiteral() && !instruction.isNegated()) {
                Object key = keys.get( instruction.getLiteral() );
                if ( key != null )
                {
                    Object value = source.get( key );
                    result.put( key, value );
                }
                i.remove();
                continue;
            }

            // Not matching a negated instruction looks
            // like an error ... Though so, but
            // in the second phase of Export-Package
            // the !package will never match anymore.
            if (instruction.isNegated()) {
                i.remove();
                continue;
            }

            // An optional instruction should not generate
            // an error
            if (instruction.isOptional()) {
                i.remove();
                continue;
            }
        }
        return result;
    }


    private static void mergeAttributes( Attributes... attributesArray ) throws IOException
    {
        for ( int i = attributesArray.length - 2; i >= 0; i-- )
        {
            Attributes mergedAttributes = attributesArray[i];
            Attributes attributes = attributesArray[i + 1];
            for ( Map.Entry<Object, Object> entry : attributes.entrySet() )
            {
                Object name = entry.getKey();
                String value = (String) entry.getValue();
                String oldValue = (String) mergedAttributes.put( name, value );
                if ( oldValue != null )
                {
                    Parameters mergedClauses = OSGiHeader.parseHeader(oldValue);
                    Parameters clauses = OSGiHeader.parseHeader( value );
                    if ( !mergedClauses.isEqual( clauses) )
                    {
                        for ( Map.Entry<String, Attrs> clauseEntry : clauses.entrySet() )
                        {
                            String clause = clauseEntry.getKey();
                            Attrs attrs = clauseEntry.getValue();
                            Attrs mergedAttrs = mergedClauses.get( clause );
                            if ( mergedAttrs == null)
                            {
                                mergedClauses.put( clause, attrs );
                            }
                            else if ( !mergedAttrs.isEqual(attrs) )
                            {
                                for ( Map.Entry<String,String> adentry : attrs.entrySet() )
                                {
                                    String adname = adentry.getKey();
                                    String ad = adentry.getValue();
                                    if ( mergedAttrs.containsKey( adname ) )
                                    {
                                        Attrs.Type type = attrs.getType( adname );
                                        switch (type)
                                        {
                                            case VERSIONS:
                                            case STRINGS:
                                            case LONGS:
                                            case DOUBLES:
                                                ExtList<String> mergedAd = ExtList.from( mergedAttrs.get( adname ) );
                                                ExtList.from( ad ).addAll( ExtList.from( ad ) );
                                                mergedAttrs.put(adname, mergedAd.join() );
                                                break;
                                        }
                                    }
                                    else
                                    {
                                        mergedAttrs.put( adname, ad );
                                    }
                                }
                            }
                        }
                        mergedAttributes.put( name, Processor.printClauses( mergedClauses ) );
                    }
                }
            }
        }
    }


    protected Set<String> getOptionalPackages(MavenProject currentProject) throws IOException, MojoExecutionException
    {
        ArrayList<Artifact> inscope = new ArrayList<>();
        final Collection<Artifact> artifacts = getSelectedDependencies(currentProject.getArtifacts() );
        for ( Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); )
        {
            Artifact artifact = it.next();
            if ( artifact.getArtifactHandler().isAddedToClasspath() )
            {
                inscope.add( artifact );
            }
        }

        HashSet<String> optionalArtifactIds = new HashSet<>();
        for ( Iterator<Artifact> it = inscope.iterator(); it.hasNext(); )
        {
            Artifact artifact = it.next();
            if ( artifact.isOptional() )
            {
                String id = artifact.toString();
                if ( artifact.getScope() != null )
                {
                    // strip the scope...
                    id = id.replaceFirst( ":[^:]*$", "" );
                }
                optionalArtifactIds.add( id );
            }

        }

        HashSet<String> required = new HashSet<>();
        HashSet<String> optional = new HashSet<>();
        for ( Iterator<Artifact> it = inscope.iterator(); it.hasNext(); )
        {
            Artifact artifact = it.next();
            File file = getFile( artifact );
            if ( file == null )
            {
                continue;
            }

            Jar jar = new Jar( artifact.getArtifactId(), file );
            if ( isTransitivelyOptional( optionalArtifactIds, artifact ) )
            {
                optional.addAll( jar.getPackages() );
            }
            else
            {
                required.addAll( jar.getPackages() );
            }
            jar.close();
        }

        optional.removeAll( required );
        return optional;
    }


    /**
     * Check to see if any dependency along the dependency trail of
     * the artifact is optional.
     *
     * @param artifact
     */
    protected boolean isTransitivelyOptional( HashSet<String> optionalArtifactIds, Artifact artifact )
    {
        List<String> trail = artifact.getDependencyTrail();
        for ( Iterator<String> iterator = trail.iterator(); iterator.hasNext(); )
        {
            String next = iterator.next();
            if ( optionalArtifactIds.contains( next ) )
            {
                return true;
            }
        }
        return false;
    }


    private void unpackBundle( File jarFile )
    {
        File outputDir = getOutputDirectory();
        if ( null == outputDir )
        {
            outputDir = new File( getBuildDirectory(), "classes" );
        }

        try
        {
            /*
             * this directory must exist before unpacking, otherwise the plexus
             * unarchiver decides to use the current working directory instead!
             */
            if ( !outputDir.exists() )
            {
                outputDir.mkdirs();
            }

            UnArchiver unArchiver = m_archiverManager.getUnArchiver( "jar" );
            unArchiver.setDestDirectory( outputDir );
            unArchiver.setSourceFile( jarFile );
            unArchiver.extract();
        }
        catch ( Exception e )
        {
            getLog().error( "Problem unpacking " + jarFile + " to " + outputDir, e );
        }
    }


    protected static String removeTagFromInstruction( String instruction, String tag )
    {
        StringBuffer buf = new StringBuffer();

        String[] clauses = instruction.split( "," );
        for ( int i = 0; i < clauses.length; i++ )
        {
            String clause = clauses[i].trim();
            if ( !tag.equals( clause ) )
            {
                if ( buf.length() > 0 )
                {
                    buf.append( ',' );
                }
                buf.append( clause );
            }
        }

        return buf.toString();
    }


    private static Map<String, String> getProperties( Model projectModel, String prefix )
    {
        Map<String, String> properties = new LinkedHashMap<>();
        Method methods[] = Model.class.getDeclaredMethods();
        for ( int i = 0; i < methods.length; i++ )
        {
            String name = methods[i].getName();
            if ( name.startsWith( "get" ) )
            {
                try
                {
                    Object v = methods[i].invoke( projectModel, null );
                    if ( v != null )
                    {
                        name = prefix + Character.toLowerCase( name.charAt( 3 ) ) + name.substring( 4 );
                        if ( v.getClass().isArray() )
                            properties.put( name, Arrays.asList( ( Object[] ) v ).toString() );
                        else
                            properties.put( name, v.toString() );

                    }
                }
                catch ( Exception e )
                {
                    // too bad
                }
            }
        }
        return properties;
    }


    private static StringBuffer printLicenses( List<License> licenses )
    {
        if ( licenses == null || licenses.size() == 0 )
            return null;
        StringBuffer sb = new StringBuffer();
        String del = "";
        for ( Iterator<License> i = licenses.iterator(); i.hasNext(); )
        {
            License l = i.next();
            String url = l.getUrl();
            if ( url == null )
                continue;
            sb.append( del );
            sb.append( url );
            del = ", ";
        }
        if ( sb.length() == 0 )
            return null;
        return sb;
    }


    /**
     * @param jar
     * @throws IOException
     */
    private void doMavenMetadata( MavenProject currentProject, Jar jar ) throws IOException
    {
        String path = "META-INF/maven/" + currentProject.getGroupId() + "/" + currentProject.getArtifactId();

        File pomFile = currentProject.getFile();
        if ( pomFile == null || !pomFile.exists() )
        {
            pomFile = new File( currentProject.getBasedir(), "pom.xml" );
        }
        if ( pomFile.exists() )
        {
            jar.putResource( path + "/pom.xml", new FileResource( pomFile ) );
        }

        Properties p = new Properties();
        p.put( "version", currentProject.getVersion() );
        p.put( "groupId", currentProject.getGroupId() );
        p.put( "artifactId", currentProject.getArtifactId() );
        jar.putResource( path + "/pom.properties", new EmbeddedResource( toFileContentAsBytes( p ), System.currentTimeMillis() ) );
    }


    private byte[] toFileContentAsBytes( Properties properties )
    {
        byte[] bytes;
        try
        {
            StringWriter sw = new StringWriter();
            properties.store( sw, null );
    
            BufferedReader r = new BufferedReader( new StringReader( sw.toString() ) );
            StringWriter stringWriter = new StringWriter();
    
            String line;
            while ( ( line = r.readLine() ) != null )
            {
                if ( !line.startsWith( "#" ) )
                {
                    stringWriter.append( line ).append( NL );
                }
            }
            bytes = stringWriter.getBuffer().toString().getBytes( StandardCharsets.ISO_8859_1 );
           
            r.close();
            sw.close();
            stringWriter.close();
        }
        catch ( IOException e )
        {
            getLog().error( "Error while converting properties to file content. Returning empty array.", e );
            bytes = new byte[] {};
        }
        return bytes;
    }


    protected ClassPathItem[] getClasspath(MavenProject currentProject) throws IOException, MojoExecutionException
    {
        List<ClassPathItem> list = new ArrayList<>( currentProject.getArtifacts().size() + 1 );

        if ( getOutputDirectory() != null && getOutputDirectory().exists() )
        {
            list.add( new ClassPathItem( ".", getOutputDirectory() ) );
        }

        final Collection<Artifact> artifacts = getSelectedDependencies(currentProject.getArtifacts() );
        for ( Artifact artifact : artifacts  )
        {
            if ( artifact.getArtifactHandler().isAddedToClasspath() && !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
            {
                File file = getFile( artifact );
                if ( file == null )
                {
                    getLog().warn(
                            "File is not available for artifact " + artifact + " in project "
                                    + currentProject.getArtifact() );
                    continue;
                }
                ClassPathItem jar = new ClassPathItem( artifact.getArtifactId(), file );
                list.add( jar );
            }
        }
        ClassPathItem[] cp = new ClassPathItem[list.size()];
        list.toArray( cp );

        return cp;
    }


    private Collection<Artifact> getSelectedDependencies(Collection<Artifact> artifacts) throws MojoExecutionException
    {
        if ( null == excludeDependencies || excludeDependencies.isEmpty() )
        {
            return artifacts;
        }
        else if ( "true".equalsIgnoreCase( excludeDependencies ) )
        {
            return Collections.emptyList();
        }

        Collection<Artifact> selectedDependencies = new LinkedHashSet<>( artifacts );
        DependencyExcluder excluder = new DependencyExcluder(artifacts );
        excluder.processHeaders( excludeDependencies );
        selectedDependencies.removeAll( excluder.getExcludedArtifacts() );

        return selectedDependencies;
    }


    /**
     * Get the file for an Artifact
     *
     * @param artifact
     */
    protected File getFile( Artifact artifact )
    {
        return artifact.getFile();
    }


    private static void header( Properties properties, String key, Object value )
    {
        if ( value == null )
            return;

        if ( value instanceof Collection && ( ( Collection ) value ).isEmpty() )
            return;

        properties.put( key, value.toString().replaceAll( "[\r\n]", "" ) );
    }


    /**
     * Convert a Maven version into an OSGi compliant version
     *
     * @param version Maven version
     * @return the OSGi version
     */
    protected String convertVersionToOsgi( String version )
    {
        return getMaven2OsgiConverter().getVersion( version );
    }


    /**
     * TODO this should return getMaven2Osgi().getBundleFileName( project.getArtifact() )
     */
    protected String getBundleName( MavenProject currentProject )
    {
        String extension;
        try
        {
            extension = currentProject.getArtifact().getArtifactHandler().getExtension();
        }
        catch ( Throwable e )
        {
            extension = currentProject.getArtifact().getType();
        }
        if ( StringUtils.isEmpty( extension ) || "bundle".equals( extension ) || "pom".equals( extension ) )
        {
            extension = "jar"; // just in case maven gets confused
        }
        if ( null != classifier && classifier.trim().length() > 0 )
        {
            return finalName + '-' + classifier + '.' + extension;
        }
        return finalName + '.' + extension;
    }


    protected String getBuildDirectory()
    {
        return buildDirectory;
    }


    protected void setBuildDirectory( String _buildirectory )
    {
        buildDirectory = _buildirectory;
    }


    protected Properties getDefaultProperties( MavenProject currentProject )
    {
        Properties properties = new Properties();

        String bsn;
        try
        {
            bsn = getMaven2OsgiConverter().getBundleSymbolicName( currentProject.getArtifact() );
        }
        catch ( Exception e )
        {
            bsn = currentProject.getGroupId() + "." + currentProject.getArtifactId();
        }

        // Setup defaults
        properties.put( MAVEN_SYMBOLICNAME, bsn );
        properties.put( Analyzer.BUNDLE_SYMBOLICNAME, bsn );
        properties.put( Analyzer.IMPORT_PACKAGE, "*" );
        properties.put( Analyzer.BUNDLE_VERSION, getMaven2OsgiConverter().getVersion( currentProject.getVersion() ) );

        // remove the extraneous Include-Resource and Private-Package entries from generated manifest
        properties.put( Constants.REMOVEHEADERS, Analyzer.INCLUDE_RESOURCE + ',' + Analyzer.PRIVATE_PACKAGE );

        header( properties, Analyzer.BUNDLE_DESCRIPTION, currentProject.getDescription() );
        StringBuffer licenseText = printLicenses( currentProject.getLicenses() );
        if ( licenseText != null )
        {
            header( properties, Analyzer.BUNDLE_LICENSE, licenseText );
        }
        header( properties, Analyzer.BUNDLE_NAME, currentProject.getName() );

        if ( currentProject.getOrganization() != null )
        {
            if ( currentProject.getOrganization().getName() != null )
            {
                String organizationName = currentProject.getOrganization().getName();
                header( properties, Analyzer.BUNDLE_VENDOR, organizationName );
                properties.put( "project.organization.name", organizationName );
                properties.put( "pom.organization.name", organizationName );
            }
            if ( currentProject.getOrganization().getUrl() != null )
            {
                String organizationUrl = currentProject.getOrganization().getUrl();
                header( properties, Analyzer.BUNDLE_DOCURL, organizationUrl );
                properties.put( "project.organization.url", organizationUrl );
                properties.put( "pom.organization.url", organizationUrl );
            }
        }

        properties.putAll( currentProject.getProperties() );
        properties.putAll( currentProject.getModel().getProperties() );

        for ( Iterator<String> i = currentProject.getFilters().iterator(); i.hasNext(); )
        {
            File filterFile = new File( i.next() );
            if ( filterFile.isFile() )
            {
                try
                {
                    properties.putAll( PropertyUtils.loadProperties( filterFile ) );
                }
                catch ( IOException e )
                {
                    // Ignore
                }
            }
        }

        if ( m_mavenSession != null )
        {
            try
            {
                // don't pass upper-case session settings to bnd as they end up in the manifest
                Properties sessionProperties = m_mavenSession.getExecutionProperties();
                for ( Enumeration<String> e = (Enumeration<String>) sessionProperties.propertyNames(); e.hasMoreElements(); )
                {
                    String key = e.nextElement();
                    if ( key.length() > 0 && !Character.isUpperCase( key.charAt( 0 ) ) )
                    {
                        properties.put( key, sessionProperties.getProperty( key ) );
                    }
                }
            }
            catch ( Exception e )
            {
                getLog().warn( "Problem with Maven session properties: " + e.getLocalizedMessage() );
            }
        }

        properties.putAll( getProperties( currentProject.getModel(), "project.build." ) );
        properties.putAll( getProperties( currentProject.getModel(), "pom." ) );
        properties.putAll( getProperties( currentProject.getModel(), "project." ) );

        properties.put( "project.baseDir", getBase( currentProject ) );
        properties.put( "project.build.directory", getBuildDirectory() );
        properties.put( "project.build.outputdirectory", getOutputDirectory() );

        properties.put( "classifier", classifier == null ? "" : classifier );

        // Add default plugins
        header( properties, Analyzer.PLUGIN, BlueprintPlugin.class.getName() + ","
                                           + SpringXMLType.class.getName() + ","
                                           + JpaPlugin.class.getName() );

        return properties;
    }


    protected static File getBase( MavenProject currentProject )
    {
        return currentProject.getBasedir() != null ? currentProject.getBasedir() : new File( "" );
    }


    protected File getOutputDirectory()
    {
        return outputDirectory;
    }


    protected void setOutputDirectory( File _outputDirectory )
    {
        outputDirectory = _outputDirectory;
    }


    private static void addLocalPackages( File outputDirectory, Analyzer analyzer ) throws IOException
    {
        Packages packages = new Packages();

        if ( outputDirectory != null && outputDirectory.isDirectory() )
        {
            // scan classes directory for potential packages
            DirectoryScanner scanner = new DirectoryScanner();
            scanner.setBasedir( outputDirectory );
            scanner.setIncludes( new String[]
                { "**/*.class" } );

            scanner.addDefaultExcludes();
            scanner.scan();

            String[] paths = scanner.getIncludedFiles();
            Arrays.sort( paths );
            for ( int i = 0; i < paths.length; i++ )
            {
                packages.put( analyzer.getPackageRef( getPackageName( paths[i] ) ) );
            }
        }

        Packages exportedPkgs = new Packages();
        Packages privatePkgs = new Packages();

        boolean noprivatePackages = "!*".equals( analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) );

        for ( PackageRef pkg : packages.keySet() )
        {
            // mark all source packages as private by default (can be overridden by export list)
            privatePkgs.put( pkg );

            // we can't export the default package (".") and we shouldn't export internal packages
            String fqn = pkg.getFQN();
            if ( noprivatePackages || !( ".".equals( fqn ) || fqn.contains( ".internal" ) || fqn.contains( ".impl" ) ) )
            {
                exportedPkgs.put( pkg );
            }
        }

        Properties properties = analyzer.getProperties();
        String exported = properties.getProperty( Analyzer.EXPORT_PACKAGE );
        if ( exported == null )
        {
            if ( !properties.containsKey( Analyzer.EXPORT_CONTENTS ) )
            {
                // no -exportcontents overriding the exports, so use our computed list
                for ( Attrs attrs : exportedPkgs.values() )
                {
                    attrs.put( Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first" );
                }
                properties.setProperty( Analyzer.EXPORT_PACKAGE, Processor.printClauses( exportedPkgs ) );
            }
            else
            {
                // leave Export-Package empty (but non-null) as we have -exportcontents
                properties.setProperty( Analyzer.EXPORT_PACKAGE, "" );
            }
        }
        else if ( exported.indexOf( LOCAL_PACKAGES ) >= 0 )
        {
            String newExported = StringUtils.replace( exported, LOCAL_PACKAGES, Processor.printClauses( exportedPkgs ) );
            properties.setProperty( Analyzer.EXPORT_PACKAGE, newExported );
        }

        String internal = properties.getProperty( Analyzer.PRIVATE_PACKAGE );
        if ( internal == null )
        {
            if ( !privatePkgs.isEmpty() )
            {
                for ( Attrs attrs : privatePkgs.values() )
                {
                    attrs.put( Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first" );
                }
                properties.setProperty( Analyzer.PRIVATE_PACKAGE, Processor.printClauses( privatePkgs ) );
            }
            else
            {
                // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
                properties.setProperty( Analyzer.PRIVATE_PACKAGE, "!*" );
            }
        }
        else if ( internal.indexOf( LOCAL_PACKAGES ) >= 0 )
        {
            String newInternal = StringUtils.replace( internal, LOCAL_PACKAGES, Processor.printClauses( privatePkgs ) );
            properties.setProperty( Analyzer.PRIVATE_PACKAGE, newInternal );
        }
    }


    private static String getPackageName( String filename )
    {
        int n = filename.lastIndexOf( File.separatorChar );
        return n < 0 ? "." : filename.substring( 0, n ).replace( File.separatorChar, '.' );
    }


    private static List<Resource> getMavenResources( MavenProject currentProject, boolean test )
    {
        List<Resource> resources = new ArrayList<>( test ? currentProject.getTestResources() : currentProject.getResources() );

        if ( currentProject.getCompileSourceRoots() != null )
        {
            // also scan for any "packageinfo" files lurking in the source folders
            final List<String> packageInfoIncludes = Collections.singletonList( "**/packageinfo" );
            for ( Iterator<String> i = currentProject.getCompileSourceRoots().iterator(); i.hasNext(); )
            {
                String sourceRoot = i.next();
                Resource packageInfoResource = new Resource();
                packageInfoResource.setDirectory( sourceRoot );
                packageInfoResource.setIncludes( packageInfoIncludes );
                resources.add( packageInfoResource );
            }
        }

        return resources;
    }


    protected static String getMavenResourcePaths( MavenProject currentProject, boolean test )
    {
        final String basePath = currentProject.getBasedir().getAbsolutePath();

        Set<String> pathSet = new LinkedHashSet<>();
        for ( Iterator<Resource> i = getMavenResources( currentProject, test ).iterator(); i.hasNext(); )
        {
            Resource resource = i.next();

            final String sourcePath = resource.getDirectory();
            final String targetPath = resource.getTargetPath();

            // ignore empty or non-local resources
            if ( new File( sourcePath ).exists() && ( ( targetPath == null ) || ( targetPath.indexOf( ".." ) < 0 ) ) )
            {
                DirectoryScanner scanner = new DirectoryScanner();

                scanner.setBasedir( sourcePath );
                if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() )
                {
                    scanner.setIncludes( resource.getIncludes().toArray( EMPTY_STRING_ARRAY ) );
                }
                else
                {
                    scanner.setIncludes( DEFAULT_INCLUDES );
                }

                if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() )
                {
                    scanner.setExcludes( resource.getExcludes().toArray( EMPTY_STRING_ARRAY ) );
                }

                scanner.addDefaultExcludes();
                scanner.scan();

                String[] f = scanner.getIncludedFiles();
                Arrays.sort( f );
                List<String> includedFiles = Arrays.asList( f );

                for ( Iterator<String> j = includedFiles.iterator(); j.hasNext(); )
                {
                    String name = j.next();
                    String path = sourcePath + '/' + name;

                    // make relative to project
                    if ( path.startsWith( basePath ) )
                    {
                        if ( path.length() == basePath.length() )
                        {
                            path = ".";
                        }
                        else
                        {
                            path = path.substring( basePath.length() + 1 );
                        }
                    }

                    // replace windows backslash with a slash
                    // this is a workaround for a problem with bnd 0.0.189
                    if ( File.separatorChar != '/' )
                    {
                        name = name.replace( File.separatorChar, '/' );
                        path = path.replace( File.separatorChar, '/' );
                    }

                    // copy to correct place
                    path = name + '=' + path;
                    if ( targetPath != null )
                    {
                        path = targetPath + '/' + path;
                    }

                    // use Bnd filtering?
                    if ( resource.isFiltering() )
                    {
                        path = '{' + path + '}';
                    }

                    pathSet.add( path );
                }
            }
        }

        StringBuffer resourcePaths = new StringBuffer();
        for ( Iterator<String> i = pathSet.iterator(); i.hasNext(); )
        {
            resourcePaths.append( i.next() );
            if ( i.hasNext() )
            {
                resourcePaths.append( ',' );
            }
        }

        return resourcePaths.toString();
    }


    protected Collection<Artifact> getEmbeddableArtifacts(MavenProject currentProject, Analyzer analyzer)
        throws MojoExecutionException
    {
        final Collection<Artifact> artifacts;

        String embedTransitive = analyzer.getProperty( DependencyEmbedder.EMBED_TRANSITIVE );
        if (Boolean.valueOf(embedTransitive))
        {
            // includes transitive dependencies
            artifacts = currentProject.getArtifacts();
        }
        else
        {
            // only includes direct dependencies
            artifacts = currentProject.getDependencyArtifacts();
        }

        return getSelectedDependencies(artifacts );
    }


    protected static void addMavenSourcePath( MavenProject currentProject, Analyzer analyzer, Log log )
    {
        // pass maven source paths onto BND analyzer
        StringBuilder mavenSourcePaths = new StringBuilder();
        StringBuilder mavenTestSourcePaths = new StringBuilder();
        Map<StringBuilder, List<String>> map = new HashMap<>(2);
        map.put(mavenSourcePaths, currentProject.getCompileSourceRoots() );
        map.put(mavenTestSourcePaths, currentProject.getTestCompileSourceRoots() );
        for ( Map.Entry<StringBuilder, List<String>> entry : map.entrySet() )
        {
            List<String> compileSourceRoots = entry.getValue();
            if ( compileSourceRoots != null )
            {
                StringBuilder sourcePaths = entry.getKey();
                for ( Iterator<String> i = compileSourceRoots.iterator(); i.hasNext(); )
                {
                    if ( sourcePaths.length() > 0 )
                    {
                        sourcePaths.append( ',' );
                    }
                    sourcePaths.append( i.next() );
                }
            }
        }
        final String sourcePath = analyzer.getProperty( Analyzer.SOURCEPATH );
        if ( sourcePath != null )
        {
            if ( sourcePath.contains(MAVEN_SOURCES) || sourcePath.contains(MAVEN_TEST_SOURCES) )
            {
                String combinedSource = StringUtils.replace( sourcePath, MAVEN_SOURCES, mavenSourcePaths.toString() );
                combinedSource = StringUtils.replace( combinedSource, MAVEN_TEST_SOURCES, mavenTestSourcePaths.toString() );
                if ( combinedSource.length() > 0 )
                {
                    analyzer.setProperty( Analyzer.SOURCEPATH, combinedSource );
                }
                else
                {
                    analyzer.unsetProperty( Analyzer.SOURCEPATH );
                }
            }
            else if ( mavenSourcePaths.length() > 0 )
            {
                log.warn( Analyzer.SOURCEPATH + ": overriding " + mavenSourcePaths + " with " + sourcePath + " (add "
                    + MAVEN_SOURCES + " if you want to include the maven sources)" );
            }
            else if ( mavenTestSourcePaths.length() > 0 )
            {
                log.warn( Analyzer.SOURCEPATH + ": overriding " + mavenTestSourcePaths + " with " + sourcePath + " (add "
                        + MAVEN_TEST_SOURCES + " if you want to include the maven test sources)" );
            }
        }
        else if ( mavenSourcePaths.length() > 0 )
        {
            analyzer.setProperty( Analyzer.SOURCEPATH, mavenSourcePaths.toString() );
        }
    }

    static class ClassPathItem {
        final String id;
        final File file;

        public ClassPathItem(String id, File file) {
            this.id = id;
            this.file = file;
        }
    }
}
