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