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