View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.felix.bundleplugin;
20  
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.Writer;
29  import java.lang.reflect.Array;
30  import java.lang.reflect.Method;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.Collections;
35  import java.util.Enumeration;
36  import java.util.HashMap;
37  import java.util.HashSet;
38  import java.util.Iterator;
39  import java.util.LinkedHashMap;
40  import java.util.LinkedHashSet;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Map.Entry;
44  import java.util.Properties;
45  import java.util.Set;
46  import java.util.TreeMap;
47  import java.util.jar.Attributes;
48  import java.util.jar.Manifest;
49  
50  import org.apache.felix.bundleplugin.pom.PomWriter;
51  import org.apache.maven.archiver.ManifestSection;
52  import org.apache.maven.archiver.MavenArchiveConfiguration;
53  import org.apache.maven.archiver.MavenArchiver;
54  import org.apache.maven.artifact.Artifact;
55  import org.apache.maven.artifact.factory.ArtifactFactory;
56  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
57  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
58  import org.apache.maven.artifact.repository.ArtifactRepository;
59  import org.apache.maven.artifact.resolver.ArtifactCollector;
60  import org.apache.maven.artifact.resolver.ArtifactResolver;
61  import org.apache.maven.execution.MavenSession;
62  import org.apache.maven.model.Dependency;
63  import org.apache.maven.model.Exclusion;
64  import org.apache.maven.model.License;
65  import org.apache.maven.model.Model;
66  import org.apache.maven.model.Resource;
67  import org.apache.maven.plugin.AbstractMojo;
68  import org.apache.maven.plugin.MojoExecutionException;
69  import org.apache.maven.plugin.MojoFailureException;
70  import org.apache.maven.plugin.logging.Log;
71  import org.apache.maven.plugins.annotations.Component;
72  import org.apache.maven.plugins.annotations.LifecyclePhase;
73  import org.apache.maven.plugins.annotations.Mojo;
74  import org.apache.maven.plugins.annotations.Parameter;
75  import org.apache.maven.plugins.annotations.ResolutionScope;
76  import org.apache.maven.project.MavenProject;
77  import org.apache.maven.project.MavenProjectBuilder;
78  import org.apache.maven.project.MavenProjectHelper;
79  import org.apache.maven.project.ProjectBuildingException;
80  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
81  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
82  import org.apache.maven.shared.osgi.DefaultMaven2OsgiConverter;
83  import org.apache.maven.shared.osgi.Maven2OsgiConverter;
84  import org.codehaus.plexus.archiver.UnArchiver;
85  import org.codehaus.plexus.archiver.manager.ArchiverManager;
86  import org.codehaus.plexus.util.DirectoryScanner;
87  import org.codehaus.plexus.util.FileUtils;
88  import org.codehaus.plexus.util.PropertyUtils;
89  import org.codehaus.plexus.util.StringUtils;
90  import org.codehaus.plexus.util.WriterFactory;
91  import org.sonatype.plexus.build.incremental.BuildContext;
92  
93  import aQute.bnd.header.Attrs;
94  import aQute.bnd.header.OSGiHeader;
95  import aQute.bnd.header.Parameters;
96  import aQute.bnd.osgi.Analyzer;
97  import aQute.bnd.osgi.Builder;
98  import aQute.bnd.osgi.Constants;
99  import aQute.bnd.osgi.Descriptors.PackageRef;
100 import aQute.bnd.osgi.EmbeddedResource;
101 import aQute.bnd.osgi.FileResource;
102 import aQute.bnd.osgi.Instruction;
103 import aQute.bnd.osgi.Instructions;
104 import aQute.bnd.osgi.Jar;
105 import aQute.bnd.osgi.Packages;
106 import aQute.bnd.osgi.Processor;
107 import aQute.lib.collections.ExtList;
108 import aQute.lib.spring.SpringXMLType;
109 import aQute.libg.generics.Create;
110 
111 
112 /**
113  * Create an OSGi bundle from Maven project
114  *
115  */
116 @Mojo( name = "bundle", requiresDependencyResolution = ResolutionScope.TEST,
117        threadSafe = true,
118        defaultPhase = LifecyclePhase.PACKAGE )
119 public class BundlePlugin extends AbstractMojo
120 {
121     /**
122      * Directory where the manifest will be written
123      */
124     @Parameter( property = "manifestLocation", defaultValue = "${project.build.outputDirectory}/META-INF" )
125     protected File manifestLocation;
126 
127     /**
128      * Output a nicely formatted manifest that still respects the 72 character line limit.
129      */
130     @Parameter( property = "niceManifest", defaultValue = "false" )
131     protected boolean niceManifest;
132 
133     /**
134      * File where the BND instructions will be dumped
135      */
136     @Parameter( property = "dumpInstructions" )
137     protected File dumpInstructions;
138 
139     /**
140      * File where the BND class-path will be dumped
141      */
142     @Parameter( property = "dumpClasspath" )
143     protected File dumpClasspath;
144 
145     /**
146      * When true, unpack the bundle contents to the outputDirectory
147      */
148     @Parameter( property = "unpackBundle" )
149     protected boolean unpackBundle;
150 
151     /**
152      * Comma separated list of artifactIds to exclude from the dependency classpath passed to BND (use "true" to exclude everything)
153      */
154     @Parameter( property = "excludeDependencies" )
155     protected String excludeDependencies;
156 
157     /**
158      * Final name of the bundle (without classifier or extension)
159      */
160     @Parameter( defaultValue = "${project.build.finalName}")
161     private String finalName;
162 
163     /**
164      * Classifier type of the bundle to be installed.  For example, "jdk14".
165      * Defaults to none which means this is the project's main bundle.
166      */
167     @Parameter
168     protected String classifier;
169 
170     /**
171      * Packaging type of the bundle to be installed.  For example, "jar".
172      * Defaults to none which means use the same packaging as the project.
173      */
174     @Parameter
175     protected String packaging;
176 
177     /**
178      * If true, remove any inlined or embedded dependencies from the resulting pom.
179      */
180     @Parameter
181     protected boolean createDependencyReducedPom;
182 
183     /**
184      * Where to put the dependency reduced pom. Note: setting a value for this parameter with a directory other than
185      * ${basedir} will change the value of ${basedir} for all executions that come after the shade execution. This is
186      * often not what you want. This is considered an open issue with this plugin.
187      */
188     @Parameter( defaultValue = "${basedir}/dependency-reduced-pom.xml" )
189     protected File dependencyReducedPomLocation;
190 
191     /**
192      * Directory where the SCR files will be written
193      */
194     @Parameter(defaultValue="${project.build.outputDirectory}")
195     protected File scrLocation;
196 
197     /**
198      * When true, dump the generated SCR files
199      */
200     @Parameter
201     protected boolean exportScr;
202     
203     @Component
204     private MavenProjectHelper m_projectHelper;
205 
206     @Component
207     private ArchiverManager m_archiverManager;
208 
209     @Component
210     private ArtifactHandlerManager m_artifactHandlerManager;
211 
212     /* The current Maven session.  */
213     @Parameter( defaultValue = "${session}", readonly = true )
214     protected MavenSession session;
215 
216 
217     /**
218      * ProjectBuilder, needed to create projects from the artifacts.
219      */
220     @Component
221     protected MavenProjectBuilder mavenProjectBuilder;
222 
223     @Component
224     private DependencyTreeBuilder dependencyTreeBuilder;
225 
226     @Component
227     private ArtifactMetadataSource artifactMetadataSource;
228 
229     @Component
230     private ArtifactCollector artifactCollector;
231 
232     @Component
233     protected ArtifactFactory artifactFactory;
234 
235     /**
236      * Artifact resolver, needed to download source jars for inclusion in classpath.
237      */
238     @Component
239     protected ArtifactResolver artifactResolver;
240 
241 
242     /**
243      * Local maven repository.
244      */
245     @Parameter( readonly = true, required = true, defaultValue = "${localRepository}" )
246     protected ArtifactRepository localRepository;
247 
248     /**
249      * Remote repositories which will be searched for source attachments.
250      */
251     @Parameter( readonly = true, required = true, defaultValue = "${project.remoteArtifactRepositories}" )
252     protected List<ArtifactRepository> remoteArtifactRepositories;
253 
254 
255 
256     /**
257      * Project types which this plugin supports.
258      */
259     @Parameter
260     protected List<String> supportedProjectTypes = Arrays.asList("jar", "bundle");
261 
262     /**
263      * Project types which are not supported, but silently ignored.
264      */
265     @Parameter
266     protected List<String> noWarningProjectTypes = Collections.emptyList();
267 
268     /**
269      * The directory for the generated bundles.
270      */
271     @Parameter( defaultValue = "${project.build.outputDirectory}" )
272     private File outputDirectory;
273 
274     /**
275      * The directory for the generated JAR.
276      */
277     @Parameter( defaultValue = "${project.build.directory}" )
278     private String buildDirectory;
279 
280     /**
281      * The Maven project.
282      */
283     @Parameter( defaultValue = "${project}", readonly = true, required = true )
284     protected MavenProject project;
285 
286     /**
287      * The BND instructions for the bundle.
288      * Maven will expand property macros in these values. If you want to use a BND macro, you must double the dollar sign
289      * for the plugin to pass it to BND correctly. For example: <br>
290      * {@code <_consumer-policy>$${range;[===,+)}<code>}<code>{@code </_consumer-policy> }
291      */
292     @Parameter
293     private Map<String, String> instructions = new LinkedHashMap<String, String>();
294 
295     /**
296      * Use locally patched version for now.
297      */
298     private final Maven2OsgiConverter m_maven2OsgiConverter = new DefaultMaven2OsgiConverter();
299 
300     /**
301      * The archive configuration to use.
302      */
303     @Parameter
304     private MavenArchiveConfiguration archive; // accessed indirectly in JarPluginConfiguration
305 
306     @Parameter( defaultValue = "${session}", readonly = true, required = true )
307     private MavenSession m_mavenSession;
308 
309     @Component
310     protected BuildContext buildContext;
311     
312     private static final String MAVEN_SYMBOLICNAME = "maven-symbolicname";
313     private static final String MAVEN_RESOURCES = "{maven-resources}";
314     private static final String MAVEN_TEST_RESOURCES = "{maven-test-resources}";
315     private static final String LOCAL_PACKAGES = "{local-packages}";
316     private static final String MAVEN_SOURCES = "{maven-sources}";
317     private static final String MAVEN_TEST_SOURCES = "{maven-test-sources}";
318     private static final String BUNDLE_PLUGIN_EXTENSION = "BNDExtension-";
319     private static final String BUNDLE_PLUGIN_PREPEND_EXTENSION = "BNDPrependExtension-";
320 
321     private static final String[] EMPTY_STRING_ARRAY =
322         {};
323     private static final String[] DEFAULT_INCLUDES =
324         { "**/**" };
325 
326     private static final String NL = System.getProperty( "line.separator" );
327 
328 
329     protected Maven2OsgiConverter getMaven2OsgiConverter()
330     {
331         return m_maven2OsgiConverter;
332     }
333 
334 
335     protected MavenProject getProject()
336     {
337         return project;
338     }
339 
340     protected Map<String, String> getInstructions() {
341         return instructions;
342     }
343 
344     /**
345      * @see org.apache.maven.plugin.AbstractMojo#execute()
346      */
347     public void execute() throws MojoExecutionException
348     {
349         String projectType = getProject().getArtifact().getType();
350 
351         // ignore unsupported project types, useful when bundleplugin is configured in parent pom
352         if ( !supportedProjectTypes.contains( projectType ) )
353         {
354             if (!noWarningProjectTypes.contains( projectType ) )
355             {
356                 getLog().warn(
357                         "Ignoring project type " + projectType + " - supportedProjectTypes = " + supportedProjectTypes);
358             }
359             return;
360         }
361 
362         try
363         {
364             execute( instructions, getClasspath( project) );
365         }
366         catch ( IOException e )
367         {
368             throw new MojoExecutionException( "Error calculating classpath for project " + project, e );
369         }
370     }
371 
372 
373     /* transform directives from their XML form to the expected BND syntax (eg. _include becomes -include) */
374     protected static Map<String, String> transformDirectives( Map<String, String> originalInstructions )
375     {
376         Map<String, String> transformedInstructions = new LinkedHashMap<String, String>();
377         for ( Map.Entry<String, String> e : originalInstructions.entrySet() )
378         {
379             String key = e.getKey();
380             if ( key.startsWith( "_" ) )
381             {
382                 key = "-" + key.substring( 1 );
383             }
384 
385             String value = e.getValue();
386             if ( null == value )
387             {
388                 value = "";
389             }
390             else
391             {
392                 value = value.replaceAll( "\\p{Blank}*[\r\n]\\p{Blank}*", "" );
393             }
394 
395             if ( Analyzer.WAB.equals( key ) && value.length() == 0 )
396             {
397                 // provide useful default
398                 value = "src/main/webapp/";
399             }
400 
401             transformedInstructions.put( key, value );
402         }
403         return transformedInstructions;
404     }
405 
406 
407     protected boolean reportErrors( String prefix, Analyzer analyzer )
408     {
409         List<String> errors = analyzer.getErrors();
410         List<String> warnings = analyzer.getWarnings();
411 
412         for ( String msg : warnings )
413         {
414             getLog().warn( prefix + " : " + msg );
415         }
416 
417         boolean hasErrors = false;
418         String fileNotFound = "Input file does not exist: ";
419         for ( String msg : errors )
420         {
421             if ( msg.startsWith(fileNotFound) && msg.endsWith( "~" ) )
422             {
423                 // treat as warning; this error happens when you have duplicate entries in Include-Resource
424                 String duplicate = Processor.removeDuplicateMarker( msg.substring( fileNotFound.length() ) );
425                 getLog().warn( prefix + " : Duplicate path '" + duplicate + "' in Include-Resource" );
426             }
427             else
428             {
429                 getLog().error( prefix + " : " + msg );
430                 hasErrors = true;
431             }
432         }
433         return hasErrors;
434     }
435 
436 
437     protected void execute(Map<String, String> originalInstructions,
438                            ClassPathItem[] classpath) throws MojoExecutionException
439     {
440         try
441         {
442             File jarFile = new File( getBuildDirectory(), getBundleName( project ) );
443             Builder builder = buildOSGiBundle( project, originalInstructions, classpath );
444             boolean hasErrors = reportErrors( "Bundle " + project.getArtifact(), builder );
445             if ( hasErrors )
446             {
447                 String failok = builder.getProperty( "-failok" );
448                 if ( null == failok || "false".equalsIgnoreCase( failok ) )
449                 {
450                     jarFile.delete();
451 
452                     throw new MojoFailureException( "Error(s) found in bundle configuration" );
453                 }
454             }
455 
456             // attach bundle to maven project
457             jarFile.getParentFile().mkdirs();
458             builder.getJar().write( jarFile );
459 
460             Artifact mainArtifact = project.getArtifact();
461 
462             if ( "bundle".equals( mainArtifact.getType() ) )
463             {
464                 // workaround for MNG-1682: force maven to install artifact using the "jar" handler
465                 mainArtifact.setArtifactHandler( m_artifactHandlerManager.getArtifactHandler( "jar" ) );
466             }
467 
468             boolean customClassifier = null != classifier && classifier.trim().length() > 0;
469             boolean customPackaging = null != packaging && packaging.trim().length() > 0;
470 
471             if ( customClassifier && customPackaging )
472             {
473                 m_projectHelper.attachArtifact( project, packaging, classifier, jarFile );
474             }
475             else if ( customClassifier )
476             {
477                 m_projectHelper.attachArtifact( project, jarFile, classifier );
478             }
479             else if ( customPackaging )
480             {
481                 m_projectHelper.attachArtifact( project, packaging, jarFile );
482             }
483             else
484             {
485                 mainArtifact.setFile( jarFile );
486             }
487 
488             if ( unpackBundle )
489             {
490                 unpackBundle( jarFile );
491             }
492 
493             if ( manifestLocation != null )
494             {
495                 File outputFile = new File( manifestLocation, "MANIFEST.MF" );
496 
497                 try
498                 {
499                     ManifestPlugin.writeManifest( builder, outputFile, niceManifest, exportScr, scrLocation, buildContext, getLog() );
500                 }
501                 catch ( IOException e )
502                 {
503                     getLog().error( "Error trying to write Manifest to file " + outputFile, e );
504                 }
505             }
506 
507             // cleanup...
508             builder.close();
509         }
510         catch ( MojoFailureException e )
511         {
512             getLog().error( e.getLocalizedMessage() );
513             throw new MojoExecutionException( "Error(s) found in bundle configuration", e );
514         }
515         catch ( Exception e )
516         {
517             getLog().error( "An internal error occurred", e );
518             throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
519         }
520     }
521 
522 
523     protected Builder getOSGiBuilder( MavenProject currentProject, Map<String, String> originalInstructions,
524         ClassPathItem[] classpath ) throws Exception
525     {
526         Properties properties = new Properties();
527         properties.putAll( getDefaultProperties( currentProject ) );
528         properties.putAll( transformDirectives( originalInstructions ) );
529 
530         // process overrides from project
531         final Map<String, String> addProps = new HashMap<>();
532         for ( Entry<Object, Object> entry : currentProject.getProperties().entrySet() )
533         {
534             final String key = entry.getKey().toString();
535             if ( key.startsWith(BUNDLE_PLUGIN_EXTENSION) )
536             {
537                 final String oKey = key.substring(BUNDLE_PLUGIN_EXTENSION.length());
538                 final String currentValue = properties.getProperty(oKey);
539                 if ( currentValue == null )
540                 {
541                     addProps.put(oKey, entry.getValue().toString());
542                 }
543                 else
544                 {
545                     addProps.put(oKey, currentValue + ',' + entry.getValue());
546                 }
547             }
548             if ( key.startsWith(BUNDLE_PLUGIN_PREPEND_EXTENSION) )
549             {
550                 final String oKey = key.substring(BUNDLE_PLUGIN_PREPEND_EXTENSION.length());
551                 final String currentValue = properties.getProperty(oKey);
552                 if ( currentValue == null )
553                 {
554                     addProps.put(oKey, entry.getValue().toString());
555                 }
556                 else
557                 {
558                     addProps.put(oKey, entry.getValue() + "," + currentValue);
559                 }
560             }
561         }
562         properties.putAll( addProps );
563         for ( String key : addProps.keySet() )
564         {
565             properties.remove(BUNDLE_PLUGIN_EXTENSION + key);
566             properties.remove(BUNDLE_PLUGIN_PREPEND_EXTENSION + key);
567         }
568 
569         if (properties.getProperty("Bundle-Activator") != null
570                 && properties.getProperty("Bundle-Activator").isEmpty())
571         {
572             properties.remove("Bundle-Activator");
573         }
574         if (properties.containsKey("-disable-plugin"))
575         {
576             String[] disabled = properties.remove("-disable-plugin").toString().replaceAll(" ", "").split(",");
577             String[] enabled = properties.getProperty(Analyzer.PLUGIN, "").replaceAll(" ", "").split(",");
578             Set<String> plugin = new LinkedHashSet<>();
579             plugin.addAll(Arrays.asList(enabled));
580             plugin.removeAll(Arrays.asList(disabled));
581             StringBuilder sb = new StringBuilder();
582             for (String s : plugin)
583             {
584                 if (sb.length() > 0)
585                 {
586                     sb.append(",");
587                 }
588                 sb.append(s);
589             }
590             properties.setProperty(Analyzer.PLUGIN, sb.toString());
591         }
592 
593         Builder builder = new Builder();
594         synchronized ( BundlePlugin.class ) // protect setBase...getBndLastModified which uses static DateFormat
595         {
596             builder.setBase( getBase( currentProject ) );
597         }
598         builder.setProperties( sanitize( properties ) );
599         if ( classpath != null )
600         {
601             Jar[] jars = new Jar[ classpath.length ];
602             for ( int i = 0; i < classpath.length; i++ ) {
603                 jars[i] = new Jar( classpath[i].id, classpath[i].file );
604             }
605             builder.setClasspath( jars );
606         }
607 
608         return builder;
609     }
610 
611 
612     protected static Properties sanitize( Properties properties )
613     {
614         // convert any non-String keys/values to Strings
615         Properties sanitizedEntries = new Properties();
616         for ( Iterator<Map.Entry<Object,Object>> itr = properties.entrySet().iterator(); itr.hasNext(); )
617         {
618             Map.Entry<Object,Object> entry = itr.next();
619             if ( !(entry.getKey() instanceof String) )
620             {
621                 String key = sanitize(entry.getKey());
622                 if ( !properties.containsKey( key ) )
623                 {
624                     sanitizedEntries.setProperty( key, sanitize( entry.getValue() ) );
625                 }
626                 itr.remove();
627             }
628             else if ( !(entry.getValue() instanceof String) )
629             {
630                 entry.setValue( sanitize( entry.getValue() ) );
631             }
632         }
633         properties.putAll( sanitizedEntries );
634         return properties;
635     }
636 
637 
638     protected static String sanitize( Object value )
639     {
640         if ( value instanceof String )
641         {
642             return ( String ) value;
643         }
644         else if ( value instanceof Iterable )
645         {
646             String delim = "";
647             StringBuilder buf = new StringBuilder();
648             for ( Object i : ( Iterable<?> ) value )
649             {
650                 buf.append( delim ).append( i );
651                 delim = ", ";
652             }
653             return buf.toString();
654         }
655         else if ( value.getClass().isArray() )
656         {
657             String delim = "";
658             StringBuilder buf = new StringBuilder();
659             for ( int i = 0, len = Array.getLength( value ); i < len; i++ )
660             {
661                 buf.append( delim ).append( Array.get( value, i ) );
662                 delim = ", ";
663             }
664             return buf.toString();
665         }
666         else
667         {
668             return String.valueOf( value );
669         }
670     }
671 
672 
673     protected void addMavenInstructions(MavenProject currentProject, Builder builder) throws Exception
674     {
675         if ( currentProject.getBasedir() != null )
676         {
677             // update BND instructions to add included Maven resources
678             includeMavenResources(currentProject, builder, getLog());
679 
680             // Fixup error messages
681             includeJava9Fixups(currentProject, builder);
682 
683             // calculate default export/private settings based on sources
684             addLocalPackages(outputDirectory, builder);
685 
686             // tell BND where the current project source resides
687             addMavenSourcePath(currentProject, builder, getLog());
688         }
689 
690         // update BND instructions to embed selected Maven dependencies
691         Collection<Artifact> embeddableArtifacts = getEmbeddableArtifacts( currentProject, builder );
692         DependencyEmbedder dependencyEmbedder = new DependencyEmbedder(getLog(), embeddableArtifacts);
693         dependencyEmbedder.processHeaders(builder);
694 
695         Collection<Artifact> embeddedArtifacts = dependencyEmbedder.getEmbeddedArtifacts();
696         if ( !embeddedArtifacts.isEmpty() && createDependencyReducedPom )
697         {
698             Set<String> embeddedIds = new HashSet<String>();
699             for ( Artifact artifact : embeddedArtifacts )
700             {
701                 embeddedIds.add( getId( artifact ) );
702             }
703             createDependencyReducedPom( embeddedIds );
704 
705         }
706 
707         if ( dumpInstructions != null || getLog().isDebugEnabled() )
708         {
709             StringBuilder buf = new StringBuilder();
710             getLog().debug( "BND Instructions:" + NL + dumpInstructions( builder.getProperties(), buf ) );
711             if ( dumpInstructions != null )
712             {
713                 getLog().info( "Writing BND instructions to " + dumpInstructions );
714                 dumpInstructions.getParentFile().mkdirs();
715                 FileUtils.fileWrite( dumpInstructions, "# BND instructions" + NL + buf );
716             }
717         }
718 
719 
720 
721         if ( dumpClasspath != null || getLog().isDebugEnabled() )
722         {
723             StringBuilder buf = new StringBuilder();
724             getLog().debug("BND Classpath:" + NL + dumpClasspath(builder.getClasspath(), buf));
725             if ( dumpClasspath != null )
726             {
727                 getLog().info( "Writing BND classpath to " + dumpClasspath );
728                 dumpClasspath.getParentFile().mkdirs();
729                 FileUtils.fileWrite( dumpClasspath, "# BND classpath" + NL + buf );
730             }
731         }
732     }
733 
734 
735     // We need to find the direct dependencies that have been included in the uber JAR so that we can modify the
736     // POM accordingly.
737     private void createDependencyReducedPom( Set<String> artifactsToRemove )
738             throws IOException, ProjectBuildingException, DependencyTreeBuilderException {
739         Model model = project.getOriginalModel();
740         List<Dependency> dependencies = new ArrayList<Dependency>();
741 
742         boolean modified = false;
743 
744         List<Dependency> transitiveDeps = new ArrayList<Dependency>();
745 
746         for ( Artifact artifact : project.getArtifacts() )
747         {
748             if ( "pom".equals( artifact.getType() ) )
749             {
750                 // don't include pom type dependencies in dependency reduced pom
751                 continue;
752             }
753 
754             //promote
755             Dependency dep = new Dependency();
756             dep.setArtifactId( artifact.getArtifactId() );
757             if ( artifact.hasClassifier() )
758             {
759                 dep.setClassifier( artifact.getClassifier() );
760             }
761             dep.setGroupId( artifact.getGroupId() );
762             dep.setOptional( artifact.isOptional() );
763             dep.setScope( artifact.getScope() );
764             dep.setType( artifact.getType() );
765             dep.setVersion( artifact.getVersion() );
766 
767             //we'll figure out the exclusions in a bit.
768 
769             transitiveDeps.add( dep );
770         }
771         List<Dependency> origDeps = project.getDependencies();
772 
773         for (Dependency d : origDeps)
774         {
775             dependencies.add(d);
776 
777             String id = getId(d);
778 
779             if (artifactsToRemove.contains(id))
780             {
781                 modified = true;
782 
783                 dependencies.remove(d);
784             }
785         }
786 
787         // Check to see if we have a reduction and if so rewrite the POM.
788         if ( modified )
789         {
790             while ( modified )
791             {
792 
793                 model.setDependencies( dependencies );
794 
795                 if ( dependencyReducedPomLocation == null )
796                 {
797                     // MSHADE-123: We can't default to 'target' because it messes up uses of ${project.basedir}
798                     dependencyReducedPomLocation = new File ( project.getBasedir(), "dependency-reduced-pom.xml" );
799                 }
800 
801                 File f = dependencyReducedPomLocation;
802                 if ( f.exists() )
803                 {
804                     f.delete();
805                 }
806 
807                 Writer w = WriterFactory.newXmlWriter( f );
808 
809                 String origRelativePath = null;
810                 String replaceRelativePath = null;
811                 if ( model.getParent() != null)
812                 {
813                     origRelativePath = model.getParent().getRelativePath();
814 
815                 }
816                 replaceRelativePath = origRelativePath;
817 
818                 if ( origRelativePath == null )
819                 {
820                     origRelativePath = "../pom.xml";
821                 }
822 
823                 if ( model.getParent() != null )
824                 {
825                     File parentFile = new File( project.getBasedir(), model.getParent().getRelativePath() ).getCanonicalFile();
826                     if ( !parentFile.isFile() )
827                     {
828                         parentFile = new File( parentFile, "pom.xml");
829                     }
830 
831                     parentFile = parentFile.getCanonicalFile();
832 
833                     String relPath = RelativizePath.convertToRelativePath( parentFile, f );
834                     model.getParent().setRelativePath( relPath );
835                 }
836 
837                 try
838                 {
839                     PomWriter.write( w, model, true );
840                 }
841                 finally
842                 {
843                     if ( model.getParent() != null )
844                     {
845                         model.getParent().setRelativePath( replaceRelativePath );
846                     }
847                     w.close();
848                 }
849 
850                 MavenProject p2 = mavenProjectBuilder.build( f, localRepository, null );
851                 modified = updateExcludesInDeps( p2, dependencies, transitiveDeps );
852 
853             }
854 
855             project.setFile( dependencyReducedPomLocation );
856         }
857     }
858 
859     private String getId( Artifact artifact )
860     {
861         return getId( artifact.getGroupId(), artifact.getArtifactId(), artifact.getType(), artifact.getClassifier() );
862     }
863 
864     private String getId( Dependency dependency )
865     {
866         return getId( dependency.getGroupId(), dependency.getArtifactId(), dependency.getType(),
867                 dependency.getClassifier() );
868     }
869 
870     private String getId( String groupId, String artifactId, String type, String classifier )
871     {
872         return groupId + ":" + artifactId + ":" + type + ":" + ( ( classifier != null ) ? classifier : "" );
873     }
874 
875     public boolean updateExcludesInDeps( MavenProject project, List<Dependency> dependencies, List<Dependency> transitiveDeps )
876             throws DependencyTreeBuilderException
877     {
878         org.apache.maven.shared.dependency.tree.DependencyNode node = dependencyTreeBuilder.buildDependencyTree(project, localRepository, artifactFactory,
879                 artifactMetadataSource, null,
880                 artifactCollector);
881         boolean modified = false;
882         for (org.apache.maven.shared.dependency.tree.DependencyNode n2 : node.getChildren())
883         {
884             for (org.apache.maven.shared.dependency.tree.DependencyNode n3 : n2.getChildren())
885             {
886                 //anything two levels deep that is marked "included"
887                 //is stuff that was excluded by the original poms, make sure it
888                 //remains excluded IF promoting transitives.
889                 if (n3.getState() == org.apache.maven.shared.dependency.tree.DependencyNode.INCLUDED)
890                 {
891                     //check if it really isn't in the list of original dependencies.  Maven
892                     //prior to 2.0.8 may grab versions from transients instead of
893                     //from the direct deps in which case they would be marked included
894                     //instead of OMITTED_FOR_DUPLICATE
895 
896                     //also, if not promoting the transitives, level 2's would be included
897                     boolean found = false;
898                     for (Dependency dep : transitiveDeps)
899                     {
900                         if (dep.getArtifactId().equals(n3.getArtifact().getArtifactId()) && dep.getGroupId().equals(
901                                 n3.getArtifact().getGroupId()))
902                         {
903                             found = true;
904                         }
905 
906                     }
907 
908                     if (!found)
909                     {
910                         for (Dependency dep : dependencies)
911                         {
912                             if (dep.getArtifactId().equals(n2.getArtifact().getArtifactId())
913                                     && dep.getGroupId().equals(n2.getArtifact().getGroupId()))
914                             {
915                                 Exclusion exclusion = new Exclusion();
916                                 exclusion.setArtifactId(n3.getArtifact().getArtifactId());
917                                 exclusion.setGroupId(n3.getArtifact().getGroupId());
918                                 dep.addExclusion(exclusion);
919                                 modified = true;
920                                 break;
921                             }
922                         }
923                     }
924                 }
925             }
926         }
927         return modified;
928     }
929 
930 
931     protected Builder buildOSGiBundle(MavenProject currentProject, Map<String, String> originalInstructions,
932                                       ClassPathItem[] classpath) throws Exception
933     {
934         Builder builder = getOSGiBuilder( currentProject, originalInstructions, classpath );
935 
936         addMavenInstructions( currentProject, builder );
937 
938         builder.build();
939 
940         mergeMavenManifest(currentProject, builder);
941 
942         return builder;
943     }
944 
945 
946     protected static StringBuilder dumpInstructions( Properties properties, StringBuilder buf )
947     {
948         try
949         {
950             buf.append( "#-----------------------------------------------------------------------" + NL );
951             Properties stringProperties = new Properties();
952             for ( Enumeration<String> e = (Enumeration<String>) properties.propertyNames(); e.hasMoreElements(); )
953             {
954                 // we can only store String properties
955                 String key = e.nextElement();
956                 String value = properties.getProperty( key );
957                 if ( value != null )
958                 {
959                     stringProperties.setProperty( key, value );
960                 }
961             }
962             ByteArrayOutputStream out = new ByteArrayOutputStream();
963             stringProperties.store( out, null ); // properties encoding is 8859_1
964             buf.append( out.toString( "8859_1" ) );
965             buf.append("#-----------------------------------------------------------------------").append(NL);
966         }
967         catch ( Throwable e )
968         {
969             // ignore...
970         }
971         return buf;
972     }
973 
974 
975     protected static StringBuilder dumpClasspath( List<Jar> classpath, StringBuilder buf )
976     {
977         try
978         {
979             buf.append("#-----------------------------------------------------------------------").append(NL);
980             buf.append("-classpath:\\").append(NL);
981             for ( Iterator<Jar> i = classpath.iterator(); i.hasNext(); )
982             {
983                 File path = i.next().getSource();
984                 if ( path != null )
985                 {
986                     buf.append(' ').append(path.toString()).append(i.hasNext() ? ",\\" : "").append(NL);
987                 }
988             }
989             buf.append("#-----------------------------------------------------------------------").append(NL);
990         }
991         catch ( Throwable e )
992         {
993             // ignore...
994         }
995         return buf;
996     }
997 
998 
999     protected static StringBuilder dumpManifest( Manifest manifest, StringBuilder buf )
1000     {
1001         try
1002         {
1003             buf.append("#-----------------------------------------------------------------------").append(NL);
1004             ByteArrayOutputStream out = new ByteArrayOutputStream();
1005             ManifestWriter.outputManifest(manifest, out, true); // manifest encoding is UTF8
1006             buf.append( out.toString( "UTF8" ) );
1007             buf.append("#-----------------------------------------------------------------------").append(NL);
1008         }
1009         catch ( Throwable e )
1010         {
1011             // ignore...
1012         }
1013         return buf;
1014     }
1015 
1016 
1017     protected static void includeMavenResources( MavenProject currentProject, Analyzer analyzer, Log log )
1018     {
1019         // pass maven resource paths onto BND analyzer
1020         final String mavenResourcePaths = getMavenResourcePaths( currentProject, false );
1021         final String mavenTestResourcePaths = getMavenResourcePaths( currentProject, true );
1022         final String includeResource = analyzer.getProperty( Analyzer.INCLUDE_RESOURCE );
1023         if ( includeResource != null )
1024         {
1025             if ( includeResource.contains( MAVEN_RESOURCES ) || includeResource.contains( MAVEN_TEST_RESOURCES ) )
1026             {
1027                 String combinedResource = StringUtils.replace( includeResource, MAVEN_RESOURCES, mavenResourcePaths );
1028                 combinedResource = StringUtils.replace( combinedResource, MAVEN_TEST_RESOURCES, mavenTestResourcePaths );
1029                 if ( combinedResource.length() > 0 )
1030                 {
1031                     analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, combinedResource );
1032                 }
1033                 else
1034                 {
1035                     analyzer.unsetProperty( Analyzer.INCLUDE_RESOURCE );
1036                 }
1037             }
1038             else if ( mavenResourcePaths.length() > 0 )
1039             {
1040                 log.warn( Analyzer.INCLUDE_RESOURCE + ": overriding " + mavenResourcePaths + " with " + includeResource
1041                         + " (add " + MAVEN_RESOURCES + " if you want to include the maven resources)" );
1042             }
1043         }
1044         else if ( mavenResourcePaths.length() > 0 )
1045         {
1046             analyzer.setProperty( Analyzer.INCLUDE_RESOURCE, mavenResourcePaths );
1047         }
1048     }
1049 
1050 
1051     protected void mergeMavenManifest(MavenProject currentProject, Builder builder) throws Exception
1052     {
1053         Jar jar = builder.getJar();
1054 
1055         if ( getLog().isDebugEnabled() )
1056         {
1057             getLog().debug( "BND Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
1058         }
1059 
1060         boolean addMavenDescriptor = currentProject.getBasedir() != null;
1061 
1062         try
1063         {
1064             /*
1065              * Grab customized manifest entries from the maven-jar-plugin configuration
1066              */
1067             MavenArchiveConfiguration archiveConfig = JarPluginConfiguration.getArchiveConfiguration( currentProject );
1068             String mavenManifestText = new MavenArchiver().getManifest( currentProject, archiveConfig ).toString();
1069             addMavenDescriptor = addMavenDescriptor && archiveConfig.isAddMavenDescriptor();
1070 
1071             Manifest mavenManifest = new Manifest();
1072 
1073             // First grab the external manifest file (if specified and different to target location)
1074             File externalManifestFile = archiveConfig.getManifestFile();
1075             if ( null != externalManifestFile )
1076             {
1077                 if ( !externalManifestFile.isAbsolute() )
1078                 {
1079                     externalManifestFile = new File( currentProject.getBasedir(), externalManifestFile.getPath() );
1080                 }
1081                 if ( externalManifestFile.exists() && !externalManifestFile.equals( new File( manifestLocation, "MANIFEST.MF" ) ) )
1082                 {
1083                     InputStream mis = new FileInputStream( externalManifestFile );
1084                     mavenManifest.read( mis );
1085                     mis.close();
1086                 }
1087             }
1088 
1089             // Then apply customized entries from the jar plugin; note: manifest encoding is UTF8
1090             mavenManifest.read( new ByteArrayInputStream( mavenManifestText.getBytes( "UTF8" ) ) );
1091 
1092             if ( !archiveConfig.isManifestSectionsEmpty() )
1093             {
1094                 /*
1095                  * Add customized manifest sections (for some reason MavenArchiver doesn't do this for us)
1096                  */
1097                 List<ManifestSection> sections = archiveConfig.getManifestSections();
1098                 for ( Iterator<ManifestSection> i = sections.iterator(); i.hasNext(); )
1099                 {
1100                     ManifestSection section = i.next();
1101                     Attributes attributes = new Attributes();
1102 
1103                     if ( !section.isManifestEntriesEmpty() )
1104                     {
1105                         Map<String, String> entries = section.getManifestEntries();
1106                         for ( Iterator<Map.Entry<String, String>> j = entries.entrySet().iterator(); j.hasNext(); )
1107                         {
1108                             Map.Entry<String, String> entry = j.next();
1109                             attributes.putValue( entry.getKey(), entry.getValue() );
1110                         }
1111                     }
1112 
1113                     mavenManifest.getEntries().put( section.getName(), attributes );
1114                 }
1115             }
1116 
1117             Attributes mainMavenAttributes = mavenManifest.getMainAttributes();
1118             mainMavenAttributes.putValue( "Created-By", "Apache Maven Bundle Plugin" );
1119 
1120             String[] removeHeaders = builder.getProperty( Constants.REMOVEHEADERS, "" ).split( "," );
1121 
1122             // apply -removeheaders to the custom manifest
1123             for ( int i = 0; i < removeHeaders.length; i++ )
1124             {
1125                 for ( Iterator<Object> j = mainMavenAttributes.keySet().iterator(); j.hasNext(); )
1126                 {
1127                     if ( j.next().toString().matches( removeHeaders[i].trim() ) )
1128                     {
1129                         j.remove();
1130                     }
1131                 }
1132             }
1133 
1134             /*
1135              * Overlay generated bundle manifest with customized entries
1136              */
1137             Properties properties = builder.getProperties();
1138             Manifest bundleManifest = jar.getManifest();
1139             if ( properties.containsKey( "Merge-Headers" ) )
1140             {
1141                 Instructions instructions = new Instructions( ExtList.from(builder.getProperty("Merge-Headers")) );
1142                 mergeManifest( instructions, bundleManifest, mavenManifest );
1143             }
1144             else
1145             {
1146                 bundleManifest.getMainAttributes().putAll( mainMavenAttributes );
1147                 bundleManifest.getEntries().putAll( mavenManifest.getEntries() );
1148             }
1149 
1150             // adjust the import package attributes so that optional dependencies use
1151             // optional resolution.
1152             String importPackages = bundleManifest.getMainAttributes().getValue( "Import-Package" );
1153             if ( importPackages != null )
1154             {
1155                 Set optionalPackages = getOptionalPackages( currentProject);
1156 
1157                 Map<String, ? extends Map<String, String>> values;
1158                 try (Analyzer analyzer = new Analyzer()) {
1159                     values = analyzer.parseHeader( importPackages );
1160                 }
1161                 for ( Map.Entry<String, ? extends Map<String, String>> entry : values.entrySet() )
1162                 {
1163                     String pkg = entry.getKey();
1164                     Map<String, String> options = entry.getValue();
1165                     if ( !options.containsKey( "resolution:" ) && optionalPackages.contains( pkg ) )
1166                     {
1167                         options.put( "resolution:", "optional" );
1168                     }
1169                 }
1170                 String result = Processor.printClauses( values );
1171                 bundleManifest.getMainAttributes().putValue( "Import-Package", result );
1172             }
1173 
1174             jar.setManifest( bundleManifest );
1175         }
1176         catch ( Exception e )
1177         {
1178             getLog().warn( "Unable to merge Maven manifest: " + e.getLocalizedMessage() );
1179         }
1180 
1181         if ( addMavenDescriptor )
1182         {
1183             doMavenMetadata( currentProject, jar );
1184         }
1185 
1186         if ( getLog().isDebugEnabled() )
1187         {
1188             getLog().debug( "Final Manifest:" + NL + dumpManifest( jar.getManifest(), new StringBuilder() ) );
1189         }
1190 
1191         builder.setJar( jar );
1192     }
1193 
1194 
1195     protected static void mergeManifest( Instructions instructions, Manifest... manifests ) throws IOException
1196     {
1197         for ( int i = manifests.length - 2; i >= 0; i-- )
1198         {
1199             Manifest mergedManifest = manifests[i];
1200             Manifest manifest = manifests[i + 1];
1201             Attributes mergedMainAttributes = mergedManifest.getMainAttributes();
1202             Attributes mainAttributes = manifest.getMainAttributes();
1203             Attributes filteredMainAttributes = filterAttributes( instructions, mainAttributes, null );
1204             if ( !filteredMainAttributes.isEmpty() )
1205             {
1206                 mergeAttributes( mergedMainAttributes, filteredMainAttributes );
1207             }
1208             Map<String, Attributes> mergedEntries = mergedManifest.getEntries();
1209             Map<String, Attributes> entries = manifest.getEntries();
1210             for ( Map.Entry<String, Attributes> entry : entries.entrySet() )
1211             {
1212                 String name = entry.getKey();
1213                 Attributes attributes = entry.getValue();
1214                 Attributes filteredAttributes = filterAttributes( instructions, attributes, null );
1215                 if ( !filteredAttributes.isEmpty() )
1216                 {
1217                     Attributes mergedAttributes = mergedManifest.getAttributes( name );
1218                     if ( mergedAttributes != null)
1219                     {
1220                         mergeAttributes(mergedAttributes, filteredAttributes);
1221                     }
1222                     else
1223                     {
1224                         mergedEntries.put(name, filteredAttributes);
1225                     }
1226                 }
1227             }
1228         }
1229     }
1230 
1231 
1232     /**
1233      * @see Analyzer#filter
1234      */
1235     private static Attributes filterAttributes(Instructions instructions, Attributes source, Set<Instruction> nomatch) {
1236         Attributes result = new Attributes();
1237         Map<String, Object> keys = new TreeMap<String, Object>();
1238         for ( Object key : source.keySet() )
1239         {
1240             keys.put( key.toString(), key );
1241         }
1242 
1243         List<Instruction> filters = new ArrayList<Instruction>( instructions.keySet() );
1244         if (nomatch == null)
1245         {
1246             nomatch = Create.set();
1247         }
1248         for ( Instruction instruction : filters ) {
1249             boolean match = false;
1250             for (Iterator<Map.Entry<String, Object>> i = keys.entrySet().iterator(); i.hasNext();)
1251             {
1252                 Map.Entry<String, Object> entry = i.next();
1253                 String key = entry.getKey();
1254                 if ( instruction.matches( key ) )
1255                 {
1256                     match = true;
1257                     if (!instruction.isNegated()) {
1258                         Object name = entry.getValue();
1259                         Object value = source.get( name );
1260                         result.put( name, value );
1261                     }
1262                     i.remove(); // Can never match again for another pattern
1263                 }
1264             }
1265             if (!match && !instruction.isAny())
1266                 nomatch.add(instruction);
1267         }
1268 
1269         /*
1270          * Tricky. If we have umatched instructions they might indicate that we
1271          * want to have multiple decorators for the same package. So we check
1272          * the unmatched against the result list. If then then match and have
1273          * actually interesting properties then we merge them
1274          */
1275 
1276         for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
1277             Instruction instruction = i.next();
1278 
1279             // We assume the user knows what he is
1280             // doing and inserted a literal. So
1281             // we ignore any not matched literals
1282             // #252, we should not be negated to make it a constant
1283             if (instruction.isLiteral() && !instruction.isNegated()) {
1284                 Object key = keys.get( instruction.getLiteral() );
1285                 if ( key != null )
1286                 {
1287                     Object value = source.get( key );
1288                     result.put( key, value );
1289                 }
1290                 i.remove();
1291                 continue;
1292             }
1293 
1294             // Not matching a negated instruction looks
1295             // like an error ... Though so, but
1296             // in the second phase of Export-Package
1297             // the !package will never match anymore.
1298             if (instruction.isNegated()) {
1299                 i.remove();
1300                 continue;
1301             }
1302 
1303             // An optional instruction should not generate
1304             // an error
1305             if (instruction.isOptional()) {
1306                 i.remove();
1307                 continue;
1308             }
1309         }
1310         return result;
1311     }
1312 
1313 
1314     private static void mergeAttributes( Attributes... attributesArray ) throws IOException
1315     {
1316         for ( int i = attributesArray.length - 2; i >= 0; i-- )
1317         {
1318             Attributes mergedAttributes = attributesArray[i];
1319             Attributes attributes = attributesArray[i + 1];
1320             for ( Map.Entry<Object, Object> entry : attributes.entrySet() )
1321             {
1322                 Object name = entry.getKey();
1323                 String value = (String) entry.getValue();
1324                 String oldValue = (String) mergedAttributes.put( name, value );
1325                 if ( oldValue != null )
1326                 {
1327                     Parameters mergedClauses = OSGiHeader.parseHeader(oldValue);
1328                     Parameters clauses = OSGiHeader.parseHeader( value );
1329                     if ( !mergedClauses.isEqual( clauses) )
1330                     {
1331                         for ( Map.Entry<String, Attrs> clauseEntry : clauses.entrySet() )
1332                         {
1333                             String clause = clauseEntry.getKey();
1334                             Attrs attrs = clauseEntry.getValue();
1335                             Attrs mergedAttrs = mergedClauses.get( clause );
1336                             if ( mergedAttrs == null)
1337                             {
1338                                 mergedClauses.put( clause, attrs );
1339                             }
1340                             else if ( !mergedAttrs.isEqual(attrs) )
1341                             {
1342                                 for ( Map.Entry<String,String> adentry : attrs.entrySet() )
1343                                 {
1344                                     String adname = adentry.getKey();
1345                                     String ad = adentry.getValue();
1346                                     if ( mergedAttrs.containsKey( adname ) )
1347                                     {
1348                                         Attrs.Type type = attrs.getType( adname );
1349                                         switch (type)
1350                                         {
1351                                             case VERSIONS:
1352                                             case STRINGS:
1353                                             case LONGS:
1354                                             case DOUBLES:
1355                                                 ExtList<String> mergedAd = ExtList.from( mergedAttrs.get( adname ) );
1356                                                 ExtList.from( ad ).addAll( ExtList.from( ad ) );
1357                                                 mergedAttrs.put(adname, mergedAd.join() );
1358                                                 break;
1359                                         }
1360                                     }
1361                                     else
1362                                     {
1363                                         mergedAttrs.put( adname, ad );
1364                                     }
1365                                 }
1366                             }
1367                         }
1368                         mergedAttributes.put( name, Processor.printClauses( mergedClauses ) );
1369                     }
1370                 }
1371             }
1372         }
1373     }
1374 
1375 
1376     protected Set<String> getOptionalPackages(MavenProject currentProject) throws IOException, MojoExecutionException
1377     {
1378         ArrayList<Artifact> inscope = new ArrayList<Artifact>();
1379         final Collection<Artifact> artifacts = getSelectedDependencies(currentProject.getArtifacts() );
1380         for ( Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); )
1381         {
1382             Artifact artifact = it.next();
1383             if ( artifact.getArtifactHandler().isAddedToClasspath() )
1384             {
1385                 inscope.add( artifact );
1386             }
1387         }
1388 
1389         HashSet<String> optionalArtifactIds = new HashSet<String>();
1390         for ( Iterator<Artifact> it = inscope.iterator(); it.hasNext(); )
1391         {
1392             Artifact artifact = it.next();
1393             if ( artifact.isOptional() )
1394             {
1395                 String id = artifact.toString();
1396                 if ( artifact.getScope() != null )
1397                 {
1398                     // strip the scope...
1399                     id = id.replaceFirst( ":[^:]*$", "" );
1400                 }
1401                 optionalArtifactIds.add( id );
1402             }
1403 
1404         }
1405 
1406         HashSet<String> required = new HashSet<String>();
1407         HashSet<String> optional = new HashSet<String>();
1408         for ( Iterator<Artifact> it = inscope.iterator(); it.hasNext(); )
1409         {
1410             Artifact artifact = it.next();
1411             File file = getFile( artifact );
1412             if ( file == null )
1413             {
1414                 continue;
1415             }
1416 
1417             Jar jar = new Jar( artifact.getArtifactId(), file );
1418             if ( isTransitivelyOptional( optionalArtifactIds, artifact ) )
1419             {
1420                 optional.addAll( jar.getPackages() );
1421             }
1422             else
1423             {
1424                 required.addAll( jar.getPackages() );
1425             }
1426             jar.close();
1427         }
1428 
1429         optional.removeAll( required );
1430         return optional;
1431     }
1432 
1433 
1434     /**
1435      * Check to see if any dependency along the dependency trail of
1436      * the artifact is optional.
1437      *
1438      * @param artifact
1439      */
1440     protected boolean isTransitivelyOptional( HashSet<String> optionalArtifactIds, Artifact artifact )
1441     {
1442         List<String> trail = artifact.getDependencyTrail();
1443         for ( Iterator<String> iterator = trail.iterator(); iterator.hasNext(); )
1444         {
1445             String next = iterator.next();
1446             if ( optionalArtifactIds.contains( next ) )
1447             {
1448                 return true;
1449             }
1450         }
1451         return false;
1452     }
1453 
1454 
1455     private void unpackBundle( File jarFile )
1456     {
1457         File outputDir = getOutputDirectory();
1458         if ( null == outputDir )
1459         {
1460             outputDir = new File( getBuildDirectory(), "classes" );
1461         }
1462 
1463         try
1464         {
1465             /*
1466              * this directory must exist before unpacking, otherwise the plexus
1467              * unarchiver decides to use the current working directory instead!
1468              */
1469             if ( !outputDir.exists() )
1470             {
1471                 outputDir.mkdirs();
1472             }
1473 
1474             UnArchiver unArchiver = m_archiverManager.getUnArchiver( "jar" );
1475             unArchiver.setDestDirectory( outputDir );
1476             unArchiver.setSourceFile( jarFile );
1477             unArchiver.extract();
1478         }
1479         catch ( Exception e )
1480         {
1481             getLog().error( "Problem unpacking " + jarFile + " to " + outputDir, e );
1482         }
1483     }
1484 
1485 
1486     protected static String removeTagFromInstruction( String instruction, String tag )
1487     {
1488         StringBuffer buf = new StringBuffer();
1489 
1490         String[] clauses = instruction.split( "," );
1491         for ( int i = 0; i < clauses.length; i++ )
1492         {
1493             String clause = clauses[i].trim();
1494             if ( !tag.equals( clause ) )
1495             {
1496                 if ( buf.length() > 0 )
1497                 {
1498                     buf.append( ',' );
1499                 }
1500                 buf.append( clause );
1501             }
1502         }
1503 
1504         return buf.toString();
1505     }
1506 
1507 
1508     private static Map<String, String> getProperties( Model projectModel, String prefix )
1509     {
1510         Map<String, String> properties = new LinkedHashMap<String, String>();
1511         Method methods[] = Model.class.getDeclaredMethods();
1512         for ( int i = 0; i < methods.length; i++ )
1513         {
1514             String name = methods[i].getName();
1515             if ( name.startsWith( "get" ) )
1516             {
1517                 try
1518                 {
1519                     Object v = methods[i].invoke( projectModel, null );
1520                     if ( v != null )
1521                     {
1522                         name = prefix + Character.toLowerCase( name.charAt( 3 ) ) + name.substring( 4 );
1523                         if ( v.getClass().isArray() )
1524                             properties.put( name, Arrays.asList( ( Object[] ) v ).toString() );
1525                         else
1526                             properties.put( name, v.toString() );
1527 
1528                     }
1529                 }
1530                 catch ( Exception e )
1531                 {
1532                     // too bad
1533                 }
1534             }
1535         }
1536         return properties;
1537     }
1538 
1539 
1540     private static StringBuffer printLicenses( List<License> licenses )
1541     {
1542         if ( licenses == null || licenses.size() == 0 )
1543             return null;
1544         StringBuffer sb = new StringBuffer();
1545         String del = "";
1546         for ( Iterator<License> i = licenses.iterator(); i.hasNext(); )
1547         {
1548             License l = i.next();
1549             String url = l.getUrl();
1550             if ( url == null )
1551                 continue;
1552             sb.append( del );
1553             sb.append( url );
1554             del = ", ";
1555         }
1556         if ( sb.length() == 0 )
1557             return null;
1558         return sb;
1559     }
1560 
1561 
1562     /**
1563      * @param jar
1564      * @throws IOException
1565      */
1566     private void doMavenMetadata( MavenProject currentProject, Jar jar ) throws IOException
1567     {
1568         String path = "META-INF/maven/" + currentProject.getGroupId() + "/" + currentProject.getArtifactId();
1569 
1570         File pomFile = currentProject.getFile();
1571         if ( pomFile == null || !pomFile.exists() )
1572         {
1573             pomFile = new File( currentProject.getBasedir(), "pom.xml" );
1574         }
1575         if ( pomFile.exists() )
1576         {
1577             jar.putResource( path + "/pom.xml", new FileResource( pomFile ) );
1578         }
1579 
1580         Properties p = new Properties();
1581         p.put( "version", currentProject.getVersion() );
1582         p.put( "groupId", currentProject.getGroupId() );
1583         p.put( "artifactId", currentProject.getArtifactId() );
1584         ByteArrayOutputStream out = new ByteArrayOutputStream();
1585         p.store( out, "Generated by org.apache.felix.bundleplugin" );
1586         jar.putResource( path + "/pom.properties", new EmbeddedResource( out.toByteArray(), System.currentTimeMillis() ) );
1587     }
1588 
1589 
1590     protected ClassPathItem[] getClasspath(MavenProject currentProject) throws IOException, MojoExecutionException
1591     {
1592         List<ClassPathItem> list = new ArrayList<ClassPathItem>( currentProject.getArtifacts().size() + 1 );
1593 
1594         String d = currentProject.getBuild() != null ? currentProject.getBuild().getOutputDirectory() : null;
1595         if ( d != null )
1596         {
1597             list.add( new ClassPathItem( ".", new File( d ) ) );
1598         }
1599 
1600         final Collection<Artifact> artifacts = getSelectedDependencies(currentProject.getArtifacts() );
1601         for ( Artifact artifact : artifacts  )
1602         {
1603             if ( artifact.getArtifactHandler().isAddedToClasspath() && !Artifact.SCOPE_TEST.equals( artifact.getScope() ) )
1604             {
1605                 File file = getFile( artifact );
1606                 if ( file == null )
1607                 {
1608                     getLog().warn(
1609                             "File is not available for artifact " + artifact + " in project "
1610                                     + currentProject.getArtifact() );
1611                     continue;
1612                 }
1613                 ClassPathItem jar = new ClassPathItem( artifact.getArtifactId(), file );
1614                 list.add( jar );
1615             }
1616         }
1617         ClassPathItem[] cp = new ClassPathItem[list.size()];
1618         list.toArray( cp );
1619 
1620         return cp;
1621     }
1622 
1623 
1624     private Collection<Artifact> getSelectedDependencies(Collection<Artifact> artifacts) throws MojoExecutionException
1625     {
1626         if ( null == excludeDependencies || excludeDependencies.length() == 0 )
1627         {
1628             return artifacts;
1629         }
1630         else if ( "true".equalsIgnoreCase( excludeDependencies ) )
1631         {
1632             return Collections.emptyList();
1633         }
1634 
1635         Collection<Artifact> selectedDependencies = new LinkedHashSet<Artifact>( artifacts );
1636         DependencyExcluder excluder = new DependencyExcluder(artifacts );
1637         excluder.processHeaders( excludeDependencies );
1638         selectedDependencies.removeAll( excluder.getExcludedArtifacts() );
1639 
1640         return selectedDependencies;
1641     }
1642 
1643 
1644     /**
1645      * Get the file for an Artifact
1646      *
1647      * @param artifact
1648      */
1649     protected File getFile( Artifact artifact )
1650     {
1651         return artifact.getFile();
1652     }
1653 
1654 
1655     private static void header( Properties properties, String key, Object value )
1656     {
1657         if ( value == null )
1658             return;
1659 
1660         if ( value instanceof Collection && ( ( Collection ) value ).isEmpty() )
1661             return;
1662 
1663         properties.put( key, value.toString().replaceAll( "[\r\n]", "" ) );
1664     }
1665 
1666 
1667     /**
1668      * Convert a Maven version into an OSGi compliant version
1669      *
1670      * @param version Maven version
1671      * @return the OSGi version
1672      */
1673     protected String convertVersionToOsgi( String version )
1674     {
1675         return getMaven2OsgiConverter().getVersion( version );
1676     }
1677 
1678 
1679     /**
1680      * TODO this should return getMaven2Osgi().getBundleFileName( project.getArtifact() )
1681      */
1682     protected String getBundleName( MavenProject currentProject )
1683     {
1684         String extension;
1685         try
1686         {
1687             extension = currentProject.getArtifact().getArtifactHandler().getExtension();
1688         }
1689         catch ( Throwable e )
1690         {
1691             extension = currentProject.getArtifact().getType();
1692         }
1693         if ( StringUtils.isEmpty( extension ) || "bundle".equals( extension ) || "pom".equals( extension ) )
1694         {
1695             extension = "jar"; // just in case maven gets confused
1696         }
1697         if ( null != classifier && classifier.trim().length() > 0 )
1698         {
1699             return finalName + '-' + classifier + '.' + extension;
1700         }
1701         return finalName + '.' + extension;
1702     }
1703 
1704 
1705     protected String getBuildDirectory()
1706     {
1707         return buildDirectory;
1708     }
1709 
1710 
1711     protected void setBuildDirectory( String _buildirectory )
1712     {
1713         buildDirectory = _buildirectory;
1714     }
1715 
1716 
1717     protected Properties getDefaultProperties( MavenProject currentProject )
1718     {
1719         Properties properties = new Properties();
1720 
1721         String bsn;
1722         try
1723         {
1724             bsn = getMaven2OsgiConverter().getBundleSymbolicName( currentProject.getArtifact() );
1725         }
1726         catch ( Exception e )
1727         {
1728             bsn = currentProject.getGroupId() + "." + currentProject.getArtifactId();
1729         }
1730 
1731         // Setup defaults
1732         properties.put( MAVEN_SYMBOLICNAME, bsn );
1733         properties.put( Analyzer.BUNDLE_SYMBOLICNAME, bsn );
1734         properties.put( Analyzer.IMPORT_PACKAGE, "*" );
1735         properties.put( Analyzer.BUNDLE_VERSION, getMaven2OsgiConverter().getVersion( currentProject.getVersion() ) );
1736 
1737         // remove the extraneous Include-Resource and Private-Package entries from generated manifest
1738         properties.put( Constants.REMOVEHEADERS, Analyzer.INCLUDE_RESOURCE + ',' + Analyzer.PRIVATE_PACKAGE );
1739 
1740         header( properties, Analyzer.BUNDLE_DESCRIPTION, currentProject.getDescription() );
1741         StringBuffer licenseText = printLicenses( currentProject.getLicenses() );
1742         if ( licenseText != null )
1743         {
1744             header( properties, Analyzer.BUNDLE_LICENSE, licenseText );
1745         }
1746         header( properties, Analyzer.BUNDLE_NAME, currentProject.getName() );
1747 
1748         if ( currentProject.getOrganization() != null )
1749         {
1750             if ( currentProject.getOrganization().getName() != null )
1751             {
1752                 String organizationName = currentProject.getOrganization().getName();
1753                 header( properties, Analyzer.BUNDLE_VENDOR, organizationName );
1754                 properties.put( "project.organization.name", organizationName );
1755                 properties.put( "pom.organization.name", organizationName );
1756             }
1757             if ( currentProject.getOrganization().getUrl() != null )
1758             {
1759                 String organizationUrl = currentProject.getOrganization().getUrl();
1760                 header( properties, Analyzer.BUNDLE_DOCURL, organizationUrl );
1761                 properties.put( "project.organization.url", organizationUrl );
1762                 properties.put( "pom.organization.url", organizationUrl );
1763             }
1764         }
1765 
1766         properties.putAll( currentProject.getProperties() );
1767         properties.putAll( currentProject.getModel().getProperties() );
1768 
1769         for ( Iterator<String> i = currentProject.getFilters().iterator(); i.hasNext(); )
1770         {
1771             File filterFile = new File( i.next() );
1772             if ( filterFile.isFile() )
1773             {
1774                 properties.putAll( PropertyUtils.loadProperties( filterFile ) );
1775             }
1776         }
1777 
1778         if ( m_mavenSession != null )
1779         {
1780             try
1781             {
1782                 // don't pass upper-case session settings to bnd as they end up in the manifest
1783                 Properties sessionProperties = m_mavenSession.getExecutionProperties();
1784                 for ( Enumeration<String> e = (Enumeration<String>) sessionProperties.propertyNames(); e.hasMoreElements(); )
1785                 {
1786                     String key = e.nextElement();
1787                     if ( key.length() > 0 && !Character.isUpperCase( key.charAt( 0 ) ) )
1788                     {
1789                         properties.put( key, sessionProperties.getProperty( key ) );
1790                     }
1791                 }
1792             }
1793             catch ( Exception e )
1794             {
1795                 getLog().warn( "Problem with Maven session properties: " + e.getLocalizedMessage() );
1796             }
1797         }
1798 
1799         properties.putAll( getProperties( currentProject.getModel(), "project.build." ) );
1800         properties.putAll( getProperties( currentProject.getModel(), "pom." ) );
1801         properties.putAll( getProperties( currentProject.getModel(), "project." ) );
1802 
1803         properties.put( "project.baseDir", getBase( currentProject ) );
1804         properties.put( "project.build.directory", getBuildDirectory() );
1805         properties.put( "project.build.outputdirectory", getOutputDirectory() );
1806 
1807         properties.put( "classifier", classifier == null ? "" : classifier );
1808 
1809         // Add default plugins
1810         header( properties, Analyzer.PLUGIN, BlueprintPlugin.class.getName() + ","
1811                                            + SpringXMLType.class.getName() + ","
1812                                            + JpaPlugin.class.getName() );
1813 
1814         return properties;
1815     }
1816 
1817 
1818     protected static File getBase( MavenProject currentProject )
1819     {
1820         return currentProject.getBasedir() != null ? currentProject.getBasedir() : new File( "" );
1821     }
1822 
1823 
1824     protected File getOutputDirectory()
1825     {
1826         return outputDirectory;
1827     }
1828 
1829 
1830     protected void setOutputDirectory( File _outputDirectory )
1831     {
1832         outputDirectory = _outputDirectory;
1833     }
1834 
1835 
1836     private static void addLocalPackages( File outputDirectory, Analyzer analyzer ) throws IOException
1837     {
1838         Packages packages = new Packages();
1839 
1840         if ( outputDirectory != null && outputDirectory.isDirectory() )
1841         {
1842             // scan classes directory for potential packages
1843             DirectoryScanner scanner = new DirectoryScanner();
1844             scanner.setBasedir( outputDirectory );
1845             scanner.setIncludes( new String[]
1846                 { "**/*.class" } );
1847 
1848             scanner.addDefaultExcludes();
1849             scanner.scan();
1850 
1851             String[] paths = scanner.getIncludedFiles();
1852             for ( int i = 0; i < paths.length; i++ )
1853             {
1854                 packages.put( analyzer.getPackageRef( getPackageName( paths[i] ) ) );
1855             }
1856         }
1857 
1858         Packages exportedPkgs = new Packages();
1859         Packages privatePkgs = new Packages();
1860 
1861         boolean noprivatePackages = "!*".equals( analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) );
1862 
1863         for ( PackageRef pkg : packages.keySet() )
1864         {
1865             // mark all source packages as private by default (can be overridden by export list)
1866             privatePkgs.put( pkg );
1867 
1868             // we can't export the default package (".") and we shouldn't export internal packages
1869             String fqn = pkg.getFQN();
1870             if ( noprivatePackages || !( ".".equals( fqn ) || fqn.contains( ".internal" ) || fqn.contains( ".impl" ) ) )
1871             {
1872                 exportedPkgs.put( pkg );
1873             }
1874         }
1875 
1876         Properties properties = analyzer.getProperties();
1877         String exported = properties.getProperty( Analyzer.EXPORT_PACKAGE );
1878         if ( exported == null )
1879         {
1880             if ( !properties.containsKey( Analyzer.EXPORT_CONTENTS ) )
1881             {
1882                 // no -exportcontents overriding the exports, so use our computed list
1883                 for ( Attrs attrs : exportedPkgs.values() )
1884                 {
1885                     attrs.put( Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first" );
1886                 }
1887                 properties.setProperty( Analyzer.EXPORT_PACKAGE, Processor.printClauses( exportedPkgs ) );
1888             }
1889             else
1890             {
1891                 // leave Export-Package empty (but non-null) as we have -exportcontents
1892                 properties.setProperty( Analyzer.EXPORT_PACKAGE, "" );
1893             }
1894         }
1895         else if ( exported.indexOf( LOCAL_PACKAGES ) >= 0 )
1896         {
1897             String newExported = StringUtils.replace( exported, LOCAL_PACKAGES, Processor.printClauses( exportedPkgs ) );
1898             properties.setProperty( Analyzer.EXPORT_PACKAGE, newExported );
1899         }
1900 
1901         String internal = properties.getProperty( Analyzer.PRIVATE_PACKAGE );
1902         if ( internal == null )
1903         {
1904             if ( !privatePkgs.isEmpty() )
1905             {
1906                 for ( Attrs attrs : privatePkgs.values() )
1907                 {
1908                     attrs.put( Constants.SPLIT_PACKAGE_DIRECTIVE, "merge-first" );
1909                 }
1910                 properties.setProperty( Analyzer.PRIVATE_PACKAGE, Processor.printClauses( privatePkgs ) );
1911             }
1912             else
1913             {
1914                 // if there are really no private packages then use "!*" as this will keep the Bnd Tool happy
1915                 properties.setProperty( Analyzer.PRIVATE_PACKAGE, "!*" );
1916             }
1917         }
1918         else if ( internal.indexOf( LOCAL_PACKAGES ) >= 0 )
1919         {
1920             String newInternal = StringUtils.replace( internal, LOCAL_PACKAGES, Processor.printClauses( privatePkgs ) );
1921             properties.setProperty( Analyzer.PRIVATE_PACKAGE, newInternal );
1922         }
1923     }
1924 
1925 
1926     private static String getPackageName( String filename )
1927     {
1928         int n = filename.lastIndexOf( File.separatorChar );
1929         return n < 0 ? "." : filename.substring( 0, n ).replace( File.separatorChar, '.' );
1930     }
1931 
1932 
1933     private static List<Resource> getMavenResources( MavenProject currentProject, boolean test )
1934     {
1935         List<Resource> resources = new ArrayList<Resource>( test ? currentProject.getTestResources() : currentProject.getResources() );
1936 
1937         if ( currentProject.getCompileSourceRoots() != null )
1938         {
1939             // also scan for any "packageinfo" files lurking in the source folders
1940             final List<String> packageInfoIncludes = Collections.singletonList( "**/packageinfo" );
1941             for ( Iterator<String> i = currentProject.getCompileSourceRoots().iterator(); i.hasNext(); )
1942             {
1943                 String sourceRoot = i.next();
1944                 Resource packageInfoResource = new Resource();
1945                 packageInfoResource.setDirectory( sourceRoot );
1946                 packageInfoResource.setIncludes( packageInfoIncludes );
1947                 resources.add( packageInfoResource );
1948             }
1949         }
1950 
1951         return resources;
1952     }
1953 
1954 
1955     protected static String getMavenResourcePaths( MavenProject currentProject, boolean test )
1956     {
1957         final String basePath = currentProject.getBasedir().getAbsolutePath();
1958 
1959         Set<String> pathSet = new LinkedHashSet<String>();
1960         for ( Iterator<Resource> i = getMavenResources( currentProject, test ).iterator(); i.hasNext(); )
1961         {
1962             Resource resource = i.next();
1963 
1964             final String sourcePath = resource.getDirectory();
1965             final String targetPath = resource.getTargetPath();
1966 
1967             // ignore empty or non-local resources
1968             if ( new File( sourcePath ).exists() && ( ( targetPath == null ) || ( targetPath.indexOf( ".." ) < 0 ) ) )
1969             {
1970                 DirectoryScanner scanner = new DirectoryScanner();
1971 
1972                 scanner.setBasedir( sourcePath );
1973                 if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() )
1974                 {
1975                     scanner.setIncludes( resource.getIncludes().toArray( EMPTY_STRING_ARRAY ) );
1976                 }
1977                 else
1978                 {
1979                     scanner.setIncludes( DEFAULT_INCLUDES );
1980                 }
1981 
1982                 if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() )
1983                 {
1984                     scanner.setExcludes( resource.getExcludes().toArray( EMPTY_STRING_ARRAY ) );
1985                 }
1986 
1987                 scanner.addDefaultExcludes();
1988                 scanner.scan();
1989 
1990                 List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );
1991 
1992                 for ( Iterator<String> j = includedFiles.iterator(); j.hasNext(); )
1993                 {
1994                     String name = j.next();
1995                     String path = sourcePath + '/' + name;
1996 
1997                     // make relative to project
1998                     if ( path.startsWith( basePath ) )
1999                     {
2000                         if ( path.length() == basePath.length() )
2001                         {
2002                             path = ".";
2003                         }
2004                         else
2005                         {
2006                             path = path.substring( basePath.length() + 1 );
2007                         }
2008                     }
2009 
2010                     // replace windows backslash with a slash
2011                     // this is a workaround for a problem with bnd 0.0.189
2012                     if ( File.separatorChar != '/' )
2013                     {
2014                         name = name.replace( File.separatorChar, '/' );
2015                         path = path.replace( File.separatorChar, '/' );
2016                     }
2017 
2018                     // copy to correct place
2019                     path = name + '=' + path;
2020                     if ( targetPath != null )
2021                     {
2022                         path = targetPath + '/' + path;
2023                     }
2024 
2025                     // use Bnd filtering?
2026                     if ( resource.isFiltering() )
2027                     {
2028                         path = '{' + path + '}';
2029                     }
2030 
2031                     pathSet.add( path );
2032                 }
2033             }
2034         }
2035 
2036         StringBuffer resourcePaths = new StringBuffer();
2037         for ( Iterator<String> i = pathSet.iterator(); i.hasNext(); )
2038         {
2039             resourcePaths.append( i.next() );
2040             if ( i.hasNext() )
2041             {
2042                 resourcePaths.append( ',' );
2043             }
2044         }
2045 
2046         return resourcePaths.toString();
2047     }
2048 
2049 
2050     protected Collection<Artifact> getEmbeddableArtifacts(MavenProject currentProject, Analyzer analyzer)
2051         throws MojoExecutionException
2052     {
2053         final Collection<Artifact> artifacts;
2054 
2055         String embedTransitive = analyzer.getProperty( DependencyEmbedder.EMBED_TRANSITIVE );
2056         if (Boolean.valueOf(embedTransitive))
2057         {
2058             // includes transitive dependencies
2059             artifacts = currentProject.getArtifacts();
2060         }
2061         else
2062         {
2063             // only includes direct dependencies
2064             artifacts = currentProject.getDependencyArtifacts();
2065         }
2066 
2067         return getSelectedDependencies(artifacts );
2068     }
2069 
2070 
2071     protected static void addMavenSourcePath( MavenProject currentProject, Analyzer analyzer, Log log )
2072     {
2073         // pass maven source paths onto BND analyzer
2074         StringBuilder mavenSourcePaths = new StringBuilder();
2075         StringBuilder mavenTestSourcePaths = new StringBuilder();
2076         Map<StringBuilder, List<String>> map = new HashMap<StringBuilder, List<String>>(2);
2077         map.put(mavenSourcePaths, currentProject.getCompileSourceRoots() );
2078         map.put(mavenTestSourcePaths, currentProject.getTestCompileSourceRoots() );
2079         for ( Map.Entry<StringBuilder, List<String>> entry : map.entrySet() )
2080         {
2081             List<String> compileSourceRoots = entry.getValue();
2082             if ( compileSourceRoots != null )
2083             {
2084                 StringBuilder sourcePaths = entry.getKey();
2085                 for ( Iterator<String> i = compileSourceRoots.iterator(); i.hasNext(); )
2086                 {
2087                     if ( sourcePaths.length() > 0 )
2088                     {
2089                         sourcePaths.append( ',' );
2090                     }
2091                     sourcePaths.append( i.next() );
2092                 }
2093             }
2094         }
2095         final String sourcePath = analyzer.getProperty( Analyzer.SOURCEPATH );
2096         if ( sourcePath != null )
2097         {
2098             if ( sourcePath.contains(MAVEN_SOURCES) || sourcePath.contains(MAVEN_TEST_RESOURCES) )
2099             {
2100                 String combinedSource = StringUtils.replace( sourcePath, MAVEN_SOURCES, mavenSourcePaths.toString() );
2101                 combinedSource = StringUtils.replace( combinedSource, MAVEN_TEST_SOURCES, mavenTestSourcePaths.toString() );
2102                 if ( combinedSource.length() > 0 )
2103                 {
2104                     analyzer.setProperty( Analyzer.SOURCEPATH, combinedSource );
2105                 }
2106                 else
2107                 {
2108                     analyzer.unsetProperty( Analyzer.SOURCEPATH );
2109                 }
2110             }
2111             else if ( mavenSourcePaths.length() > 0 )
2112             {
2113                 log.warn( Analyzer.SOURCEPATH + ": overriding " + mavenSourcePaths + " with " + sourcePath + " (add "
2114                     + MAVEN_SOURCES + " if you want to include the maven sources)" );
2115             }
2116             else if ( mavenTestSourcePaths.length() > 0 )
2117             {
2118                 log.warn( Analyzer.SOURCEPATH + ": overriding " + mavenTestSourcePaths + " with " + sourcePath + " (add "
2119                         + MAVEN_TEST_SOURCES + " if you want to include the maven test sources)" );
2120             }
2121         }
2122         else if ( mavenSourcePaths.length() > 0 )
2123         {
2124             analyzer.setProperty( Analyzer.SOURCEPATH, mavenSourcePaths.toString() );
2125         }
2126     }
2127 
2128     /**
2129      * Downgrade the message "Classes found in the wrong directory" to a warning. This allows the plugin
2130      * to process a multi-release JAR (see JEP 238, http://openjdk.java.net/jeps/238).
2131      * 
2132      * Note that the version-specific paths will NOT be visible at runtime nor processed by bnd for
2133      * imported packages etc. This will not be possible until a runtime solution for multi-release
2134      * JARs exists in OSGi. This fix only allows these JARs to be processed at all and to be usable on
2135      * Java 8 (and below), and also on Java 9 where the version-specific customizations are optional.
2136      */
2137     protected static void includeJava9Fixups(MavenProject currentProject, Analyzer analyzer)
2138     {
2139         final String classesInWrongDirError = "Classes found in the wrong directory";
2140         final String newFixup = "Classes found in the wrong directory;"
2141             + Analyzer.FIXUPMESSAGES_IS_DIRECTIVE + "="
2142             + Analyzer.FIXUPMESSAGES_IS_WARNING;
2143 
2144         String fixups = analyzer.getProperty(Analyzer.FIXUPMESSAGES);
2145         if (fixups != null && !fixups.isEmpty()) {
2146             if (!fixups.contains(classesInWrongDirError)) {
2147                 fixups += "," + newFixup;
2148             }
2149         } else {
2150             fixups = newFixup;
2151         }
2152         analyzer.setProperty(Analyzer.FIXUPMESSAGES, fixups);
2153     }
2154 
2155     static class ClassPathItem {
2156         final String id;
2157         final File file;
2158 
2159         public ClassPathItem(String id, File file) {
2160             this.id = id;
2161             this.file = file;
2162         }
2163     }
2164 }