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.File;
23  import java.io.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.util.Iterator;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Map.Entry;
33  import java.util.Properties;
34  import java.util.jar.Manifest;
35  
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugin.MojoFailureException;
38  import org.apache.maven.plugin.logging.Log;
39  import org.apache.maven.plugins.annotations.Component;
40  import org.apache.maven.plugins.annotations.LifecyclePhase;
41  import org.apache.maven.plugins.annotations.Mojo;
42  import org.apache.maven.plugins.annotations.Parameter;
43  import org.apache.maven.plugins.annotations.ResolutionScope;
44  import org.apache.maven.project.MavenProject;
45  import org.apache.maven.shared.dependency.graph.DependencyNode;
46  import org.codehaus.plexus.util.Scanner;
47  import org.osgi.service.metatype.MetaTypeService;
48  import org.sonatype.plexus.build.incremental.BuildContext;
49  
50  import aQute.bnd.osgi.Analyzer;
51  import aQute.bnd.osgi.Builder;
52  import aQute.bnd.osgi.Instructions;
53  import aQute.bnd.osgi.Jar;
54  import aQute.bnd.osgi.Resource;
55  import aQute.lib.collections.ExtList;
56  
57  
58  /**
59   * Generate an OSGi manifest for this project
60   */
61  @Mojo( name = "manifest", requiresDependencyResolution = ResolutionScope.TEST,
62         threadSafe = true,
63         defaultPhase = LifecyclePhase.PROCESS_CLASSES)
64  public class ManifestPlugin extends BundlePlugin
65  {
66      /**
67       * When true, generate the manifest by rebuilding the full bundle in memory
68       */
69      @Parameter( property = "rebuildBundle" )
70      protected boolean rebuildBundle;
71  
72      /**
73       * When true, manifest generation on incremental builds is supported in IDEs like Eclipse.
74       * Please note that the underlying BND library does not support incremental build, which means
75       * always the whole manifest and SCR metadata is generated.
76       */
77      @Parameter( property = "supportIncrementalBuild" )
78      private boolean supportIncrementalBuild;
79  
80      @Component
81      private BuildContext buildContext;
82  
83      @Override
84      protected void execute( MavenProject project, DependencyNode dependencyGraph, Map<String, String> instructions, Properties properties, Jar[] classpath )
85          throws MojoExecutionException
86      {
87  
88          // in incremental build execute manifest generation only when explicitly activated
89          // and when any java file was touched since last build
90          if (buildContext.isIncremental() && !(supportIncrementalBuild && anyJavaSourceFileTouchedSinceLastBuild())) {
91              getLog().debug("Skipping manifest generation because no java source file was added, updated or removed since last build.");
92              return;
93          }
94  
95          Analyzer analyzer;
96          try
97          {
98              analyzer = getAnalyzer(project, dependencyGraph, instructions, properties, classpath);
99          }
100         catch ( FileNotFoundException e )
101         {
102             throw new MojoExecutionException( "Cannot find " + e.getMessage()
103                 + " (manifest goal must be run after compile phase)", e );
104         }
105         catch ( IOException e )
106         {
107             throw new MojoExecutionException( "Error trying to generate Manifest", e );
108         }
109         catch ( MojoFailureException e )
110         {
111             getLog().error( e.getLocalizedMessage() );
112             throw new MojoExecutionException( "Error(s) found in manifest configuration", e );
113         }
114         catch ( Exception e )
115         {
116             getLog().error( "An internal error occurred", e );
117             throw new MojoExecutionException( "Internal error in maven-bundle-plugin", e );
118         }
119 
120         File outputFile = new File( manifestLocation, "MANIFEST.MF" );
121 
122         try
123         {
124             writeManifest( analyzer, outputFile, niceManifest, exportScr, scrLocation, buildContext, getLog() );
125         }
126         catch ( Exception e )
127         {
128             throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
129         }
130         finally
131         {
132             try
133             {
134                 analyzer.close();
135             }
136             catch ( IOException e )
137             {
138                 throw new MojoExecutionException( "Error trying to write Manifest to file " + outputFile, e );
139             }
140         }
141     }
142 
143     /**
144      * Checks if any *.java file was added, updated or removed since last build in any source directory.
145      */
146     private boolean anyJavaSourceFileTouchedSinceLastBuild() {
147         @SuppressWarnings("unchecked")
148         List<String> sourceDirectories = project.getCompileSourceRoots();
149         for (String sourceDirectory : sourceDirectories) {
150             File directory = new File(sourceDirectory);
151             Scanner scanner = buildContext.newScanner(directory);
152             Scanner deleteScanner = buildContext.newDeleteScanner(directory);
153             if (containsJavaFile(scanner) || containsJavaFile(deleteScanner)) {
154                 return true;
155             }
156         }
157         return false;
158     }
159     private boolean containsJavaFile(Scanner scanner) {
160         String[] includes = new String[] { "**/*.java" };
161         scanner.setIncludes(includes);
162         scanner.scan();
163         return scanner.getIncludedFiles().length > 0;
164     }
165 
166     public Manifest getManifest( MavenProject project, DependencyNode dependencyGraph, Jar[] classpath ) throws IOException, MojoFailureException,
167         MojoExecutionException, Exception
168     {
169         return getManifest( project, dependencyGraph, new LinkedHashMap<String, String>(), new Properties(), classpath, buildContext );
170     }
171 
172 
173     public Manifest getManifest( MavenProject project, DependencyNode dependencyGraph, Map<String, String> instructions, Properties properties, Jar[] classpath,
174             BuildContext buildContext) throws IOException, MojoFailureException, MojoExecutionException, Exception
175     {
176         Analyzer analyzer = getAnalyzer(project, dependencyGraph, instructions, properties, classpath);
177 
178         Jar jar = analyzer.getJar();
179         Manifest manifest = jar.getManifest();
180 
181         if (exportScr)
182         {
183             exportScr(analyzer, jar, scrLocation, buildContext, getLog() );
184         }
185 
186         // cleanup...
187         analyzer.close();
188 
189         return manifest;
190     }
191 
192     private static void exportScr(Analyzer analyzer, Jar jar, File scrLocation, BuildContext buildContext, Log log ) throws Exception {
193         log.debug("Export SCR metadata to: " + scrLocation.getPath());
194         scrLocation.mkdirs();
195 
196      // export SCR metadata files from OSGI-INF/
197         Map<String, Resource> scrDir = jar.getDirectories().get("OSGI-INF");
198         if (scrDir != null) {
199             for (Map.Entry<String, Resource> entry : scrDir.entrySet()) {
200                 String path = entry.getKey();
201                 Resource resource = entry.getValue();
202                 writeSCR(resource, new File(scrLocation, path), buildContext,
203                         log);
204             }
205         }
206 
207         // export metatype files from OSGI-INF/metatype
208         Map<String,Resource> metatypeDir = jar.getDirectories().get(MetaTypeService.METATYPE_DOCUMENTS_LOCATION);
209         if (metatypeDir != null) {
210             for (Map.Entry<String, Resource> entry : metatypeDir.entrySet())
211             {
212                 String path = entry.getKey();
213                 Resource resource = entry.getValue();
214                 writeSCR(resource, new File(scrLocation, path), buildContext, log);
215             }
216         }
217 
218     }
219 
220     private static void writeSCR(Resource resource, File destination, BuildContext buildContext, Log log ) throws Exception
221     {
222         log.debug("Write SCR file: " + destination.getPath());
223         destination.getParentFile().mkdirs();
224         OutputStream os = buildContext.newFileOutputStream(destination);
225         try
226         {
227             resource.write(os);
228         }
229         finally
230         {
231             os.close();
232         }
233     }
234 
235     protected Analyzer getAnalyzer( MavenProject project, DependencyNode dependencyGraph, Jar[] classpath ) throws IOException, MojoExecutionException,
236         Exception
237     {
238         return getAnalyzer( project, dependencyGraph, new LinkedHashMap<String, String>(), new Properties(), classpath );
239     }
240 
241 
242     protected Analyzer getAnalyzer( MavenProject project, DependencyNode dependencyGraph, Map<String, String> instructions, Properties properties, Jar[] classpath )
243         throws IOException, MojoExecutionException, Exception
244     {
245         if ( rebuildBundle && supportedProjectTypes.contains( project.getArtifact().getType() ) )
246         {
247             return buildOSGiBundle( project, dependencyGraph, instructions, properties, classpath );
248         }
249 
250         File file = getOutputDirectory();
251         if ( file == null )
252         {
253             file = project.getArtifact().getFile();
254         }
255 
256         if ( !file.exists() )
257         {
258             if ( file.equals( getOutputDirectory() ) )
259             {
260                 file.mkdirs();
261             }
262             else
263             {
264                 throw new FileNotFoundException( file.getPath() );
265             }
266         }
267 
268         Builder analyzer = getOSGiBuilder( project, instructions, properties, classpath );
269 
270         analyzer.setJar( file );
271 
272         // calculateExportsFromContents when we have no explicit instructions defining
273         // the contents of the bundle *and* we are not analyzing the output directory,
274         // otherwise fall-back to addMavenInstructions approach
275 
276         boolean isOutputDirectory = file.equals( getOutputDirectory() );
277 
278         if ( analyzer.getProperty( Analyzer.EXPORT_PACKAGE ) == null
279             && analyzer.getProperty( Analyzer.EXPORT_CONTENTS ) == null
280             && analyzer.getProperty( Analyzer.PRIVATE_PACKAGE ) == null && !isOutputDirectory )
281         {
282             String export = calculateExportsFromContents( analyzer.getJar() );
283             analyzer.setProperty( Analyzer.EXPORT_PACKAGE, export );
284         }
285 
286         addMavenInstructions( project, dependencyGraph, analyzer );
287 
288         // if we spot Embed-Dependency and the bundle is "target/classes", assume we need to rebuild
289         if ( analyzer.getProperty( DependencyEmbedder.EMBED_DEPENDENCY ) != null && isOutputDirectory )
290         {
291             analyzer.build();
292         }
293         else
294         {
295             analyzer.mergeManifest( analyzer.getJar().getManifest() );
296             analyzer.getJar().setManifest( analyzer.calcManifest() );
297         }
298 
299         mergeMavenManifest( project, dependencyGraph, analyzer );
300 
301         boolean hasErrors = reportErrors( "Manifest " + project.getArtifact(), analyzer );
302         if ( hasErrors )
303         {
304             String failok = analyzer.getProperty( "-failok" );
305             if ( null == failok || "false".equalsIgnoreCase( failok ) )
306             {
307                 throw new MojoFailureException( "Error(s) found in manifest configuration" );
308             }
309         }
310 
311         Jar jar = analyzer.getJar();
312 
313         if ( unpackBundle )
314         {
315             File outputFile = getOutputDirectory();
316             for ( Entry<String, Resource> entry : jar.getResources().entrySet() )
317             {
318                 File entryFile = new File( outputFile, entry.getKey() );
319                 if ( !entryFile.exists() || entry.getValue().lastModified() == 0 )
320                 {
321                     entryFile.getParentFile().mkdirs();
322                     OutputStream os = buildContext.newFileOutputStream( entryFile );
323                     entry.getValue().write( os );
324                     os.close();
325                 }
326             }
327         }
328 
329         return analyzer;
330     }
331 
332 
333     public static void writeManifest( Analyzer analyzer, File outputFile, boolean niceManifest,
334             boolean exportScr, File scrLocation, BuildContext buildContext, Log log ) throws Exception
335     {
336         Properties properties = analyzer.getProperties();
337         Jar jar = analyzer.getJar();
338         Manifest manifest = jar.getManifest();
339         if ( outputFile.exists() && properties.containsKey( "Merge-Headers" ) )
340         {
341             Manifest analyzerManifest = manifest;
342             manifest = new Manifest();
343             InputStream inputStream = new FileInputStream( outputFile );
344             try
345             {
346                 manifest.read( inputStream );
347             }
348             finally
349             {
350                 inputStream.close();
351             }
352             Instructions instructions = new Instructions( ExtList.from( analyzer.getProperty("Merge-Headers") ) );
353             mergeManifest( instructions, manifest, analyzerManifest );
354         }
355         else
356         {
357             File parentFile = outputFile.getParentFile();
358             parentFile.mkdirs();
359         }
360         writeManifest( manifest, outputFile, niceManifest, buildContext, log );
361 
362         if (exportScr)
363         {
364             exportScr(analyzer, jar, scrLocation, buildContext, log);
365         }
366     }
367 
368 
369     public static void writeManifest( Manifest manifest, File outputFile, boolean niceManifest,
370             BuildContext buildContext, Log log ) throws IOException
371     {
372         log.debug("Write manifest to " + outputFile.getPath());
373         outputFile.getParentFile().mkdirs();
374 
375         OutputStream os = buildContext.newFileOutputStream( outputFile );
376         try
377         {
378             ManifestWriter.outputManifest( manifest, os, niceManifest );
379         }
380         finally
381         {
382             try
383             {
384                 os.close();
385             }
386             catch ( IOException e )
387             {
388                 // nothing we can do here
389             }
390         }
391     }
392 
393 
394     /*
395      * Patched version of bnd's Analyzer.calculateExportsFromContents
396      */
397     public static String calculateExportsFromContents( Jar bundle )
398     {
399         String ddel = "";
400         StringBuffer sb = new StringBuffer();
401         Map<String, Map<String, Resource>> map = bundle.getDirectories();
402         for ( Iterator<Entry<String, Map<String, Resource>>> i = map.entrySet().iterator(); i.hasNext(); )
403         {
404             //----------------------------------------------------
405             // should also ignore directories with no resources
406             //----------------------------------------------------
407             Entry<String, Map<String, Resource>> entry = i.next();
408             if ( entry.getValue() == null || entry.getValue().isEmpty() )
409                 continue;
410             //----------------------------------------------------
411             String directory = entry.getKey();
412             if ( directory.equals( "META-INF" ) || directory.startsWith( "META-INF/" ) )
413                 continue;
414             if ( directory.equals( "OSGI-OPT" ) || directory.startsWith( "OSGI-OPT/" ) )
415                 continue;
416             if ( directory.equals( "/" ) )
417                 continue;
418 
419             if ( directory.endsWith( "/" ) )
420                 directory = directory.substring( 0, directory.length() - 1 );
421 
422             directory = directory.replace( '/', '.' );
423             sb.append( ddel );
424             sb.append( directory );
425             ddel = ",";
426         }
427         return sb.toString();
428     }
429 }