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.BufferedReader;
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.net.URL;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.TreeSet;
35  import java.util.regex.Pattern;
36  
37  import javax.xml.transform.Transformer;
38  import javax.xml.transform.TransformerFactory;
39  import javax.xml.transform.stream.StreamResult;
40  import javax.xml.transform.stream.StreamSource;
41  
42  import aQute.bnd.service.AnalyzerPlugin;
43  import aQute.lib.osgi.Analyzer;
44  import aQute.lib.osgi.Jar;
45  import aQute.lib.osgi.Processor;
46  import aQute.lib.osgi.Resource;
47  import aQute.libg.generics.Create;
48  import aQute.libg.qtokens.QuotedTokenizer;
49  import aQute.libg.reporter.Reporter;
50  
51  
52  public class BlueprintPlugin implements AnalyzerPlugin
53  {
54  
55      static Pattern QN = Pattern.compile( "[_A-Za-z$][_A-Za-z0-9$]*(\\.[_A-Za-z$][_A-Za-z0-9$]*)*" );
56      static Pattern PATHS = Pattern.compile( ".*\\.xml" );
57  
58      Transformer transformer;
59  
60  
61      public BlueprintPlugin() throws Exception
62      {
63          transformer = getTransformer( getClass().getResource( "blueprint.xsl" ) );
64      }
65  
66  
67      public boolean analyzeJar( Analyzer analyzer ) throws Exception
68      {
69          transformer.setParameter( "nsh_interface",
70              analyzer.getProperty( "nsh_interface" ) != null ? analyzer.getProperty( "nsh_interface" ) : "" );
71          transformer.setParameter( "nsh_namespace",
72              analyzer.getProperty( "nsh_namespace" ) != null ? analyzer.getProperty( "nsh_namespace" ) : "" );
73  
74          Set<String> headers = Create.set();
75  
76          String bpHeader = analyzer.getProperty( "Bundle-Blueprint", "OSGI-INF/blueprint" );
77          Map<String, Map<String, String>> map = Processor.parseHeader( bpHeader, null );
78          for ( String root : map.keySet() )
79          {
80              Jar jar = analyzer.getJar();
81              Map<String, Resource> dir = jar.getDirectories().get( root );
82              if ( dir == null || dir.isEmpty() )
83              {
84                  Resource resource = jar.getResource( root );
85                  if ( resource != null )
86                      process( analyzer, root, resource, headers );
87                  return false;
88              }
89              for ( Map.Entry<String, Resource> entry : dir.entrySet() )
90              {
91                  String path = entry.getKey();
92                  Resource resource = entry.getValue();
93                  if ( PATHS.matcher( path ).matches() )
94                      process( analyzer, path, resource, headers );
95              }
96  
97          }
98  
99          // Group and analyze
100         Map<String, Set<Attribute>> hdrs = Create.map();
101         for ( String str : headers )
102         {
103             int idx = str.indexOf( ':' );
104             if ( idx < 0 )
105             {
106                 analyzer.warning( ( new StringBuilder( "Error analyzing services in blueprint resource: " ) ).append(
107                     str ).toString() );
108                 continue;
109             }
110             String h = str.substring( 0, idx ).trim();
111             String v = str.substring( idx + 1 ).trim();
112             Set<Attribute> att = hdrs.get( h );
113             if ( att == null )
114             {
115                 att = new TreeSet<Attribute>();
116                 hdrs.put( h, att );
117             }
118             att.addAll( parseHeader( v, null ) );
119         }
120         // Merge
121         for ( String header : hdrs.keySet() )
122         {
123             if ( "Import-Class".equals( header ) || "Import-Package".equals( header ) )
124             {
125                 Set<Attribute> newAttr = hdrs.get( header );
126                 for ( Attribute a : newAttr )
127                 {
128                     String pkg = a.getName();
129                     if ( "Import-Class".equals( header ) )
130                     {
131                         int n = a.getName().lastIndexOf( '.' );
132                         if ( n > 0 )
133                         {
134                             pkg = pkg.subSequence( 0, n ).toString();
135                         }
136                         else
137                         {
138                             continue;
139                         }
140                     }
141                     if ( !analyzer.getReferred().containsKey( pkg ) )
142                     {
143                         analyzer.getReferred().put( pkg, a.getProperties() );
144                     }
145                 }
146             }
147             else
148             {
149                 Set<Attribute> orgAttr = parseHeader( analyzer.getProperty( header ), null );
150                 Set<Attribute> newAttr = hdrs.get( header );
151                 for ( Iterator<Attribute> it = newAttr.iterator(); it.hasNext(); )
152                 {
153                     Attribute a = it.next();
154                     for ( Attribute b : orgAttr )
155                     {
156                         if ( b.getName().equals( a.getName() ) )
157                         {
158                             it.remove();
159                             break;
160                         }
161                     }
162                 }
163                 orgAttr.addAll( newAttr );
164                 // Rebuild from orgAttr
165                 StringBuilder sb = new StringBuilder();
166                 for ( Attribute a : orgAttr )
167                 {
168                     if ( sb.length() > 0 )
169                     {
170                         sb.append( "," );
171                     }
172                     sb.append( a.getName() );
173                     for ( Map.Entry<String, String> prop : a.getProperties().entrySet() )
174                     {
175                         sb.append( ';' ).append( prop.getKey() ).append( "=" );
176                         if ( prop.getValue().matches( "[0-9a-zA-Z_-]+" ) )
177                         {
178                             sb.append( prop.getValue() );
179                         }
180                         else
181                         {
182                             sb.append( "\"" );
183                             sb.append( prop.getValue().replace( "\"", "\\\"" ) );
184                             sb.append( "\"" );
185                         }
186                     }
187                 }
188                 analyzer.setProperty( header, sb.toString() );
189             }
190         }
191         return false;
192     }
193 
194 
195     private void process( Analyzer analyzer, String path, Resource resource, Set<String> headers )
196     {
197         InputStream in = null;
198         try
199         {
200             in = resource.openInputStream();
201 
202             // Retrieve headers
203             Set<String> set = analyze( in );
204             headers.addAll( set );
205         }
206         catch ( Exception e )
207         {
208             analyzer.error( ( new StringBuilder( "Unexpected exception in processing spring resources(" ) )
209                 .append( path ).append( "): " ).append( e ).toString() );
210         }
211         finally
212         {
213             try
214             {
215                 if ( in != null )
216                 {
217                     in.close();
218                 }
219             }
220             catch ( IOException e )
221             {
222             }
223         }
224     }
225 
226 
227     public Set<String> analyze( InputStream in ) throws Exception
228     {
229         Set<String> refers = new HashSet<String>();
230         ByteArrayOutputStream bout = new ByteArrayOutputStream();
231         javax.xml.transform.Result r = new StreamResult( bout );
232         javax.xml.transform.Source s = new StreamSource( in );
233         transformer.transform( s, r );
234         ByteArrayInputStream bin = new ByteArrayInputStream( bout.toByteArray() );
235         bout.close();
236         BufferedReader br = new BufferedReader( new InputStreamReader( bin ) );
237         for ( String line = br.readLine(); line != null; line = br.readLine() )
238         {
239             line = line.trim();
240             line = line.replace( ";availability:=mandatory", "" );
241             if ( line.length() > 0 )
242             {
243                 refers.add( line );
244             }
245         }
246 
247         br.close();
248         return refers;
249     }
250 
251 
252     protected Transformer getTransformer( URL url ) throws Exception
253     {
254         TransformerFactory tf = TransformerFactory.newInstance();
255         javax.xml.transform.Source source = new StreamSource( url.openStream() );
256         return tf.newTransformer( source );
257     }
258 
259     public static class Attribute implements Comparable<Attribute>
260     {
261         private final String name;
262         private final Map<String, String> properties;
263 
264 
265         public Attribute( String name, Map<String, String> properties )
266         {
267             this.name = name;
268             this.properties = properties;
269         }
270 
271 
272         public String getName()
273         {
274             return name;
275         }
276 
277 
278         public Map<String, String> getProperties()
279         {
280             return properties;
281         }
282 
283 
284         public int compareTo( Attribute a )
285         {
286             int c = name.compareTo( a.name );
287             if ( c == 0 )
288             {
289                 c = properties.equals( a.properties ) ? 0 : properties.size() < a.properties.size() ? -1 : properties
290                     .hashCode() < a.properties.hashCode() ? -1 : +1;
291             }
292             return c;
293         }
294 
295 
296         @Override
297         public boolean equals( Object o )
298         {
299             if ( this == o )
300                 return true;
301             if ( o == null || getClass() != o.getClass() )
302                 return false;
303 
304             Attribute attribute = ( Attribute ) o;
305 
306             if ( name != null ? !name.equals( attribute.name ) : attribute.name != null )
307                 return false;
308             if ( properties != null ? !properties.equals( attribute.properties ) : attribute.properties != null )
309                 return false;
310 
311             return true;
312         }
313 
314 
315         @Override
316         public int hashCode()
317         {
318             int result = name != null ? name.hashCode() : 0;
319             result = 31 * result + ( properties != null ? properties.hashCode() : 0 );
320             return result;
321         }
322     }
323 
324 
325     public static Set<Attribute> parseHeader( String value, Reporter logger )
326     {
327         if ( ( value == null ) || ( value.trim().length() == 0 ) )
328         {
329             return new TreeSet<Attribute>();
330         }
331         Set<Attribute> result = new TreeSet<Attribute>();
332         QuotedTokenizer qt = new QuotedTokenizer( value, ";=," );
333         char del = '\0';
334         do
335         {
336             boolean hadAttribute = false;
337             Map<String, String> clause = Create.map();
338             List<String> aliases = Create.list();
339             String name = qt.nextToken( ",;" );
340 
341             del = qt.getSeparator();
342             if ( ( name == null ) || ( name.length() == 0 ) )
343             {
344                 if ( ( logger != null ) && ( logger.isPedantic() ) )
345                 {
346                     logger
347                         .warning( "Empty clause, usually caused by repeating a comma without any name field or by having "
348                             + "spaces after the backslash of a property file: " + value );
349                 }
350 
351                 if ( name != null )
352                     continue;
353                 break;
354             }
355             name = name.trim();
356 
357             aliases.add( name );
358             String advalue;
359             while ( del == ';' )
360             {
361                 String adname = qt.nextToken();
362                 if ( ( del = qt.getSeparator() ) != '=' )
363                 {
364                     if ( ( hadAttribute ) && ( logger != null ) )
365                     {
366                         logger.error( "Header contains name field after attribute or directive: " + adname + " from "
367                             + value + ". Name fields must be consecutive, separated by a ';' like a;b;c;x=3;y=4" );
368                     }
369 
370                     if ( ( adname != null ) && ( adname.length() > 0 ) )
371                         aliases.add( adname.trim() );
372                 }
373                 else
374                 {
375                     advalue = qt.nextToken();
376                     if ( ( clause.containsKey( adname ) ) && ( logger != null ) && ( logger.isPedantic() ) )
377                     {
378                         logger.warning( "Duplicate attribute/directive name " + adname + " in " + value
379                             + ". This attribute/directive will be ignored" );
380                     }
381 
382                     if ( advalue == null )
383                     {
384                         if ( logger != null )
385                         {
386                             logger.error( "No value after '=' sign for attribute " + adname );
387                         }
388                         advalue = "";
389                     }
390                     clause.put( adname.trim(), advalue.trim() );
391                     del = qt.getSeparator();
392                     hadAttribute = true;
393                 }
394             }
395 
396             for ( String clauseName : aliases )
397             {
398                 result.add( new Attribute( clauseName, clause ) );
399             }
400         }
401         while ( del == ',' );
402         return result;
403     }
404 
405 }