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  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.IOException;
24  import java.util.Formatter;
25  import java.util.Set;
26  import java.util.TreeSet;
27  import java.util.jar.Attributes;
28  import java.util.jar.JarEntry;
29  import java.util.jar.JarInputStream;
30  import java.util.jar.Manifest;
31  import java.util.regex.Pattern;
32  
33  import org.apache.felix.utils.manifest.Clause;
34  import org.apache.felix.utils.manifest.Parser;
35  import org.apache.maven.plugin.AbstractMojo;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugin.MojoFailureException;
38  import org.apache.maven.plugins.annotations.Component;
39  import org.apache.maven.plugins.annotations.LifecyclePhase;
40  import org.apache.maven.plugins.annotations.Mojo;
41  import org.apache.maven.plugins.annotations.Parameter;
42  import org.apache.maven.project.MavenProject;
43  
44  /**
45   * Verifies OSGi bundle metadata contains valid entries.
46   *
47   * Supported checks in the current version:
48   * <ul>
49   * <li>All packages declared in the <a href="http://bnd.bndtools.org/heads/export_package.html">Export-Package</a> header are really included in the bundle.</li>
50   * </ul>
51   */
52  @Mojo(
53      name = "verify",
54      threadSafe = true,
55      defaultPhase = LifecyclePhase.VERIFY
56  )
57  public final class VerifyBundlePlugin
58      extends AbstractMojo
59  {
60  
61      private static final String EXPORT_PACKAGE = "Export-Package";
62  
63      private Pattern skipDirs = Pattern.compile( "(META|OSGI)-INF(.*)" );
64  
65      @Component
66      private MavenProject project;
67  
68      /**
69       * Flag to easily skip execution.
70       */
71      @Parameter( property = "skip", defaultValue = "false" )
72      protected boolean skip;
73  
74      /**
75       * Whether to fail on errors.
76       */
77      @Parameter( property = "failOnError", defaultValue = "true" )
78      protected boolean failOnError;
79  
80      @Override
81      public void execute()
82          throws MojoExecutionException, MojoFailureException
83      {
84          if ( skip )
85          {
86              getLog().info( "Skipping Verify execution" );
87              return;
88          }
89  
90          Set<String> packagesNotFound = checkPackages();
91  
92          if ( !packagesNotFound.isEmpty() )
93          {
94              Formatter formatter = new Formatter();
95              formatter.format( "Current bundle %s exports packages that do not exist:%n",
96                                project.getArtifact().getFile() );
97              for ( String packageNotFound : packagesNotFound )
98              {
99                  formatter.format( " * %s%n", packageNotFound );
100             }
101             formatter.format( "Please review the <Export-Package> instruction in the `configuration/instructions` element of the `maven-bundle-plugin`%n" );
102             formatter.format( "For more details, see http://bnd.bndtools.org/heads/export_package.html" );
103             String message = formatter.toString();
104             formatter.close();
105 
106             if ( failOnError )
107             {
108                 throw new MojoFailureException( message );
109             }
110             else
111             {
112                 getLog().warn( message );
113             }
114         }
115     }
116 
117     private Set<String> checkPackages()
118         throws MojoExecutionException
119     {
120         Set<String> packagesNotFound = new TreeSet<String>();
121 
122         File bundle = project.getArtifact().getFile();
123         JarInputStream input = null;
124 
125         try
126         {
127             input = new JarInputStream( new FileInputStream( bundle ) );
128             Manifest manifest = input.getManifest();
129             Attributes mainAttributes = manifest.getMainAttributes();
130             String exportPackage = mainAttributes.getValue( EXPORT_PACKAGE );
131 
132             if ( exportPackage == null || exportPackage.isEmpty() )
133             {
134                 getLog().warn( "Bundle manifest file does not contain valid 'Export-Package' OSGi entry, it will be ignored" );
135                 return packagesNotFound;
136             }
137 
138             // use a technique similar to the Sieve of Eratosthenes:
139             // create a set with all exported packages
140             Clause[] clauses = Parser.parseHeader( exportPackage );
141             for ( Clause clause : clauses )
142             {
143                 packagesNotFound.add( clause.getName() );
144             }
145 
146             // then, for each package found in the bundle, drop it from the set
147             JarEntry jarEntry = null;
148             while ( ( jarEntry = input.getNextJarEntry() ) != null )
149             {
150                 String entryName = jarEntry.getName();
151                 if ( jarEntry.isDirectory() && !skipDirs.matcher( entryName ).matches() )
152                 {
153                     if ( File.separatorChar == entryName.charAt( entryName.length() - 1 ) )
154                     {
155                         entryName = entryName.substring( 0, entryName.length() - 1 );
156                     }
157 
158                     String currentPackage = entryName.replace( File.separatorChar, '.' );
159                     packagesNotFound.remove( currentPackage );
160                 }
161             }
162 
163             // if there is a package not found in the set, it is a misconfigured package
164             return packagesNotFound;
165         }
166         catch ( IOException ioe )
167         {
168             throw new MojoExecutionException( "An error occurred while reading manifest file " + bundle, ioe );
169         }
170         finally
171         {
172             if ( input != null )
173             {
174                 try
175                 {
176                     input.close();
177                 }
178                 catch ( IOException e )
179                 {
180                     // close it quietly
181                 }
182             }
183         }
184     }
185 
186 }